@blocklet/ui-react 3.4.7 → 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.
@@ -36,9 +36,9 @@ function ye({ session: a, locale: h = "en" }) {
36
36
  console.error("Failed to fetch organizations list", e);
37
37
  }
38
38
  }
39
- ), { list: p = [], total: q = 0 } = K || {}, T = p.length < q, U = k((e) => {
39
+ ), { list: p = [], total: q = 0 } = K || {}, M = p.length < q, U = k((e) => {
40
40
  const o = e.target, { scrollTop: c, scrollHeight: y, clientHeight: b } = o;
41
- y - c - b < 50 && !z && T && V();
41
+ y - c - b < 50 && !z && M && V();
42
42
  }), x = !!S;
43
43
  ee(() => {
44
44
  x && O();
@@ -56,7 +56,7 @@ function ye({ session: a, locale: h = "en" }) {
56
56
  ), f();
57
57
  }, Q = () => {
58
58
  u(!0);
59
- }, M = () => {
59
+ }, T = () => {
60
60
  const e = le(ce(ne, "user/orgs"), { locale: h });
61
61
  window.location.href = e, f();
62
62
  }, X = (e) => /* @__PURE__ */ i(
@@ -214,7 +214,7 @@ function ye({ session: a, locale: h = "en" }) {
214
214
  const c = o === p.length - 1;
215
215
  return /* @__PURE__ */ i(r, { children: [
216
216
  X(e),
217
- c && T && /* @__PURE__ */ t(
217
+ c && M && /* @__PURE__ */ t(
218
218
  r,
219
219
  {
220
220
  sx: {
@@ -267,7 +267,7 @@ function ye({ session: a, locale: h = "en" }) {
267
267
  /* @__PURE__ */ t(r, { sx: { p: 1.5 }, children: /* @__PURE__ */ t(
268
268
  P,
269
269
  {
270
- onClick: M,
270
+ onClick: T,
271
271
  variant: "text",
272
272
  component: "a",
273
273
  size: "small",
@@ -289,7 +289,7 @@ function ye({ session: a, locale: h = "en" }) {
289
289
  ]
290
290
  }
291
291
  ),
292
- j && /* @__PURE__ */ t(xe, { onSuccess: M, onCancel: () => u(!1), locale: h })
292
+ j && /* @__PURE__ */ t(xe, { onSuccess: T, onCancel: () => u(!1), locale: h })
293
293
  ] });
294
294
  }
295
295
  ye.propTypes = {
@@ -10,12 +10,18 @@ declare namespace translations {
10
10
  let nameEmpty: string;
11
11
  let nameTooLong: string;
12
12
  let descriptionTooLong: string;
13
+ let avatarEmpty: string;
13
14
  let cancel: string;
14
15
  let create: string;
16
+ let upload: string;
17
+ let avatar: string;
15
18
  namespace mutate {
16
19
  let title: string;
17
20
  let name: string;
18
21
  let description: string;
22
+ let avatarRequired: string;
23
+ let uploadAvatarTitle: string;
24
+ let uploadAvatarTip: string;
19
25
  }
20
26
  }
21
27
  namespace zh {
@@ -37,10 +43,16 @@ declare namespace translations {
37
43
  export { nameTooLong_1 as nameTooLong };
38
44
  let descriptionTooLong_1: string;
39
45
  export { descriptionTooLong_1 as descriptionTooLong };
46
+ let avatarEmpty_1: string;
47
+ export { avatarEmpty_1 as avatarEmpty };
48
+ let upload_1: string;
49
+ export { upload_1 as upload };
40
50
  let cancel_1: string;
41
51
  export { cancel_1 as cancel };
42
52
  let create_1: string;
43
53
  export { create_1 as create };
54
+ let avatar_1: string;
55
+ export { avatar_1 as avatar };
44
56
  export namespace mutate_1 {
45
57
  let title_1: string;
46
58
  export { title_1 as title };
@@ -48,6 +60,12 @@ declare namespace translations {
48
60
  export { name_1 as name };
49
61
  let description_1: string;
50
62
  export { description_1 as description };
63
+ let avatarRequired_1: string;
64
+ export { avatarRequired_1 as avatarRequired };
65
+ let uploadAvatarTitle_1: string;
66
+ export { uploadAvatarTitle_1 as uploadAvatarTitle };
67
+ let uploadAvatarTip_1: string;
68
+ export { uploadAvatarTip_1 as uploadAvatarTip };
51
69
  }
52
70
  export { mutate_1 as mutate };
53
71
  }
@@ -1,4 +1,4 @@
1
- const e = {
1
+ const a = {
2
2
  en: {
3
3
  search: "Search",
4
4
  orgs: "Organizations",
@@ -9,12 +9,18 @@ const e = {
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
- description: "Description"
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,15 +33,21 @@ const e = {
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
- description: "组织描述"
44
+ description: "组织描述",
45
+ avatarRequired: "头像为必填项",
46
+ uploadAvatarTitle: "上传头像",
47
+ uploadAvatarTip: "点击上传组织头像"
36
48
  }
37
49
  }
38
50
  };
39
51
  export {
40
- e as default
52
+ a as default
41
53
  };
@@ -1,5 +1,6 @@
1
1
  export default function useOrg(session: any): {
2
2
  getOrgs: (this: any, args_0?: any) => Promise<any>;
3
3
  createOrg: (this: any, args_0?: any) => Promise<any>;
4
+ updateOrg: (this: any, orgId?: any, args_1?: any) => Promise<any>;
4
5
  getCurrentOrg: (this: any, roleName?: any) => Promise<any>;
5
6
  };
@@ -1,13 +1,13 @@
1
- import { useMemoizedFn as n } from "ahooks";
2
- import i from "@arcblock/ux/lib/Toast";
3
- import { getBlockletSDK as m } from "@blocklet/js-sdk";
4
- import { formatAxiosError as p } from "../../UserCenter/libs/utils.js";
5
- function w(c) {
6
- const e = m(), a = n(async ({ search: t, page: r = 1, pageSize: o = 20 }) => {
1
+ import { useMemoizedFn as s } from "ahooks";
2
+ import c from "@arcblock/ux/lib/Toast";
3
+ import { getBlockletSDK as y } from "@blocklet/js-sdk";
4
+ import { formatAxiosError as u } from "../../UserCenter/libs/utils.js";
5
+ function x(g) {
6
+ const n = y(), l = s(async ({ search: t, page: r = 1, pageSize: o = 20 }) => {
7
7
  try {
8
- return await e.user.getOrgs({ search: t, page: r, pageSize: o });
9
- } catch (s) {
10
- return console.error(s), {
8
+ return await n.user.getOrgs({ search: t, page: r, pageSize: o });
9
+ } catch (e) {
10
+ return console.error(e), {
11
11
  orgs: [],
12
12
  paging: {
13
13
  page: r,
@@ -16,32 +16,39 @@ function w(c) {
16
16
  }
17
17
  };
18
18
  }
19
- }), u = n(async ({ name: t, description: r }) => {
19
+ }), i = s(async ({ name: t, description: r, avatar: o }) => {
20
20
  try {
21
- return (await e.user.createOrg({ name: t, description: r })).data;
22
- } catch (o) {
23
- return console.error(o), i.error(p(o)), null;
21
+ return (await n.user.createOrg({ name: t, description: r, avatar: o })).data;
22
+ } catch (e) {
23
+ return console.error(e), c.error(u(e)), null;
24
24
  }
25
- }), g = n(async (t) => {
25
+ }), p = s(async (t, { name: r, description: o, avatar: e }) => {
26
26
  try {
27
- return await e.user.getOrg(t);
27
+ return (await n.user.updateOrg(t, { name: r, description: o, avatar: e })).data;
28
+ } catch (a) {
29
+ return console.error(a), c.error(u(a)), null;
30
+ }
31
+ }), d = s(async (t) => {
32
+ try {
33
+ return await n.user.getOrg(t);
28
34
  } catch (r) {
29
- return c.logout(), console.error(r), null;
35
+ return g.logout(), console.error(r), null;
30
36
  }
31
- }), l = n(async (t) => {
37
+ }), m = s(async (t) => {
32
38
  try {
33
- const r = await e.user.getRole(t);
34
- return r.orgId ? await g(r.orgId) : null;
39
+ const r = await n.user.getRole(t);
40
+ return r.orgId ? await d(r.orgId) : null;
35
41
  } catch (r) {
36
42
  return console.error(r), null;
37
43
  }
38
44
  });
39
45
  return {
40
- getOrgs: a,
41
- createOrg: u,
42
- getCurrentOrg: l
46
+ getOrgs: l,
47
+ createOrg: i,
48
+ updateOrg: p,
49
+ getCurrentOrg: m
43
50
  };
44
51
  }
45
52
  export {
46
- w as default
53
+ x as default
47
54
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/ui-react",
3
- "version": "3.4.7",
3
+ "version": "3.4.8",
4
4
  "description": "Some useful front-end web components that can be used in Blocklets.",
5
5
  "keywords": [
6
6
  "react",
@@ -36,11 +36,12 @@
36
36
  "dependencies": {
37
37
  "@abtnode/constant": "^1.17.7",
38
38
  "@abtnode/util": "^1.17.7",
39
- "@arcblock/bridge": "3.4.7",
40
- "@arcblock/icons": "3.4.7",
41
- "@arcblock/react-hooks": "3.4.7",
39
+ "@arcblock/bridge": "3.4.8",
40
+ "@arcblock/icons": "3.4.8",
41
+ "@arcblock/react-hooks": "3.4.8",
42
42
  "@arcblock/ws": "^1.28.5",
43
43
  "@blocklet/did-space-react": "^1.2.15",
44
+ "@blocklet/uploader": "^0.3.19",
44
45
  "@iconify-icons/logos": "^1.2.36",
45
46
  "@iconify-icons/material-symbols": "^1.2.58",
46
47
  "@iconify-icons/tabler": "^1.2.95",
@@ -83,7 +84,7 @@
83
84
  "access": "public"
84
85
  },
85
86
  "devDependencies": {
86
- "@arcblock/did-connect-react": "3.4.7",
87
+ "@arcblock/did-connect-react": "3.4.8",
87
88
  "@babel/preset-env": "^7.28.0",
88
89
  "@babel/preset-react": "^7.27.1",
89
90
  "@babel/preset-typescript": "^7.27.1",
@@ -97,5 +98,5 @@
97
98
  "typescript": "~5.5.4",
98
99
  "unbuild": "^2.0.0"
99
100
  },
100
- "gitHead": "eb0fca0015ba4ee80affb3b03d47505c45f8ec1c"
101
+ "gitHead": "6bbdc63e6e0b7ddfd5e37f2ac6bb44ee49da7139"
101
102
  }
@@ -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
+ }));