@arcblock/ux 3.1.47 → 3.1.49
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/Config/config-provider.d.ts +1 -0
- package/lib/DIDConnect/auth-apps/switch-role.d.ts +1 -0
- package/lib/Locale/context.d.ts +1 -0
- package/lib/Locale/context.js +45 -45
- package/lib/OrgTransfer/index.d.ts +5 -0
- package/lib/OrgTransfer/index.js +38 -0
- package/lib/OrgTransfer/locales.d.ts +25 -0
- package/lib/OrgTransfer/locales.js +27 -0
- package/lib/OrgTransfer/selector.d.ts +2 -0
- package/lib/OrgTransfer/selector.js +174 -0
- package/lib/OrgTransfer/type.d.ts +28 -0
- package/lib/OrgTransfer/type.js +1 -0
- package/lib/UserCard/use-follow.d.ts +0 -2
- package/lib/UserCard/use-follow.js +35 -39
- package/lib/Util/client.d.ts +6 -0
- package/lib/Util/client.js +7 -0
- package/lib/Util/index.d.ts +2 -0
- package/lib/Util/index.js +58 -54
- package/lib/package.json.js +1 -1
- package/package.json +7 -7
- package/src/DIDConnect/auth-apps/switch-role.tsx +1 -1
- package/src/Locale/context.tsx +9 -3
- package/src/OrgTransfer/index.tsx +53 -0
- package/src/OrgTransfer/locales.ts +25 -0
- package/src/OrgTransfer/selector.tsx +249 -0
- package/src/OrgTransfer/type.ts +31 -0
- package/src/UserCard/use-follow.tsx +1 -10
- package/src/Util/client.ts +9 -0
- package/src/Util/index.ts +11 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org transfer selector
|
|
3
|
+
*/
|
|
4
|
+
import { useImperativeHandle, useState } from 'react';
|
|
5
|
+
import { useReactive, useDebounce, useInfiniteScroll, useMemoizedFn } from 'ahooks';
|
|
6
|
+
import { Button, Typography, Box, Autocomplete, TextField, CircularProgress } from '@mui/material';
|
|
7
|
+
import { AxiosError } from 'axios';
|
|
8
|
+
import Toast from '../Toast';
|
|
9
|
+
import Dialog from '../Dialog';
|
|
10
|
+
import { Org, OrgTransferSelectorProps } from './type';
|
|
11
|
+
import { client, OrgQueryType } from '../Util/client';
|
|
12
|
+
import { formatAxiosError } from '../Util';
|
|
13
|
+
import { translate } from '../Locale/util';
|
|
14
|
+
import translations from './locales';
|
|
15
|
+
|
|
16
|
+
const PAGE_SIZE = 20;
|
|
17
|
+
|
|
18
|
+
export default function OrgTransferSelector({
|
|
19
|
+
ref,
|
|
20
|
+
org,
|
|
21
|
+
onSuccess,
|
|
22
|
+
resourceId,
|
|
23
|
+
locale = 'en',
|
|
24
|
+
dialogProps,
|
|
25
|
+
}: OrgTransferSelectorProps) {
|
|
26
|
+
const t = useMemoizedFn((key, data = {}) => {
|
|
27
|
+
return translate(translations, key, locale, 'en', data);
|
|
28
|
+
});
|
|
29
|
+
const [searchText, setSearchText] = useState('');
|
|
30
|
+
|
|
31
|
+
const state = useReactive<{
|
|
32
|
+
open: boolean;
|
|
33
|
+
selectedOrg: Org | null;
|
|
34
|
+
}>({
|
|
35
|
+
open: false,
|
|
36
|
+
selectedOrg: null,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const debouncedSearchText = useDebounce(searchText, { wait: 500 });
|
|
40
|
+
|
|
41
|
+
const { data, loadMore, loadingMore, loading, reload } = useInfiniteScroll(
|
|
42
|
+
async (d) => {
|
|
43
|
+
// 只有对话框打开时才发起请求
|
|
44
|
+
if (!state.open) return { list: [], total: 0 };
|
|
45
|
+
|
|
46
|
+
const page = d ? Math.ceil(d.list.length / PAGE_SIZE) + 1 : 1;
|
|
47
|
+
const params = { page, pageSize: PAGE_SIZE, search: debouncedSearchText, type: OrgQueryType.OWNED };
|
|
48
|
+
const response = await client.user.getOrgs(params);
|
|
49
|
+
|
|
50
|
+
const { orgs: resultOrgs = [], paging } = response || {};
|
|
51
|
+
return { list: resultOrgs, total: paging?.total || 0 };
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
reloadDeps: [debouncedSearchText],
|
|
55
|
+
isNoMore: (d) => {
|
|
56
|
+
if (!d?.list.length) return true;
|
|
57
|
+
return d.list.length >= d?.total;
|
|
58
|
+
},
|
|
59
|
+
onError: (error) => {
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.error('Get orgs failed', error);
|
|
62
|
+
},
|
|
63
|
+
manual: true, // 手动触发,在对话框打开时再加载
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const handleClose = useMemoizedFn(() => {
|
|
68
|
+
state.open = false;
|
|
69
|
+
state.selectedOrg = null;
|
|
70
|
+
setSearchText(''); // 关闭时重置搜索
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const handleConfirm = useMemoizedFn(async () => {
|
|
74
|
+
try {
|
|
75
|
+
if (!resourceId) {
|
|
76
|
+
throw new Error(t('resourceIdRequired'));
|
|
77
|
+
}
|
|
78
|
+
if (!state.selectedOrg) {
|
|
79
|
+
throw new Error(t('placeholder'));
|
|
80
|
+
}
|
|
81
|
+
if (state.selectedOrg.id === org.id) {
|
|
82
|
+
throw new Error(t('organizationSameAsCurrent'));
|
|
83
|
+
}
|
|
84
|
+
await client.user.migrateResourceToOrg({ form: org.id, to: state.selectedOrg.id, resourceId });
|
|
85
|
+
handleClose();
|
|
86
|
+
onSuccess?.(state.selectedOrg);
|
|
87
|
+
Toast.success(t('tranferSuccess'));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Resource transfer failed', error);
|
|
90
|
+
Toast.error(formatAxiosError(error as AxiosError));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
useImperativeHandle(ref, () => ({
|
|
95
|
+
open: (options = {}) => {
|
|
96
|
+
Object.assign(state, { open: true, selectedOrg: null }, options);
|
|
97
|
+
// 对话框打开时重新加载数据
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
if (state.open) {
|
|
100
|
+
reload();
|
|
101
|
+
}
|
|
102
|
+
}, 0);
|
|
103
|
+
},
|
|
104
|
+
close: handleClose,
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
const organizations = data?.list || [];
|
|
108
|
+
// const hasMore = data ? data.list.length < data.total : false;
|
|
109
|
+
|
|
110
|
+
const { placeholder = '', ...rest } = dialogProps || {};
|
|
111
|
+
return (
|
|
112
|
+
<Dialog
|
|
113
|
+
title={t('transferOrg')}
|
|
114
|
+
fullWidth
|
|
115
|
+
maxWidth="sm"
|
|
116
|
+
open={state.open}
|
|
117
|
+
onClose={handleClose}
|
|
118
|
+
actions={
|
|
119
|
+
<>
|
|
120
|
+
<Button onClick={handleClose} color="secondary" variant="outlined">
|
|
121
|
+
{t('cancel')}
|
|
122
|
+
</Button>
|
|
123
|
+
<Button onClick={handleConfirm} color="primary" variant="contained" disabled={!state.selectedOrg}>
|
|
124
|
+
{t('confirm')}
|
|
125
|
+
</Button>
|
|
126
|
+
</>
|
|
127
|
+
}
|
|
128
|
+
{...(rest || {})}>
|
|
129
|
+
<Box>
|
|
130
|
+
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
131
|
+
{placeholder || t('placeholder')}
|
|
132
|
+
</Typography>
|
|
133
|
+
|
|
134
|
+
<Autocomplete
|
|
135
|
+
options={organizations}
|
|
136
|
+
getOptionLabel={(option) => option?.name || ''}
|
|
137
|
+
isOptionEqualToValue={(option, value) => option?.id === value?.id}
|
|
138
|
+
value={state.selectedOrg}
|
|
139
|
+
onChange={(_, newValue) => {
|
|
140
|
+
// 如果点击的是加载项,触发加载更多
|
|
141
|
+
if (newValue?.isLoadingItem && !loadingMore) {
|
|
142
|
+
loadMore();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// 忽略加载项的选择
|
|
146
|
+
if (newValue?.isLoadingItem) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
state.selectedOrg = newValue;
|
|
150
|
+
// 选择选项后清空搜索文本
|
|
151
|
+
if (newValue) {
|
|
152
|
+
setSearchText('');
|
|
153
|
+
}
|
|
154
|
+
}}
|
|
155
|
+
inputValue={state.selectedOrg ? state.selectedOrg.name : searchText}
|
|
156
|
+
onInputChange={(_, newInputValue, reason) => {
|
|
157
|
+
// 只有用户键盘输入时才更新搜索文本
|
|
158
|
+
if (reason === 'input') {
|
|
159
|
+
// 用户开始输入时,清除当前选中的组织,进入搜索模式
|
|
160
|
+
if (state.selectedOrg) {
|
|
161
|
+
state.selectedOrg = null;
|
|
162
|
+
}
|
|
163
|
+
setSearchText(newInputValue);
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
loading={loading}
|
|
167
|
+
loadingText={t('loading')}
|
|
168
|
+
noOptionsText={loading ? t('loading') : t('noResults')}
|
|
169
|
+
renderInput={(params) => (
|
|
170
|
+
<TextField
|
|
171
|
+
{...params}
|
|
172
|
+
variant="outlined"
|
|
173
|
+
fullWidth
|
|
174
|
+
InputProps={{
|
|
175
|
+
...params.InputProps,
|
|
176
|
+
endAdornment: (
|
|
177
|
+
<>
|
|
178
|
+
{loading && <CircularProgress color="inherit" size={20} />}
|
|
179
|
+
{params.InputProps.endAdornment}
|
|
180
|
+
</>
|
|
181
|
+
),
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
renderOption={(props, option) => {
|
|
186
|
+
// 特殊处理加载项
|
|
187
|
+
if (option.isLoadingItem) {
|
|
188
|
+
return (
|
|
189
|
+
<Box component="li" {...props} key={option.id}>
|
|
190
|
+
<Box
|
|
191
|
+
sx={{
|
|
192
|
+
display: 'flex',
|
|
193
|
+
alignItems: 'center',
|
|
194
|
+
justifyContent: 'center',
|
|
195
|
+
flex: 1,
|
|
196
|
+
py: 1,
|
|
197
|
+
color: 'primary.main',
|
|
198
|
+
cursor: 'pointer',
|
|
199
|
+
}}>
|
|
200
|
+
{loadingMore && <CircularProgress size={16} sx={{ mr: 1 }} />}
|
|
201
|
+
<Typography variant="body2" sx={{ fontStyle: loadingMore ? 'italic' : 'normal' }}>
|
|
202
|
+
{option.name}
|
|
203
|
+
</Typography>
|
|
204
|
+
</Box>
|
|
205
|
+
</Box>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 正常组织项的渲染
|
|
210
|
+
return (
|
|
211
|
+
<Box component="li" {...props} key={option.id}>
|
|
212
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0 }}>
|
|
213
|
+
<Typography
|
|
214
|
+
variant="body1"
|
|
215
|
+
sx={{
|
|
216
|
+
overflow: 'hidden',
|
|
217
|
+
textOverflow: 'ellipsis',
|
|
218
|
+
whiteSpace: 'nowrap',
|
|
219
|
+
}}>
|
|
220
|
+
{option.name}
|
|
221
|
+
</Typography>
|
|
222
|
+
{option.description && (
|
|
223
|
+
<Typography
|
|
224
|
+
variant="body2"
|
|
225
|
+
color="text.secondary"
|
|
226
|
+
sx={{
|
|
227
|
+
display: '-webkit-box',
|
|
228
|
+
WebkitLineClamp: 3,
|
|
229
|
+
WebkitBoxOrient: 'vertical',
|
|
230
|
+
overflow: 'hidden',
|
|
231
|
+
textOverflow: 'ellipsis',
|
|
232
|
+
}}>
|
|
233
|
+
{option.description}
|
|
234
|
+
</Typography>
|
|
235
|
+
)}
|
|
236
|
+
</Box>
|
|
237
|
+
</Box>
|
|
238
|
+
);
|
|
239
|
+
}}
|
|
240
|
+
sx={{
|
|
241
|
+
'& .MuiAutocomplete-listbox': {
|
|
242
|
+
maxHeight: 200,
|
|
243
|
+
},
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
</Box>
|
|
247
|
+
</Dialog>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { ButtonProps } from '@mui/material';
|
|
3
|
+
import Dialog from '../Dialog';
|
|
4
|
+
|
|
5
|
+
export type Org = {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
ownerDid: string;
|
|
10
|
+
metadata?: Record<string, any>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type DialogProps = ComponentProps<typeof Dialog> & {
|
|
14
|
+
placeholder?: string; // 搜索框的 placeholder
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type OrgTransferSelectorProps = Omit<OrgTransferProps, 'children' | 'buttonProps' | 'buttonText'> & {
|
|
18
|
+
ref: React.RefObject<unknown | null>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface OrgTransferProps {
|
|
22
|
+
resourceId: string;
|
|
23
|
+
onSuccess?: (org: Org) => void;
|
|
24
|
+
org?: Org; // 当前所在的 Org
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
buttonProps?: ButtonProps;
|
|
27
|
+
buttonText?: string;
|
|
28
|
+
dialogProps?: DialogProps;
|
|
29
|
+
locale?: string;
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}
|
|
@@ -5,18 +5,9 @@ import isNil from 'lodash/isNil';
|
|
|
5
5
|
import type { AxiosError } from 'axios';
|
|
6
6
|
import { BlockletSDK } from '@blocklet/js-sdk';
|
|
7
7
|
import Toast from '../Toast';
|
|
8
|
+
import { formatAxiosError } from '../Util';
|
|
8
9
|
import type { User } from './types';
|
|
9
10
|
|
|
10
|
-
export const formatAxiosError = (err: AxiosError) => {
|
|
11
|
-
const { response } = err;
|
|
12
|
-
|
|
13
|
-
if (response) {
|
|
14
|
-
return `Request failed: ${response.status} ${response.statusText}: ${JSON.stringify(response.data)}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return err.message;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
11
|
/**
|
|
21
12
|
* 登录用户与当前用户(userDid)的关注关系
|
|
22
13
|
*/
|
package/src/Util/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import timezone from 'dayjs/plugin/timezone';
|
|
|
15
15
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
16
16
|
import updateLocale from 'dayjs/plugin/updateLocale';
|
|
17
17
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
|
18
|
+
import type { AxiosError } from 'axios';
|
|
18
19
|
import semver from 'semver';
|
|
19
20
|
|
|
20
21
|
import { DID_PREFIX, BLOCKLET_SERVICE_PATH_PREFIX } from './constant';
|
|
@@ -737,3 +738,13 @@ export const isSupportFollow = () => {
|
|
|
737
738
|
const jsSdkVersionSupport = compareVersions(jsSdkVersion, '1.16.49-beta-20250822-070545-6d3344cc');
|
|
738
739
|
return uxVersionSupport && serverVersionSupport && jsSdkVersionSupport;
|
|
739
740
|
};
|
|
741
|
+
|
|
742
|
+
export const formatAxiosError = (err: AxiosError) => {
|
|
743
|
+
const { response } = err;
|
|
744
|
+
|
|
745
|
+
if (response) {
|
|
746
|
+
return `Request failed: ${response.status} ${response.statusText}: ${JSON.stringify(response.data)}`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return err.message || 'Unknown error occurred';
|
|
750
|
+
};
|