@blocklet/ui-react 3.4.6 → 3.4.8
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/lib/UserCenter/components/user-info/link-preview-input.js +5 -5
- package/lib/UserCenter/components/user-info/metadata.js +32 -30
- package/lib/UserCenter/components/user-info/user-basic-info.js +16 -16
- package/lib/common/header-addons.js +40 -48
- package/lib/common/org-switch/avatar-uploader.d.ts +30 -0
- package/lib/common/org-switch/avatar-uploader.js +170 -0
- package/lib/common/org-switch/create.d.ts +24 -4
- package/lib/common/org-switch/create.js +152 -91
- package/lib/common/org-switch/index.js +6 -6
- package/lib/common/org-switch/locales.d.ts +18 -0
- package/lib/common/org-switch/locales.js +16 -4
- package/lib/common/org-switch/use-org.d.ts +1 -0
- package/lib/common/org-switch/use-org.js +30 -23
- package/package.json +10 -8
- package/src/UserCenter/components/user-info/metadata.tsx +5 -3
- package/src/UserCenter/components/user-info/user-basic-info.tsx +4 -2
- package/src/common/header-addons.jsx +0 -17
- package/src/common/org-switch/avatar-uploader.jsx +271 -0
- package/src/common/org-switch/create.jsx +179 -55
- package/src/common/org-switch/index.jsx +1 -1
- package/src/common/org-switch/locales.js +12 -0
- package/src/common/org-switch/use-org.jsx +14 -2
|
@@ -11,29 +11,17 @@ import LocaleSelector from '@arcblock/ux/lib/Locale/selector';
|
|
|
11
11
|
import SessionBlocklet from '@arcblock/ux/lib/SessionBlocklet';
|
|
12
12
|
import SessionUser from '@arcblock/ux/lib/SessionUser';
|
|
13
13
|
import { Fragment } from 'react/jsx-runtime';
|
|
14
|
-
import { WELLKNOWN_BLOCKLET_ADMIN_PATH } from '@abtnode/constant';
|
|
15
14
|
|
|
16
15
|
import { filterNavByRole, getLocalizedNavigation } from '../blocklets';
|
|
17
16
|
import { SessionManagerProps } from '../types';
|
|
18
17
|
import DomainWarning from './domain-warning';
|
|
19
18
|
import NotificationAddon from './notification-addon';
|
|
20
|
-
import OrgsSwitch from './org-switch';
|
|
21
19
|
|
|
22
20
|
const hasNotification = () => {
|
|
23
21
|
const navigations = window?.blocklet?.navigation ?? [];
|
|
24
22
|
return !!navigations.find((n) => n.id === '/userCenter/notification');
|
|
25
23
|
};
|
|
26
24
|
|
|
27
|
-
const isOrgsEnabled = (blocklet) => {
|
|
28
|
-
const { settings = {} } = blocklet ?? window?.blocklet ?? {};
|
|
29
|
-
return settings?.org?.enabled || false;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const inDashboard = () => {
|
|
33
|
-
const { pathname = '' } = window.location || {};
|
|
34
|
-
return pathname.startsWith(WELLKNOWN_BLOCKLET_ADMIN_PATH);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
25
|
// eslint-disable-next-line no-shadow
|
|
38
26
|
export default function HeaderAddons({
|
|
39
27
|
formattedBlocklet,
|
|
@@ -103,11 +91,6 @@ export default function HeaderAddons({
|
|
|
103
91
|
);
|
|
104
92
|
}
|
|
105
93
|
|
|
106
|
-
// 在最前面添加 orgs switch, 在 Dashboard 中不显示
|
|
107
|
-
if (!inDashboard() && isOrgsEnabled(formattedBlocklet) && sessionCtx?.session?.user) {
|
|
108
|
-
addonsArray.unshift(<OrgsSwitch key="orgs-switch" session={sessionCtx.session} locale={locale} />);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
94
|
if (typeof addons === 'function') {
|
|
112
95
|
addonsArray = addons(addonsArray) || [];
|
|
113
96
|
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AvatarUploader - 组织头像上传组件
|
|
3
|
+
*
|
|
4
|
+
* 用于创建和编辑组织时上传头像
|
|
5
|
+
* 支持图片裁剪、旋转等编辑功能
|
|
6
|
+
* 上传成功后返回文件路径供表单使用
|
|
7
|
+
*/
|
|
8
|
+
import { lazy, Suspense, useRef, useMemo, useState } from 'react';
|
|
9
|
+
import noop from 'lodash/noop';
|
|
10
|
+
import PropTypes from 'prop-types';
|
|
11
|
+
import { Box, CircularProgress } from '@mui/material';
|
|
12
|
+
import { styled } from '@arcblock/ux/lib/Theme';
|
|
13
|
+
import { joinURL } from 'ufo';
|
|
14
|
+
import { useMemoizedFn } from 'ahooks';
|
|
15
|
+
import CameraAltIcon from '@mui/icons-material/CameraAlt';
|
|
16
|
+
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
17
|
+
import Img from '@arcblock/ux/lib/Img';
|
|
18
|
+
|
|
19
|
+
import translations from './locales';
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line import/no-unresolved
|
|
22
|
+
const Uploader = lazy(() => import('@blocklet/uploader').then((res) => ({ default: res.Uploader })));
|
|
23
|
+
|
|
24
|
+
// 配置常量
|
|
25
|
+
const UPLOAD_PREFIX = '/blocklet';
|
|
26
|
+
const ALLOWED_FILE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp', '.bmp', '.ico'];
|
|
27
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
28
|
+
const MIN_CROP_SIZE = 256;
|
|
29
|
+
const ICON_SIZE_RATIO = 3;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 构建组织头像显示 URL
|
|
33
|
+
* @param {string} prefix - URL 前缀
|
|
34
|
+
* @param {string} teamDid - 团队 DID
|
|
35
|
+
* @param {string} orgId - 组织 ID
|
|
36
|
+
* @param {string} avatar - 头像文件名
|
|
37
|
+
* @returns {string} 完整的 URL 路径
|
|
38
|
+
*/
|
|
39
|
+
function buildAvatarDisplayUrl(prefix, teamDid, avatar) {
|
|
40
|
+
return joinURL(prefix, UPLOAD_PREFIX, teamDid, 'orgs', 'avatar', avatar);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 构建头像上传 API URL(不需要 orgId)
|
|
45
|
+
* @param {string} prefix - URL 前缀
|
|
46
|
+
* @param {string} teamDid - 团队 DID
|
|
47
|
+
* @returns {string} 上传 API URL
|
|
48
|
+
*/
|
|
49
|
+
function buildAvatarUploadUrl(prefix, teamDid) {
|
|
50
|
+
return joinURL(prefix, UPLOAD_PREFIX, teamDid, 'orgs', 'avatar', 'upload');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default function AvatarUploader({
|
|
54
|
+
org = null,
|
|
55
|
+
size = 80,
|
|
56
|
+
teamDid: teamDidProp = '',
|
|
57
|
+
prefix = '/.well-known/service',
|
|
58
|
+
locale = 'en',
|
|
59
|
+
headers = noop,
|
|
60
|
+
onChange = noop,
|
|
61
|
+
onError = noop,
|
|
62
|
+
editable = true,
|
|
63
|
+
}) {
|
|
64
|
+
const t = useMemoizedFn((key, data = {}) => {
|
|
65
|
+
return translate(translations, key, locale, 'en', data);
|
|
66
|
+
});
|
|
67
|
+
const uploaderRef = useRef(null);
|
|
68
|
+
const [uploading, setUploading] = useState(false);
|
|
69
|
+
// 本地预览的头像路径(用于新上传的图片)
|
|
70
|
+
const [localAvatar, setLocalAvatar] = useState('');
|
|
71
|
+
|
|
72
|
+
// 获取 teamDid:优先使用 prop,否则从 window.blocklet.did 获取
|
|
73
|
+
const teamDid = useMemo(() => {
|
|
74
|
+
if (teamDidProp) return teamDidProp;
|
|
75
|
+
// eslint-disable-next-line no-undef
|
|
76
|
+
return typeof window !== 'undefined' ? window.blocklet?.did : '';
|
|
77
|
+
}, [teamDidProp]);
|
|
78
|
+
|
|
79
|
+
// 构建上传 API URL(不需要 orgId)
|
|
80
|
+
const uploadApiUrl = useMemo(() => {
|
|
81
|
+
if (!teamDid) return null;
|
|
82
|
+
return buildAvatarUploadUrl(prefix, teamDid);
|
|
83
|
+
}, [prefix, teamDid]);
|
|
84
|
+
|
|
85
|
+
// 是否可以上传(需要有 teamDid 且 editable 为 true)
|
|
86
|
+
const canUpload = editable && !!uploadApiUrl;
|
|
87
|
+
|
|
88
|
+
// 打开上传弹窗
|
|
89
|
+
const handleOpen = useMemoizedFn(() => {
|
|
90
|
+
if (canUpload && !uploading) {
|
|
91
|
+
uploaderRef.current?.open();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 上传完成处理 - 返回文件路径
|
|
96
|
+
const handleFinish = useMemoizedFn((result) => {
|
|
97
|
+
uploaderRef.current?.close();
|
|
98
|
+
setUploading(false);
|
|
99
|
+
const { avatarPath } = result.data;
|
|
100
|
+
setLocalAvatar(result.uploadURL);
|
|
101
|
+
onChange(avatarPath);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// 上传开始处理
|
|
105
|
+
const handleUploadStart = useMemoizedFn(() => {
|
|
106
|
+
setUploading(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 错误处理
|
|
110
|
+
const handleError = useMemoizedFn((error) => {
|
|
111
|
+
setUploading(false);
|
|
112
|
+
console.error('Avatar upload failed:', error);
|
|
113
|
+
onError(error);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 构建头像显示 URL
|
|
117
|
+
const avatarUrl = useMemo(() => {
|
|
118
|
+
// 优先使用本地上传的头像
|
|
119
|
+
if (localAvatar) {
|
|
120
|
+
return localAvatar;
|
|
121
|
+
}
|
|
122
|
+
const avatar = org?.avatar;
|
|
123
|
+
if (!avatar) return null;
|
|
124
|
+
if (org?.id) {
|
|
125
|
+
return buildAvatarDisplayUrl(prefix, teamDid, avatar);
|
|
126
|
+
}
|
|
127
|
+
if (avatar.startsWith('http://') || avatar.startsWith('https://') || avatar.startsWith('/')) {
|
|
128
|
+
return avatar;
|
|
129
|
+
}
|
|
130
|
+
return joinURL(prefix, avatar);
|
|
131
|
+
}, [localAvatar, org?.avatar, org?.id, prefix, teamDid]);
|
|
132
|
+
|
|
133
|
+
// 获取显示名称
|
|
134
|
+
const displayName = org?.name || '';
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Root $size={size} $editable={editable} $uploading={uploading} onClick={handleOpen}>
|
|
138
|
+
{uploadApiUrl && (
|
|
139
|
+
<Suspense fallback={null}>
|
|
140
|
+
<Uploader
|
|
141
|
+
ref={uploaderRef}
|
|
142
|
+
locale={locale}
|
|
143
|
+
popup
|
|
144
|
+
onUploadFinish={handleFinish}
|
|
145
|
+
onUploadStart={handleUploadStart}
|
|
146
|
+
onError={handleError}
|
|
147
|
+
plugins={['ImageEditor']}
|
|
148
|
+
installerProps={{ disabled: true }}
|
|
149
|
+
apiPathProps={{
|
|
150
|
+
uploader: uploadApiUrl,
|
|
151
|
+
disableMediaKitPrefix: true,
|
|
152
|
+
disableMediaKitStatus: true,
|
|
153
|
+
}}
|
|
154
|
+
coreProps={{
|
|
155
|
+
restrictions: {
|
|
156
|
+
allowedFileExts: ALLOWED_FILE_EXTENSIONS,
|
|
157
|
+
maxFileSize: MAX_FILE_SIZE,
|
|
158
|
+
maxNumberOfFiles: 1,
|
|
159
|
+
},
|
|
160
|
+
}}
|
|
161
|
+
dashboardProps={{
|
|
162
|
+
autoOpen: 'imageEditor',
|
|
163
|
+
}}
|
|
164
|
+
imageEditorProps={{
|
|
165
|
+
actions: {
|
|
166
|
+
revert: true,
|
|
167
|
+
rotate: true,
|
|
168
|
+
granularRotate: true,
|
|
169
|
+
flip: true,
|
|
170
|
+
zoomIn: true,
|
|
171
|
+
zoomOut: true,
|
|
172
|
+
cropSquare: false,
|
|
173
|
+
cropWidescreen: false,
|
|
174
|
+
cropWidescreenVertical: false,
|
|
175
|
+
},
|
|
176
|
+
cropperOptions: {
|
|
177
|
+
autoCrop: true,
|
|
178
|
+
autoCropArea: 1,
|
|
179
|
+
aspectRatio: 1,
|
|
180
|
+
initialAspectRatio: 1,
|
|
181
|
+
croppedCanvasOptions: {
|
|
182
|
+
minWidth: MIN_CROP_SIZE,
|
|
183
|
+
minHeight: MIN_CROP_SIZE,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}}
|
|
187
|
+
tusProps={{
|
|
188
|
+
headers,
|
|
189
|
+
}}
|
|
190
|
+
/>
|
|
191
|
+
</Suspense>
|
|
192
|
+
)}
|
|
193
|
+
{/* 使用 Img 组件显示头像,自动处理加载错误和占位图片 */}
|
|
194
|
+
<Img
|
|
195
|
+
key={avatarUrl || 'no-avatar'}
|
|
196
|
+
src={avatarUrl || ''}
|
|
197
|
+
alt={displayName}
|
|
198
|
+
width={size}
|
|
199
|
+
height={size}
|
|
200
|
+
ratio={1}
|
|
201
|
+
size="cover"
|
|
202
|
+
position="center"
|
|
203
|
+
lazy={false}
|
|
204
|
+
style={{
|
|
205
|
+
borderRadius: '50%',
|
|
206
|
+
overflow: 'hidden',
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
{editable && (
|
|
210
|
+
<Box className="upload-overlay">
|
|
211
|
+
{uploading ? (
|
|
212
|
+
<CircularProgress size={size / ICON_SIZE_RATIO} sx={{ color: 'white' }} />
|
|
213
|
+
) : (
|
|
214
|
+
<>
|
|
215
|
+
<CameraAltIcon sx={{ fontSize: size / ICON_SIZE_RATIO, color: 'white' }} />
|
|
216
|
+
<Box component="span" sx={{ fontSize: 12, color: 'white' }}>
|
|
217
|
+
{t('upload')}
|
|
218
|
+
</Box>
|
|
219
|
+
</>
|
|
220
|
+
)}
|
|
221
|
+
</Box>
|
|
222
|
+
)}
|
|
223
|
+
</Root>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
AvatarUploader.propTypes = {
|
|
228
|
+
org: PropTypes.shape({
|
|
229
|
+
id: PropTypes.string,
|
|
230
|
+
name: PropTypes.string,
|
|
231
|
+
avatar: PropTypes.string,
|
|
232
|
+
}),
|
|
233
|
+
size: PropTypes.number,
|
|
234
|
+
teamDid: PropTypes.string,
|
|
235
|
+
prefix: PropTypes.string,
|
|
236
|
+
headers: PropTypes.func,
|
|
237
|
+
onChange: PropTypes.func,
|
|
238
|
+
onError: PropTypes.func,
|
|
239
|
+
editable: PropTypes.bool,
|
|
240
|
+
locale: PropTypes.string,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const Root = styled(Box, {
|
|
244
|
+
shouldForwardProp: (prop) => !['$size', '$editable', '$uploading'].includes(prop),
|
|
245
|
+
})(({ $size, $editable, $uploading }) => ({
|
|
246
|
+
position: 'relative',
|
|
247
|
+
width: $size,
|
|
248
|
+
height: $size,
|
|
249
|
+
borderRadius: '50%',
|
|
250
|
+
overflow: 'hidden',
|
|
251
|
+
cursor: $editable && !$uploading ? 'pointer' : 'default',
|
|
252
|
+
|
|
253
|
+
'.upload-overlay': {
|
|
254
|
+
position: 'absolute',
|
|
255
|
+
top: 0,
|
|
256
|
+
left: 0,
|
|
257
|
+
right: 0,
|
|
258
|
+
bottom: 0,
|
|
259
|
+
display: 'flex',
|
|
260
|
+
flexDirection: 'column',
|
|
261
|
+
alignItems: 'center',
|
|
262
|
+
justifyContent: 'center',
|
|
263
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
264
|
+
opacity: $uploading ? 1 : 0,
|
|
265
|
+
transition: 'opacity 0.2s ease-in-out',
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
'&:hover .upload-overlay': {
|
|
269
|
+
opacity: $editable ? 1 : 0,
|
|
270
|
+
},
|
|
271
|
+
}));
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* OrgMutateDialog - 组织创建/编辑弹窗
|
|
4
|
+
*
|
|
5
|
+
* 支持两种模式:
|
|
6
|
+
* - 创建模式:新建组织,头像必填
|
|
7
|
+
* - 编辑模式:编辑已有组织信息
|
|
8
|
+
*/
|
|
9
|
+
import { useState, useEffect } from 'react';
|
|
3
10
|
import PropTypes from 'prop-types';
|
|
4
11
|
import { useReactive, useMemoizedFn } from 'ahooks';
|
|
5
12
|
import noop from 'lodash/noop';
|
|
6
13
|
import Dialog from '@arcblock/ux/lib/Dialog';
|
|
7
|
-
import { CircularProgress, DialogContentText, Typography, TextField, Alert } from '@mui/material';
|
|
14
|
+
import { CircularProgress, DialogContentText, Typography, TextField, Alert, Box } from '@mui/material';
|
|
8
15
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
9
16
|
import Button from '@arcblock/ux/lib/Button';
|
|
10
17
|
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
@@ -12,11 +19,31 @@ import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
|
12
19
|
import { formatAxiosError } from '../../UserCenter/libs/utils';
|
|
13
20
|
import useOrg from './use-org';
|
|
14
21
|
import translations from './locales';
|
|
22
|
+
import AvatarUploader from './avatar-uploader';
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
// 操作模式
|
|
25
|
+
const MODE_CREATE = 'create';
|
|
26
|
+
const MODE_EDIT = 'edit';
|
|
27
|
+
|
|
28
|
+
export default function OrgMutateDialog({
|
|
29
|
+
mode = MODE_CREATE,
|
|
30
|
+
org = null,
|
|
31
|
+
onSuccess = noop,
|
|
32
|
+
onCancel = noop,
|
|
33
|
+
locale = 'en',
|
|
34
|
+
teamDid: teamDidProp = '',
|
|
35
|
+
prefix = '/.well-known/service',
|
|
36
|
+
headers = noop,
|
|
37
|
+
}) {
|
|
38
|
+
const isEditMode = mode === MODE_EDIT;
|
|
17
39
|
const [loading, setLoading] = useState(false);
|
|
18
40
|
const [error, setError] = useState('');
|
|
19
|
-
const { createOrg } = useOrg();
|
|
41
|
+
const { createOrg, updateOrg } = useOrg();
|
|
42
|
+
|
|
43
|
+
// 获取 teamDid:优先使用 prop,否则从 window.blocklet.did 获取
|
|
44
|
+
// eslint-disable-next-line no-undef
|
|
45
|
+
const teamDid = teamDidProp || (typeof window !== 'undefined' ? window.blocklet?.did : '');
|
|
46
|
+
|
|
20
47
|
const t = useMemoizedFn((key, data = {}) => {
|
|
21
48
|
return translate(translations, key, locale, 'en', data);
|
|
22
49
|
});
|
|
@@ -24,32 +51,82 @@ export default function CreateOrgDialog({ onSuccess = noop, onCancel = noop, loc
|
|
|
24
51
|
const form = useReactive({
|
|
25
52
|
name: '',
|
|
26
53
|
description: '',
|
|
54
|
+
avatar: '',
|
|
27
55
|
});
|
|
28
56
|
|
|
29
|
-
|
|
57
|
+
// 编辑模式下初始化表单数据
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (isEditMode && org) {
|
|
60
|
+
form.name = org.name || '';
|
|
61
|
+
form.description = org.description || '';
|
|
62
|
+
form.avatar = org.avatar || '';
|
|
63
|
+
}
|
|
64
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
|
+
}, [isEditMode, org]);
|
|
66
|
+
|
|
67
|
+
// 头像变化处理
|
|
68
|
+
const handleAvatarChange = useMemoizedFn((avatarPath) => {
|
|
69
|
+
setError('');
|
|
70
|
+
form.avatar = avatarPath;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// 表单验证
|
|
74
|
+
const validateForm = useMemoizedFn(() => {
|
|
30
75
|
const _name = form.name.trim();
|
|
31
76
|
if (!_name) {
|
|
32
77
|
setError(t('nameEmpty'));
|
|
33
|
-
return;
|
|
78
|
+
return false;
|
|
34
79
|
}
|
|
35
80
|
|
|
36
81
|
if (_name.length > 25) {
|
|
37
82
|
setError(t('nameTooLong', { length: 25 }));
|
|
38
|
-
return;
|
|
83
|
+
return false;
|
|
39
84
|
}
|
|
40
85
|
|
|
41
86
|
const _description = form.description.trim();
|
|
42
|
-
|
|
43
87
|
if (_description.length > 255) {
|
|
44
|
-
setError(t('descriptionTooLong', { length:
|
|
88
|
+
setError(t('descriptionTooLong', { length: 255 }));
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 头像必填验证
|
|
93
|
+
if (!form.avatar) {
|
|
94
|
+
setError(t('avatarEmpty'));
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 提交处理
|
|
102
|
+
const onSubmit = async () => {
|
|
103
|
+
if (!validateForm()) {
|
|
45
104
|
return;
|
|
46
105
|
}
|
|
47
106
|
|
|
48
107
|
setError('');
|
|
49
108
|
setLoading(true);
|
|
50
109
|
|
|
110
|
+
const _name = form.name.trim();
|
|
111
|
+
const _description = form.description.trim();
|
|
112
|
+
const _avatar = form.avatar;
|
|
113
|
+
|
|
51
114
|
try {
|
|
52
|
-
|
|
115
|
+
if (isEditMode && org?.id) {
|
|
116
|
+
// 编辑模式:更新组织
|
|
117
|
+
await updateOrg(org.id, {
|
|
118
|
+
name: _name,
|
|
119
|
+
description: _description,
|
|
120
|
+
avatar: _avatar,
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
// 创建模式:新建组织
|
|
124
|
+
await createOrg({
|
|
125
|
+
name: _name,
|
|
126
|
+
description: _description,
|
|
127
|
+
avatar: _avatar,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
53
130
|
onSuccess();
|
|
54
131
|
} catch (err) {
|
|
55
132
|
console.error(err);
|
|
@@ -61,50 +138,15 @@ export default function CreateOrgDialog({ onSuccess = noop, onCancel = noop, loc
|
|
|
61
138
|
}
|
|
62
139
|
};
|
|
63
140
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
autoComplete="off"
|
|
70
|
-
variant="outlined"
|
|
71
|
-
name="name"
|
|
72
|
-
data-cy="mutate-org-input-name"
|
|
73
|
-
fullWidth
|
|
74
|
-
autoFocus
|
|
75
|
-
value={form.name}
|
|
76
|
-
onChange={(e) => {
|
|
77
|
-
setError('');
|
|
78
|
-
form.name = e.target.value;
|
|
79
|
-
}}
|
|
80
|
-
disabled={loading}
|
|
81
|
-
/>
|
|
82
|
-
</Typography>
|
|
83
|
-
|
|
84
|
-
<Typography component="div" style={{ marginTop: 16, marginBottom: 16 }}>
|
|
85
|
-
<TextField
|
|
86
|
-
label={t('mutate.description')}
|
|
87
|
-
autoComplete="off"
|
|
88
|
-
variant="outlined"
|
|
89
|
-
name="description"
|
|
90
|
-
data-cy="mutate-org-input-description"
|
|
91
|
-
fullWidth
|
|
92
|
-
value={form.description}
|
|
93
|
-
onChange={(e) => {
|
|
94
|
-
setError('');
|
|
95
|
-
form.description = e.target.value;
|
|
96
|
-
}}
|
|
97
|
-
disabled={loading}
|
|
98
|
-
multiline
|
|
99
|
-
rows={3}
|
|
100
|
-
/>
|
|
101
|
-
</Typography>
|
|
102
|
-
</div>
|
|
103
|
-
);
|
|
141
|
+
// 对话框标题
|
|
142
|
+
const dialogTitle = isEditMode ? t('mutate.title', { mode: t('edit') }) : t('mutate.title', { mode: t('create') });
|
|
143
|
+
|
|
144
|
+
// 提交按钮文本
|
|
145
|
+
const submitButtonText = isEditMode ? t('save') : t('create');
|
|
104
146
|
|
|
105
147
|
return (
|
|
106
148
|
<Dialog
|
|
107
|
-
title={
|
|
149
|
+
title={dialogTitle}
|
|
108
150
|
fullWidth
|
|
109
151
|
open
|
|
110
152
|
onClose={onCancel}
|
|
@@ -121,12 +163,75 @@ export default function CreateOrgDialog({ onSuccess = noop, onCancel = noop, loc
|
|
|
121
163
|
disabled={loading}
|
|
122
164
|
variant="contained"
|
|
123
165
|
autoFocus>
|
|
124
|
-
{loading && <CircularProgress size={16} />}
|
|
125
|
-
{
|
|
166
|
+
{loading && <CircularProgress size={16} sx={{ mr: 1 }} />}
|
|
167
|
+
{submitButtonText}
|
|
126
168
|
</Button>
|
|
127
169
|
</>
|
|
128
170
|
}>
|
|
129
|
-
<DialogContentText component="div">
|
|
171
|
+
<DialogContentText component="div">
|
|
172
|
+
{/* 头像上传区域 */}
|
|
173
|
+
<Box
|
|
174
|
+
sx={{
|
|
175
|
+
display: 'flex',
|
|
176
|
+
flexDirection: 'column',
|
|
177
|
+
alignItems: 'center',
|
|
178
|
+
py: 2,
|
|
179
|
+
}}>
|
|
180
|
+
<AvatarUploader
|
|
181
|
+
org={isEditMode ? org : { name: form.name }}
|
|
182
|
+
size={90}
|
|
183
|
+
teamDid={teamDid}
|
|
184
|
+
prefix={prefix}
|
|
185
|
+
headers={headers}
|
|
186
|
+
onChange={handleAvatarChange}
|
|
187
|
+
value={form.avatar}
|
|
188
|
+
editable
|
|
189
|
+
/>
|
|
190
|
+
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
|
|
191
|
+
{t('avatar')}
|
|
192
|
+
</Typography>
|
|
193
|
+
</Box>
|
|
194
|
+
|
|
195
|
+
{/* 表单字段 */}
|
|
196
|
+
<Typography component="div" style={{ marginTop: 16 }}>
|
|
197
|
+
<TextField
|
|
198
|
+
label={t('mutate.name')}
|
|
199
|
+
autoComplete="off"
|
|
200
|
+
variant="outlined"
|
|
201
|
+
name="name"
|
|
202
|
+
data-cy="mutate-org-input-name"
|
|
203
|
+
fullWidth
|
|
204
|
+
autoFocus
|
|
205
|
+
value={form.name}
|
|
206
|
+
onChange={(e) => {
|
|
207
|
+
setError('');
|
|
208
|
+
form.name = e.target.value;
|
|
209
|
+
}}
|
|
210
|
+
disabled={loading}
|
|
211
|
+
required
|
|
212
|
+
/>
|
|
213
|
+
</Typography>
|
|
214
|
+
|
|
215
|
+
<Typography component="div" style={{ marginTop: 16, marginBottom: 16 }}>
|
|
216
|
+
<TextField
|
|
217
|
+
label={t('mutate.description')}
|
|
218
|
+
autoComplete="off"
|
|
219
|
+
variant="outlined"
|
|
220
|
+
name="description"
|
|
221
|
+
data-cy="mutate-org-input-description"
|
|
222
|
+
fullWidth
|
|
223
|
+
value={form.description}
|
|
224
|
+
onChange={(e) => {
|
|
225
|
+
setError('');
|
|
226
|
+
form.description = e.target.value;
|
|
227
|
+
}}
|
|
228
|
+
disabled={loading}
|
|
229
|
+
multiline
|
|
230
|
+
rows={3}
|
|
231
|
+
/>
|
|
232
|
+
</Typography>
|
|
233
|
+
</DialogContentText>
|
|
234
|
+
|
|
130
235
|
{!!error && (
|
|
131
236
|
<Alert severity="error" style={{ width: '100%', margin: 0 }}>
|
|
132
237
|
{error}
|
|
@@ -136,8 +241,27 @@ export default function CreateOrgDialog({ onSuccess = noop, onCancel = noop, loc
|
|
|
136
241
|
);
|
|
137
242
|
}
|
|
138
243
|
|
|
139
|
-
|
|
244
|
+
OrgMutateDialog.propTypes = {
|
|
245
|
+
/** 操作模式:'create' 创建 | 'edit' 编辑 */
|
|
246
|
+
mode: PropTypes.oneOf([MODE_CREATE, MODE_EDIT]),
|
|
247
|
+
/** 编辑模式下的组织数据 */
|
|
248
|
+
org: PropTypes.shape({
|
|
249
|
+
id: PropTypes.string,
|
|
250
|
+
name: PropTypes.string,
|
|
251
|
+
description: PropTypes.string,
|
|
252
|
+
avatar: PropTypes.string,
|
|
253
|
+
}),
|
|
140
254
|
onSuccess: PropTypes.func,
|
|
141
255
|
onCancel: PropTypes.func,
|
|
142
256
|
locale: PropTypes.string,
|
|
257
|
+
teamDid: PropTypes.string,
|
|
258
|
+
prefix: PropTypes.string,
|
|
259
|
+
headers: PropTypes.func,
|
|
143
260
|
};
|
|
261
|
+
|
|
262
|
+
// 导出模式常量供外部使用
|
|
263
|
+
OrgMutateDialog.MODE_CREATE = MODE_CREATE;
|
|
264
|
+
OrgMutateDialog.MODE_EDIT = MODE_EDIT;
|
|
265
|
+
|
|
266
|
+
// 保持向后兼容的别名
|
|
267
|
+
export { OrgMutateDialog as CreateOrgDialog };
|
|
@@ -23,7 +23,7 @@ import { KeyboardArrowDown, Search, Add, OpenInNew } from '@mui/icons-material';
|
|
|
23
23
|
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
24
24
|
|
|
25
25
|
import useOrg from './use-org';
|
|
26
|
-
import CreateOrgDialog from './create';
|
|
26
|
+
import { CreateOrgDialog } from './create';
|
|
27
27
|
import translations from './locales';
|
|
28
28
|
|
|
29
29
|
const PAGE_SIZE = 20;
|
|
@@ -9,12 +9,18 @@ const translations = {
|
|
|
9
9
|
nameEmpty: 'Name cannot be empty',
|
|
10
10
|
nameTooLong: 'Name must be less than {length} characters',
|
|
11
11
|
descriptionTooLong: 'Description must be less than {length} characters',
|
|
12
|
+
avatarEmpty: 'Avatar cannot be empty',
|
|
12
13
|
cancel: 'Cancel',
|
|
13
14
|
create: 'Create',
|
|
15
|
+
upload: 'Upload',
|
|
16
|
+
avatar: 'Avatar',
|
|
14
17
|
mutate: {
|
|
15
18
|
title: '{mode} Organization',
|
|
16
19
|
name: 'Name',
|
|
17
20
|
description: 'Description',
|
|
21
|
+
avatarRequired: 'Avatar is required',
|
|
22
|
+
uploadAvatarTitle: 'Upload Avatar',
|
|
23
|
+
uploadAvatarTip: 'Click to upload an avatar for your organization',
|
|
18
24
|
},
|
|
19
25
|
},
|
|
20
26
|
zh: {
|
|
@@ -27,12 +33,18 @@ const translations = {
|
|
|
27
33
|
nameEmpty: '名称不能为空',
|
|
28
34
|
nameTooLong: '名称不能超过{length}个字符',
|
|
29
35
|
descriptionTooLong: '描述不能超过{length}个字符',
|
|
36
|
+
avatarEmpty: '头像不能为空',
|
|
37
|
+
upload: '上传',
|
|
30
38
|
cancel: '取消',
|
|
31
39
|
create: '创建',
|
|
40
|
+
avatar: '头像',
|
|
32
41
|
mutate: {
|
|
33
42
|
title: '{mode}组织',
|
|
34
43
|
name: '组织名称',
|
|
35
44
|
description: '组织描述',
|
|
45
|
+
avatarRequired: '头像为必填项',
|
|
46
|
+
uploadAvatarTitle: '上传头像',
|
|
47
|
+
uploadAvatarTip: '点击上传组织头像',
|
|
36
48
|
},
|
|
37
49
|
},
|
|
38
50
|
};
|
|
@@ -23,9 +23,20 @@ export default function useOrg(session) {
|
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
const createOrg = useMemoizedFn(async ({ name, description }) => {
|
|
26
|
+
const createOrg = useMemoizedFn(async ({ name, description, avatar }) => {
|
|
27
27
|
try {
|
|
28
|
-
const response = await client.user.createOrg({ name, description });
|
|
28
|
+
const response = await client.user.createOrg({ name, description, avatar });
|
|
29
|
+
return response.data;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(error);
|
|
32
|
+
Toast.error(formatAxiosError(error));
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const updateOrg = useMemoizedFn(async (orgId, { name, description, avatar }) => {
|
|
38
|
+
try {
|
|
39
|
+
const response = await client.user.updateOrg(orgId, { name, description, avatar });
|
|
29
40
|
return response.data;
|
|
30
41
|
} catch (error) {
|
|
31
42
|
console.error(error);
|
|
@@ -62,6 +73,7 @@ export default function useOrg(session) {
|
|
|
62
73
|
return {
|
|
63
74
|
getOrgs,
|
|
64
75
|
createOrg,
|
|
76
|
+
updateOrg,
|
|
65
77
|
getCurrentOrg,
|
|
66
78
|
};
|
|
67
79
|
}
|