@allbluecn/admin-app 0.3.2 → 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 (198) 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/providers/index.tsx +1 -1
  57. package/src/routeTree.gen.ts +67 -78
  58. package/src/routes/__root.tsx +4 -4
  59. package/src/routes/_nav/audit/route.lazy.tsx +1 -2
  60. package/src/routes/_nav/compliance/route.lazy.tsx +5 -3
  61. package/src/routes/_nav/components/route.lazy.tsx +14 -34
  62. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +18 -38
  63. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +18 -38
  64. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +6 -6
  65. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +3 -3
  66. package/src/routes/_nav/licenses/new/route.lazy.tsx +5 -5
  67. package/src/routes/_nav/licenses/route.lazy.tsx +1 -1
  68. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +2 -2
  69. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +10 -10
  70. package/src/routes/_nav/monitoring/errors/route.tsx +7 -0
  71. package/src/routes/_nav/monitoring/health/route.lazy.tsx +13 -15
  72. package/src/routes/_nav/monitoring/health/route.tsx +14 -0
  73. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +24 -9
  74. package/src/routes/_nav/monitoring/logs/route.tsx +13 -0
  75. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +12 -14
  76. package/src/routes/_nav/monitoring/metrics/route.tsx +7 -0
  77. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +14 -13
  78. package/src/routes/_nav/monitoring/overview/route.tsx +17 -0
  79. package/src/routes/_nav/monitoring/route.lazy.tsx +6 -6
  80. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +2 -2
  81. package/src/routes/_nav/permissions/route.lazy.tsx +21 -311
  82. package/src/routes/_nav/permissions/route.tsx +1 -1
  83. package/src/routes/_nav/route.tsx +5 -13
  84. package/src/routes/_nav/sso/route.lazy.tsx +2 -3
  85. package/src/routes/_nav/users/$userId/route.lazy.tsx +91 -156
  86. package/src/routes/_nav/users/-components/users-table.tsx +1 -1
  87. package/src/routes/_nav/users/route.lazy.tsx +1 -1
  88. package/src/routes/login/route.lazy.tsx +1 -1
  89. package/src/routes/setup/route.lazy.tsx +2 -2
  90. package/src/server/__tests__/admin-billing.test.ts +191 -16
  91. package/src/server/__tests__/compliance.test.ts +44 -7
  92. package/src/server/__tests__/permissions.test.ts +10 -5
  93. package/src/server/__tests__/prompt-defaults.test.ts +3 -3
  94. package/src/server/admin-ai-alerts.ts +2 -1
  95. package/src/server/admin-ai-usage.ts +2 -1
  96. package/src/server/ai-memory/store.ts +6 -2
  97. package/src/server/{serverFns/ai-prompts → ai-prompts}/prompt-defaults.ts +5 -3
  98. package/src/server/{serverFns/dependency → dependency}/helpers.ts +4 -3
  99. package/src/server/knowledge-public/github-sync.ts +2 -1
  100. package/src/server/license-issue.ts +2 -1
  101. package/src/server/middlewares/auth-rate-limit.ts +117 -0
  102. package/src/server/observability/health-check.ts +4 -1
  103. package/src/server/observability/metrics.ts +17 -15
  104. package/src/server/observability/request-middleware.ts +2 -1
  105. package/src/server/observability/timing.ts +14 -16
  106. package/src/server/serverFns/admin-billing.ts +107 -12
  107. package/src/server/serverFns/ai-memory/list-users.ts +2 -1
  108. package/src/server/serverFns/ai-prompts/activate.ts +2 -1
  109. package/src/server/serverFns/ai-prompts/get.ts +3 -2
  110. package/src/server/serverFns/ai-prompts/list.ts +3 -2
  111. package/src/server/serverFns/ai-prompts/rollback.ts +3 -1
  112. package/src/server/serverFns/ai-prompts/save.ts +5 -3
  113. package/src/server/serverFns/ai-provider/create.ts +4 -1
  114. package/src/server/serverFns/ai-provider/delete.ts +4 -1
  115. package/src/server/serverFns/ai-provider/get.ts +4 -1
  116. package/src/server/serverFns/ai-provider/list.ts +28 -1
  117. package/src/server/serverFns/ai-provider/seed-from-seed.ts +2 -1
  118. package/src/server/serverFns/ai-provider/update.ts +4 -1
  119. package/src/server/serverFns/anyhub/market.ts +8 -1
  120. package/src/server/serverFns/audit.ts +14 -1
  121. package/src/server/serverFns/auth/setup.ts +3 -1
  122. package/src/server/serverFns/compliance.ts +32 -3
  123. package/src/server/serverFns/components/import.ts +69 -14
  124. package/src/server/serverFns/components/list.ts +4 -1
  125. package/src/server/serverFns/components/mutations.ts +9 -1
  126. package/src/server/serverFns/dependency/list.ts +28 -5
  127. package/src/server/serverFns/dependency/sync.ts +7 -2
  128. package/src/server/serverFns/dependency/update.ts +4 -1
  129. package/src/server/serverFns/developer-log-auto-generate.ts +63 -32
  130. package/src/server/serverFns/developer-logs.ts +5 -1
  131. package/src/server/serverFns/invitations.ts +7 -1
  132. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +2 -1
  133. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +2 -1
  134. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +2 -1
  135. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +2 -1
  136. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +2 -1
  137. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +2 -1
  138. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  139. package/src/server/serverFns/knowledge-public/compliance/delete.ts +2 -1
  140. package/src/server/serverFns/knowledge-public/compliance/detail.ts +2 -1
  141. package/src/server/serverFns/knowledge-public/compliance/list.ts +2 -1
  142. package/src/server/serverFns/knowledge-public/compliance/update.ts +2 -1
  143. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  144. package/src/server/serverFns/knowledge-public/legal/delete.ts +2 -1
  145. package/src/server/serverFns/knowledge-public/legal/detail.ts +2 -1
  146. package/src/server/serverFns/knowledge-public/legal/list.ts +2 -1
  147. package/src/server/serverFns/knowledge-public/legal/update.ts +2 -1
  148. package/src/server/serverFns/licenses.ts +8 -2
  149. package/src/server/serverFns/monitoring/get-errors.ts +2 -1
  150. package/src/server/serverFns/monitoring/get-health.ts +2 -1
  151. package/src/server/serverFns/monitoring/get-metrics.ts +2 -1
  152. package/src/server/serverFns/monitoring/get-request-logs.ts +2 -1
  153. package/src/server/serverFns/monitoring/report-error.ts +2 -1
  154. package/src/server/serverFns/monitoring/reset.ts +2 -1
  155. package/src/server/serverFns/oauth-providers.ts +4 -1
  156. package/src/server/serverFns/permissions.ts +18 -11
  157. package/src/server/serverFns/sentry/config.ts +11 -8
  158. package/src/server/serverFns/sso-config.ts +17 -2
  159. package/src/server/serverFns/user/create.ts +6 -1
  160. package/src/server/serverFns/user/delete.ts +6 -1
  161. package/src/server/serverFns/user/detail.ts +5 -2
  162. package/src/server/serverFns/user/list.ts +19 -4
  163. package/src/server/serverFns/user/reset-password.ts +10 -2
  164. package/src/server/serverFns/user/update-role.ts +10 -2
  165. package/src/server/serverFns/user/update-status.ts +10 -2
  166. package/src/styles/globals.css +4 -0
  167. package/src/components/data-table/data-table-pagination.tsx +0 -112
  168. package/src/components/data-table/data-table.tsx +0 -218
  169. package/src/components/data-table/table-detail-layout.tsx +0 -98
  170. package/src/components/ui/alert-dialog.tsx +0 -187
  171. package/src/components/ui/alert.tsx +0 -75
  172. package/src/components/ui/avatar.tsx +0 -106
  173. package/src/components/ui/badge.tsx +0 -51
  174. package/src/components/ui/button.tsx +0 -57
  175. package/src/components/ui/card.tsx +0 -102
  176. package/src/components/ui/checkbox.tsx +0 -28
  177. package/src/components/ui/collapsible.tsx +0 -19
  178. package/src/components/ui/dialog.tsx +0 -155
  179. package/src/components/ui/dropdown-menu.tsx +0 -267
  180. package/src/components/ui/field.tsx +0 -236
  181. package/src/components/ui/input-group.tsx +0 -158
  182. package/src/components/ui/input.tsx +0 -19
  183. package/src/components/ui/label.tsx +0 -17
  184. package/src/components/ui/pagination.tsx +0 -130
  185. package/src/components/ui/select.tsx +0 -234
  186. package/src/components/ui/separator.tsx +0 -22
  187. package/src/components/ui/sheet.tsx +0 -135
  188. package/src/components/ui/sidebar.tsx +0 -721
  189. package/src/components/ui/skeleton.tsx +0 -13
  190. package/src/components/ui/switch.tsx +0 -29
  191. package/src/components/ui/table.tsx +0 -115
  192. package/src/components/ui/textarea.tsx +0 -17
  193. package/src/components/ui/toast.tsx +0 -282
  194. package/src/components/ui/tooltip.tsx +0 -63
  195. package/src/hooks/use-mobile.ts +0 -19
  196. package/src/lib/avatar.ts +0 -41
  197. /package/src/routes/{api.auth.$.ts → api/auth/$.ts} +0 -0
  198. /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
+ }