@blocklet/ui-react 3.4.7 → 3.4.9

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.
@@ -1,10 +1,17 @@
1
1
  /* eslint-disable @typescript-eslint/naming-convention */
2
- import { useState } from 'react';
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
- export default function CreateOrgDialog({ onSuccess = noop, onCancel = noop, locale = 'en' }) {
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
- const onSubmit = async () => {
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: 25 }));
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
- await createOrg({ name: _name, description: _description });
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
- const body = (
65
- <div>
66
- <Typography component="div" style={{ marginTop: 16 }}>
67
- <TextField
68
- label={t('mutate.name')}
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={t('mutate.title', { mode: t('create') })}
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
- {t('create')}
166
+ {loading && <CircularProgress size={16} sx={{ mr: 1 }} />}
167
+ {submitButtonText}
126
168
  </Button>
127
169
  </>
128
170
  }>
129
- <DialogContentText component="div">{body}</DialogContentText>
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
- CreateOrgDialog.propTypes = {
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
  }