@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.
- package/package.json +19 -17
- package/src/components/admin/admin-user-menu.tsx +4 -4
- package/src/components/admin/developer-log.tsx +16 -32
- package/src/components/ai-memory/memory-facts-panel.tsx +30 -70
- package/src/components/ai-prompts/preview-panel.tsx +1 -1
- package/src/components/ai-prompts/prompt-editor.tsx +8 -9
- package/src/components/ai-prompts/prompt-table.tsx +6 -6
- package/src/components/ai-prompts/section-editor.tsx +3 -3
- package/src/components/ai-prompts/variable-panel.tsx +2 -2
- package/src/components/ai-prompts/version-history.tsx +21 -41
- package/src/components/ai-provider/model-list-editor.tsx +2 -3
- package/src/components/ai-provider/provider-form.tsx +7 -7
- package/src/components/ai-provider/provider-table.tsx +17 -40
- package/src/components/auth/admin-login-form.tsx +7 -7
- package/src/components/auth/icon-input.tsx +2 -2
- package/src/components/auth/password-input.tsx +2 -2
- package/src/components/component/component-form-dialog.tsx +10 -18
- package/src/components/component/component-import-dialog.tsx +6 -6
- package/src/components/component/component-preview.tsx +311 -0
- package/src/components/component/component-table.tsx +3 -3
- package/src/components/dependency/changelog-dialog.tsx +2 -2
- package/src/components/dependency/columns.tsx +3 -3
- package/src/components/dependency/dependency-table.tsx +7 -7
- package/src/components/dependency/edit-dialog.tsx +8 -8
- package/src/components/invitations/invitations-page.tsx +26 -49
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +6 -6
- package/src/components/knowledge-public/compliance-form-dialog.tsx +6 -6
- package/src/components/knowledge-public/legal-form-dialog.tsx +7 -7
- package/src/components/layout/sidebar-02/app-sidebar.tsx +1 -2
- package/src/components/layout/sidebar-02/index.tsx +1 -1
- package/src/components/layout/sidebar-02/nav-main.tsx +2 -3
- package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -3
- package/src/components/market/columns.tsx +4 -4
- package/src/components/market/market-app-form.tsx +6 -7
- package/src/components/market/market-app-list.tsx +14 -29
- package/src/components/monitoring/error-list.tsx +2 -2
- package/src/components/monitoring/metrics-stats.tsx +1 -1
- package/src/components/monitoring/request-log-table.tsx +2 -2
- package/src/components/oauth-providers/provider-page.tsx +20 -38
- package/src/components/permissions/assign-dialog.tsx +135 -0
- package/src/components/permissions/create-role-dialog.tsx +135 -0
- package/src/components/permissions/types.ts +49 -0
- package/src/components/user/columns.tsx +6 -6
- package/src/components/user/settings-activity-log.tsx +7 -7
- package/src/components/user/settings-profile/avatar-uploader.tsx +118 -0
- package/src/components/user/settings-profile/social-links.tsx +63 -0
- package/src/components/user/settings-profile.tsx +26 -125
- package/src/components/user/user-create-dialog.tsx +6 -6
- package/src/components/user/user-detail/error-boundary.tsx +19 -0
- package/src/components/user/user-detail/user-detail-dialogs.tsx +203 -0
- package/src/components/user/user-filters.tsx +4 -4
- package/src/lib/auth/page-gate.ts +28 -0
- package/src/lib/auth.config.ts +25 -9
- package/src/lib/i18n/detect.ts +1 -1
- package/src/lib/loader-serial.ts +14 -0
- package/src/providers/index.tsx +1 -1
- package/src/routeTree.gen.ts +67 -78
- package/src/routes/__root.tsx +4 -4
- package/src/routes/_nav/audit/route.lazy.tsx +1 -2
- package/src/routes/_nav/compliance/route.lazy.tsx +5 -3
- package/src/routes/_nav/components/route.lazy.tsx +14 -34
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +18 -38
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +18 -38
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +6 -6
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +3 -3
- package/src/routes/_nav/licenses/new/route.lazy.tsx +5 -5
- package/src/routes/_nav/licenses/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +2 -2
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +10 -10
- package/src/routes/_nav/monitoring/errors/route.tsx +7 -0
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +13 -15
- package/src/routes/_nav/monitoring/health/route.tsx +14 -0
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +24 -9
- package/src/routes/_nav/monitoring/logs/route.tsx +13 -0
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +12 -14
- package/src/routes/_nav/monitoring/metrics/route.tsx +7 -0
- package/src/routes/_nav/monitoring/overview/route.lazy.tsx +14 -13
- package/src/routes/_nav/monitoring/overview/route.tsx +17 -0
- package/src/routes/_nav/monitoring/route.lazy.tsx +6 -6
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +2 -2
- package/src/routes/_nav/permissions/route.lazy.tsx +21 -311
- package/src/routes/_nav/permissions/route.tsx +1 -1
- package/src/routes/_nav/route.tsx +5 -13
- package/src/routes/_nav/sso/route.lazy.tsx +2 -3
- package/src/routes/_nav/users/$userId/route.lazy.tsx +91 -156
- package/src/routes/_nav/users/-components/users-table.tsx +1 -1
- package/src/routes/_nav/users/route.lazy.tsx +1 -1
- package/src/routes/login/route.lazy.tsx +1 -1
- package/src/routes/setup/route.lazy.tsx +2 -2
- package/src/server/__tests__/admin-billing.test.ts +191 -16
- package/src/server/__tests__/compliance.test.ts +44 -7
- package/src/server/__tests__/permissions.test.ts +10 -5
- package/src/server/__tests__/prompt-defaults.test.ts +3 -3
- package/src/server/admin-ai-alerts.ts +2 -1
- package/src/server/admin-ai-usage.ts +2 -1
- package/src/server/ai-memory/store.ts +6 -2
- package/src/server/{serverFns/ai-prompts → ai-prompts}/prompt-defaults.ts +5 -3
- package/src/server/{serverFns/dependency → dependency}/helpers.ts +4 -3
- package/src/server/knowledge-public/github-sync.ts +2 -1
- package/src/server/license-issue.ts +2 -1
- package/src/server/middlewares/auth-rate-limit.ts +117 -0
- package/src/server/observability/health-check.ts +4 -1
- package/src/server/observability/metrics.ts +17 -15
- package/src/server/observability/request-middleware.ts +2 -1
- package/src/server/observability/timing.ts +14 -16
- package/src/server/serverFns/admin-billing.ts +107 -12
- package/src/server/serverFns/ai-memory/list-users.ts +2 -1
- package/src/server/serverFns/ai-prompts/activate.ts +2 -1
- package/src/server/serverFns/ai-prompts/get.ts +3 -2
- package/src/server/serverFns/ai-prompts/list.ts +3 -2
- package/src/server/serverFns/ai-prompts/rollback.ts +3 -1
- package/src/server/serverFns/ai-prompts/save.ts +5 -3
- package/src/server/serverFns/ai-provider/create.ts +4 -1
- package/src/server/serverFns/ai-provider/delete.ts +4 -1
- package/src/server/serverFns/ai-provider/get.ts +4 -1
- package/src/server/serverFns/ai-provider/list.ts +28 -1
- package/src/server/serverFns/ai-provider/seed-from-seed.ts +2 -1
- package/src/server/serverFns/ai-provider/update.ts +4 -1
- package/src/server/serverFns/anyhub/market.ts +8 -1
- package/src/server/serverFns/audit.ts +14 -1
- package/src/server/serverFns/auth/setup.ts +3 -1
- package/src/server/serverFns/compliance.ts +32 -3
- package/src/server/serverFns/components/import.ts +69 -14
- package/src/server/serverFns/components/list.ts +4 -1
- package/src/server/serverFns/components/mutations.ts +9 -1
- package/src/server/serverFns/dependency/list.ts +28 -5
- package/src/server/serverFns/dependency/sync.ts +7 -2
- package/src/server/serverFns/dependency/update.ts +4 -1
- package/src/server/serverFns/developer-log-auto-generate.ts +63 -32
- package/src/server/serverFns/developer-logs.ts +5 -1
- package/src/server/serverFns/invitations.ts +7 -1
- package/src/server/serverFns/knowledge-public/bookmarks/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/list.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/update.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/delete.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/detail.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/list.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/update.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/delete.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/detail.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/list.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/update.ts +2 -1
- package/src/server/serverFns/licenses.ts +8 -2
- package/src/server/serverFns/monitoring/get-errors.ts +2 -1
- package/src/server/serverFns/monitoring/get-health.ts +2 -1
- package/src/server/serverFns/monitoring/get-metrics.ts +2 -1
- package/src/server/serverFns/monitoring/get-request-logs.ts +2 -1
- package/src/server/serverFns/monitoring/report-error.ts +2 -1
- package/src/server/serverFns/monitoring/reset.ts +2 -1
- package/src/server/serverFns/oauth-providers.ts +4 -1
- package/src/server/serverFns/permissions.ts +18 -11
- package/src/server/serverFns/sentry/config.ts +11 -8
- package/src/server/serverFns/sso-config.ts +17 -2
- package/src/server/serverFns/user/create.ts +6 -1
- package/src/server/serverFns/user/delete.ts +6 -1
- package/src/server/serverFns/user/detail.ts +5 -2
- package/src/server/serverFns/user/list.ts +19 -4
- package/src/server/serverFns/user/reset-password.ts +10 -2
- package/src/server/serverFns/user/update-role.ts +10 -2
- package/src/server/serverFns/user/update-status.ts +10 -2
- package/src/styles/globals.css +4 -0
- package/src/components/data-table/data-table-pagination.tsx +0 -112
- package/src/components/data-table/data-table.tsx +0 -218
- package/src/components/data-table/table-detail-layout.tsx +0 -98
- package/src/components/ui/alert-dialog.tsx +0 -187
- package/src/components/ui/alert.tsx +0 -75
- package/src/components/ui/avatar.tsx +0 -106
- package/src/components/ui/badge.tsx +0 -51
- package/src/components/ui/button.tsx +0 -57
- package/src/components/ui/card.tsx +0 -102
- package/src/components/ui/checkbox.tsx +0 -28
- package/src/components/ui/collapsible.tsx +0 -19
- package/src/components/ui/dialog.tsx +0 -155
- package/src/components/ui/dropdown-menu.tsx +0 -267
- package/src/components/ui/field.tsx +0 -236
- package/src/components/ui/input-group.tsx +0 -158
- package/src/components/ui/input.tsx +0 -19
- package/src/components/ui/label.tsx +0 -17
- package/src/components/ui/pagination.tsx +0 -130
- package/src/components/ui/select.tsx +0 -234
- package/src/components/ui/separator.tsx +0 -22
- package/src/components/ui/sheet.tsx +0 -135
- package/src/components/ui/sidebar.tsx +0 -721
- package/src/components/ui/skeleton.tsx +0 -13
- package/src/components/ui/switch.tsx +0 -29
- package/src/components/ui/table.tsx +0 -115
- package/src/components/ui/textarea.tsx +0 -17
- package/src/components/ui/toast.tsx +0 -282
- package/src/components/ui/tooltip.tsx +0 -63
- package/src/hooks/use-mobile.ts +0 -19
- package/src/lib/avatar.ts +0 -41
- /package/src/routes/{api.auth.$.ts → api/auth/$.ts} +0 -0
- /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 "
|
|
20
|
-
import { Button } from "
|
|
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/
|
|
22
|
+
import { UserAvatar } from "@allbluecn/ui/components/user-avatar";
|
|
23
23
|
import type { AvatarConfig } from "@allbluecn/kernel";
|
|
24
|
-
import type { AdminColumnDef } from "
|
|
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
|
-
|
|
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.
|
|
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 "
|
|
30
|
-
import { Button } from "
|
|
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 "
|
|
38
|
-
import { FieldLabel } from "
|
|
37
|
+
} from "@allbluecn/ui/card";
|
|
38
|
+
import { FieldLabel } from "@allbluecn/ui/field";
|
|
39
39
|
import {
|
|
40
40
|
InputGroup,
|
|
41
41
|
InputGroupInput,
|
|
42
|
-
} from "
|
|
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 "
|
|
50
|
-
import { Separator } from "
|
|
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 {
|
|
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 "
|
|
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 "
|
|
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 "
|
|
35
|
+
} from "@allbluecn/ui/field";
|
|
36
36
|
import {
|
|
37
37
|
InputGroup,
|
|
38
38
|
InputGroupInput,
|
|
39
|
-
} from "
|
|
40
|
-
import { Separator } from "
|
|
41
|
-
import { Textarea } from "
|
|
42
|
-
import {
|
|
43
|
-
|
|
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
|
-
<
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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 "
|
|
27
|
-
import { Button } from "
|
|
28
|
-
import { Input } from "
|
|
29
|
-
import { Label } from "
|
|
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 "
|
|
36
|
+
} from "@allbluecn/ui/select";
|
|
37
37
|
import { createUserFn } from "@/server/serverFns/user/create";
|
|
38
|
-
import { toast } from '
|
|
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
|
+
}
|