@blocklet/ui-react 2.10.2 → 2.10.4

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.
@@ -100,3 +100,38 @@ export type CreatePassportProps = {
100
100
  width?: string;
101
101
  height?: string;
102
102
  };
103
+ export type BlockletMetaProps = {
104
+ appLogo?: React.ReactNode;
105
+ appName?: string;
106
+ theme?: {
107
+ background?: string;
108
+ };
109
+ enableConnect?: boolean;
110
+ enableLocale?: boolean;
111
+ navigation?: Array<{
112
+ title?: string | object;
113
+ link?: string | object;
114
+ icon?: string;
115
+ items?: Array<{
116
+ title?: string | object;
117
+ link?: string | object;
118
+ }>;
119
+ }>;
120
+ };
121
+ export type SessionManagerProps = {
122
+ showText?: boolean;
123
+ showRole?: boolean;
124
+ switchDid?: boolean;
125
+ switchProfile?: boolean;
126
+ switchPassport?: boolean;
127
+ disableLogout?: boolean;
128
+ onLogin?: () => void;
129
+ onLogout?: () => void;
130
+ onSwitchDid?: () => void;
131
+ onSwitchProfile?: () => void;
132
+ onSwitchPassport?: () => void;
133
+ menu?: any[];
134
+ menuRender?: () => void;
135
+ dark?: boolean;
136
+ size?: number;
137
+ };
@@ -1,10 +1,11 @@
1
+ import { BlockletMetaProps, SessionManagerProps } from '../@types';
1
2
  declare const _default: import("react").ComponentType<{
2
- [x: string]: any;
3
- meta: any;
4
- addons: any;
5
- sessionManagerProps: any;
6
- homeLink: any;
7
- theme: any;
8
- hideNavMenu: any;
3
+ meta: BlockletMetaProps;
4
+ addons: Function | JSX.Element;
5
+ sessionManagerProps: SessionManagerProps;
6
+ homeLink: string;
7
+ theme: object;
8
+ hideNavMenu: boolean;
9
+ bordered?: boolean;
9
10
  }>;
10
11
  export default _default;
@@ -1,6 +1,5 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
- import PropTypes from "prop-types";
4
3
  import { withErrorBoundary } from "react-error-boundary";
5
4
  import { ErrorFallback } from "@arcblock/ux/lib/ErrorBoundary";
6
5
  import { styled } from "@arcblock/ux/lib/Theme";
@@ -11,7 +10,6 @@ import { temp as colors } from "@arcblock/ux/lib/Colors";
11
10
  import omit from "lodash/omit";
12
11
  import Icon from "../Icon/index.js";
13
12
  import OverridableThemeProvider from "../common/overridable-theme-provider.js";
14
- import { BlockletMetaProps, SessionManagerProps } from "../types.js";
15
13
  import { mapRecursive, flatRecursive, matchPaths } from "../utils.js";
16
14
  import { publicPath, formatBlockletInfo, getLocalizedNavigation } from "../blocklets.js";
17
15
  import HeaderAddons from "../common/header-addons.js";
@@ -50,7 +48,17 @@ const parseNavigation = (navigation) => {
50
48
  const matchedIndex = matchPaths(flattened.map((item) => item.link));
51
49
  return { navItems, activeId: matchedIndex >= 0 ? flattened[matchedIndex].id : null };
52
50
  };
53
- function Header({ meta, addons, sessionManagerProps, homeLink, theme: themeOverrides, hideNavMenu, ...rest }) {
51
+ function Header({
52
+ meta = {},
53
+ addons,
54
+ sessionManagerProps = {
55
+ showRole: true
56
+ },
57
+ homeLink = publicPath,
58
+ theme: themeOverrides,
59
+ hideNavMenu = false,
60
+ ...rest
61
+ }) {
54
62
  useWalletHiddenTopbar();
55
63
  const { locale } = useLocaleContext() || {};
56
64
  const formattedBlocklet = useMemo(() => {
@@ -70,7 +78,10 @@ function Header({ meta, addons, sessionManagerProps, homeLink, theme: themeOverr
70
78
  const parsedNavigation = parseNavigation(navigation);
71
79
  const { navItems, activeId } = parsedNavigation;
72
80
  const _addons = typeof addons === "function" ? (builtInAddons) => addons(builtInAddons, { navigation: parsedNavigation }) : addons;
73
- const headerAddons = /* @__PURE__ */ jsx(HeaderAddons, { formattedBlocklet, addons: _addons, sessionManagerProps });
81
+ const headerAddons = (
82
+ // @ts-ignore
83
+ /* @__PURE__ */ jsx(HeaderAddons, { formattedBlocklet, addons: _addons, sessionManagerProps })
84
+ );
74
85
  return /* @__PURE__ */ jsx(OverridableThemeProvider, { theme: themeOverrides, children: /* @__PURE__ */ jsx(
75
86
  StyledUxHeader,
76
87
  {
@@ -80,42 +91,23 @@ function Header({ meta, addons, sessionManagerProps, homeLink, theme: themeOverr
80
91
  ...omit(rest, ["bordered"]),
81
92
  $bordered: rest?.bordered,
82
93
  $bgcolor: theme?.background?.header,
83
- children: hideNavMenu || !navItems?.length ? null : ({ isMobile }) => /* @__PURE__ */ jsx(
84
- NavMenu,
85
- {
86
- mode: isMobile ? "inline" : "horizontal",
87
- activeId,
88
- items: navItems,
89
- className: "header-nav",
90
- bgColor: "transparent",
91
- textColor: "#777"
92
- }
94
+ children: hideNavMenu || !navItems?.length ? null : ({ isMobile }) => (
95
+ // @ts-ignore
96
+ /* @__PURE__ */ jsx(
97
+ NavMenu,
98
+ {
99
+ mode: isMobile ? "inline" : "horizontal",
100
+ activeId,
101
+ items: navItems,
102
+ className: "header-nav",
103
+ bgColor: "transparent",
104
+ textColor: "#777"
105
+ }
106
+ )
93
107
  )
94
108
  }
95
109
  ) });
96
110
  }
97
- Header.propTypes = {
98
- meta: BlockletMetaProps,
99
- // 需要考虑 定制的 addons 与内置的 连接钱包/选择语言 addons 共存的情况
100
- // - PropTypes.func: 可以把自定义 addons 插在 session-manager 或 locale-selector (如果存在的话) 前/中/后
101
- // - PropTypes.node: 将 addons 原样传给 UX Header 组件
102
- addons: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
103
- sessionManagerProps: SessionManagerProps,
104
- homeLink: PropTypes.string,
105
- // 允许覆盖 header 内置的 theme
106
- theme: PropTypes.object,
107
- hideNavMenu: PropTypes.bool
108
- };
109
- Header.defaultProps = {
110
- meta: {},
111
- addons: null,
112
- sessionManagerProps: {
113
- showRole: true
114
- },
115
- homeLink: publicPath,
116
- theme: null,
117
- hideNavMenu: false
118
- };
119
111
  const StyledUxHeader = styled(ResponsiveHeader)`
120
112
  ${({ $bgcolor }) => `background-color: ${$bgcolor || "#fff"};`}
121
113
  font-family: Inter, Avenir, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,
@@ -1,23 +1,10 @@
1
+ import 'iconify-icon';
2
+ import type { AvatarProps, BoxProps } from '@mui/material';
1
3
  /**
2
4
  * Icon 组件, 基于 mui Avatar 组件扩展对 iconify 的支持
3
5
  */
4
- declare function Icon({ icon, size, sx, ...rest }: {
5
- [x: string]: any;
6
- icon: any;
7
- size: any;
8
- sx: any;
9
- }): import("react").JSX.Element | null;
10
- declare namespace Icon {
11
- namespace propTypes {
12
- let icon: any;
13
- let size: any;
14
- let sx: any;
15
- }
16
- namespace defaultProps {
17
- let size_1: null;
18
- export { size_1 as size };
19
- let sx_1: null;
20
- export { sx_1 as sx };
21
- }
22
- }
23
- export default Icon;
6
+ export default function Icon({ icon, size, sx, ...rest }: {
7
+ icon: string;
8
+ size?: number;
9
+ sx?: BoxProps['sx'];
10
+ } & AvatarProps): import("react").JSX.Element | null;
package/lib/Icon/index.js CHANGED
@@ -1,9 +1,14 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import "iconify-icon";
3
- import PropTypes from "prop-types";
4
- import Avatar from "@mui/material/Avatar";
3
+ import { Avatar } from "@mui/material";
4
+ import { Icon as IconifyIcon } from "@iconify/react";
5
5
  import { isUrl, isIconifyString } from "../utils.js";
6
- export default function Icon({ icon, size, sx, ...rest }) {
6
+ export default function Icon({
7
+ icon,
8
+ size,
9
+ sx,
10
+ ...rest
11
+ }) {
7
12
  const _sx = [...Array.isArray(sx) ? sx : [sx]];
8
13
  if (size) {
9
14
  _sx.push({ width: size, height: size });
@@ -24,11 +29,11 @@ export default function Icon({ icon, size, sx, ...rest }) {
24
29
  });
25
30
  }
26
31
  if (isUrl(icon)) {
27
- return /* @__PURE__ */ jsx(Avatar, { as: "span", ...rest, src: icon, sx: _sx });
32
+ return /* @__PURE__ */ jsx(Avatar, { component: "span", ...rest, src: icon, sx: _sx });
28
33
  }
29
34
  if (isIconifyString(icon)) {
30
35
  const height = size ? 0.6 * size + 4 : 0;
31
- return /* @__PURE__ */ jsx(Avatar, { as: "span", ...rest, sx: _sx, children: /* @__PURE__ */ jsx("iconify-icon", { icon, height: height || void 0 }) });
36
+ return /* @__PURE__ */ jsx(Avatar, { component: "span", ...rest, sx: _sx, children: /* @__PURE__ */ jsx(IconifyIcon, { icon, height: height || void 0 }) });
32
37
  }
33
38
  if (icon && typeof icon === "string") {
34
39
  _sx.push({
@@ -37,19 +42,7 @@ export default function Icon({ icon, size, sx, ...rest }) {
37
42
  ...size && { fontSize: size - 2 }
38
43
  }
39
44
  });
40
- return /* @__PURE__ */ jsx(Avatar, { as: "span", ...rest, sx: _sx, children: Array.from(icon)[0] });
45
+ return /* @__PURE__ */ jsx(Avatar, { component: "span", ...rest, sx: _sx, children: Array.from(icon)[0] });
41
46
  }
42
47
  return null;
43
48
  }
44
- Icon.propTypes = {
45
- // icon 支持 2 种形式:
46
- // 1. iconify icon name: <prefix>:<name>
47
- // 2. url
48
- icon: PropTypes.string.isRequired,
49
- size: PropTypes.number,
50
- sx: PropTypes.oneOfType([PropTypes.array, PropTypes.func, PropTypes.object])
51
- };
52
- Icon.defaultProps = {
53
- size: null,
54
- sx: null
55
- };
@@ -1,4 +1,5 @@
1
1
  import { User } from '../../@types';
2
- export default function ConfigProfile({ user }: {
2
+ export default function ConfigProfile({ user, onSave }: {
3
3
  user: User;
4
+ onSave: (type: 'profile') => void;
4
5
  }): import("react").JSX.Element;
@@ -10,8 +10,8 @@ const languages = [
10
10
  { label: "English", value: "en" },
11
11
  { label: "\u4E2D\u6587", value: "zh" }
12
12
  ];
13
- export default function ConfigProfile({ user }) {
14
- const { locale } = useLocaleContext();
13
+ export default function ConfigProfile({ user, onSave }) {
14
+ const { locale, changeLocale } = useLocaleContext();
15
15
  const t = useMemoizedFn((key, data = {}) => {
16
16
  return translate(translations, key, locale, "en", data);
17
17
  });
@@ -29,6 +29,8 @@ export default function ConfigProfile({ user }) {
29
29
  }),
30
30
  sleep(350)
31
31
  ]);
32
+ await onSave("profile");
33
+ changeLocale(value);
32
34
  currentState.locale = value;
33
35
  } finally {
34
36
  currentState.loading = false;
@@ -3,8 +3,8 @@ type PrivacyConfig = {
3
3
  name: string;
4
4
  value: boolean;
5
5
  };
6
- export default function Privacy({ configList, onSave }: {
6
+ export default function Privacy({ configList, onSave, }: {
7
7
  configList: PrivacyConfig[];
8
- onSave: () => void;
8
+ onSave: (type: 'privacy') => void;
9
9
  }): import("react").JSX.Element;
10
10
  export {};
@@ -9,7 +9,10 @@ import Toast from "@arcblock/ux/lib/Toast";
9
9
  import { translations } from "../libs/locales.js";
10
10
  import { client } from "../../libs/client.js";
11
11
  import { formatAxiosError } from "../libs/utils.js";
12
- export default function Privacy({ configList, onSave }) {
12
+ export default function Privacy({
13
+ configList,
14
+ onSave
15
+ }) {
13
16
  const [dataList, setDataList] = useState(configList);
14
17
  const { locale } = useLocaleContext();
15
18
  const t = useMemoizedFn((key, data = {}) => {
@@ -29,7 +32,7 @@ export default function Privacy({ configList, onSave }) {
29
32
  })
30
33
  );
31
34
  Toast.success(t("saveSuccess"));
32
- onSave();
35
+ onSave("privacy");
33
36
  } catch (err) {
34
37
  Toast.error(formatAxiosError(err));
35
38
  }
@@ -69,31 +72,33 @@ export default function Privacy({ configList, onSave }) {
69
72
  }
70
73
  }
71
74
  },
72
- children: dataList.map((item) => /* @__PURE__ */ jsx(
73
- Switch,
74
- {
75
- checked: !item.value,
76
- labelProps: {
77
- label: /* @__PURE__ */ jsx(
78
- Typography,
79
- {
80
- color: "text.primary",
81
- sx: {
82
- fontSize: 14,
83
- display: "flex",
84
- flexFlow: "wrap",
85
- columnGap: 1,
86
- flex: 1
87
- },
88
- children: t("toPublic", { name: item.name })
89
- }
90
- )
75
+ children: dataList.map((item) => {
76
+ return /* @__PURE__ */ jsx(
77
+ Switch,
78
+ {
79
+ checked: !item.value,
80
+ labelProps: {
81
+ label: /* @__PURE__ */ jsx(
82
+ Typography,
83
+ {
84
+ color: "text.primary",
85
+ sx: {
86
+ fontSize: 14,
87
+ display: "flex",
88
+ flexFlow: "wrap",
89
+ columnGap: 1,
90
+ flex: 1
91
+ },
92
+ children: t("toPublic", { name: item.name })
93
+ }
94
+ )
95
+ },
96
+ size: "small",
97
+ onChange: (event) => handleChangeSwitch(item.key, event.target.checked)
91
98
  },
92
- size: "small",
93
- onChange: (event) => handleChangeSwitch(item.key, event.target.checked)
94
- },
95
- item.key
96
- ))
99
+ item.key
100
+ );
101
+ })
97
102
  }
98
103
  );
99
104
  }
@@ -2,7 +2,7 @@ import type { BoxProps } from '@mui/material';
2
2
  import { User, UserCenterTab } from '../../@types';
3
3
  export default function Settings({ user, settings, onSave, ...rest }: {
4
4
  user: User;
5
- onSave: () => void;
5
+ onSave: (type: 'privacy' | 'profile') => void;
6
6
  settings: {
7
7
  userCenterTabs: UserCenterTab[];
8
8
  };
@@ -37,7 +37,7 @@ export default function Settings({
37
37
  {
38
38
  label: t("commonSetting.title"),
39
39
  value: "common",
40
- content: /* @__PURE__ */ jsx(ConfigProfile, { user })
40
+ content: /* @__PURE__ */ jsx(ConfigProfile, { user, onSave })
41
41
  },
42
42
  {
43
43
  label: t("notificationManagement"),
@@ -149,13 +149,19 @@ export default function UserCenter({
149
149
  {
150
150
  user: userState.data,
151
151
  settings: { userCenterTabs },
152
- onSave: async () => {
153
- await privacyState.runAsync();
154
- return privacyState.data;
152
+ onSave: async (type) => {
153
+ if (type === "privacy") {
154
+ await privacyState.runAsync();
155
+ return privacyState.data;
156
+ }
157
+ if (type === "profile") {
158
+ await session.refresh();
159
+ }
160
+ return null;
155
161
  }
156
162
  }
157
163
  );
158
- }, [userState.data]);
164
+ }, [userState.data, userCenterTabs, privacyState.data, privacyState.runAsync]);
159
165
  const openSettings = useMemoizedFn(() => {
160
166
  confirmApi.open({
161
167
  title: t("settings"),
@@ -352,15 +358,13 @@ export default function UserCenter({
352
358
  }
353
359
  }
354
360
  ),
355
- isMyself ? /* @__PURE__ */ jsxs(Fragment, { children: [
356
- /* @__PURE__ */ jsxs(Box, { children: [
357
- /* @__PURE__ */ jsx(Typography, { sx: { fontWeight: 600, mb: 1.5 }, children: t("myInfo") }),
358
- /* @__PURE__ */ jsx(UserInfo, { user: userState.data })
359
- ] }),
360
- /* @__PURE__ */ jsxs(Box, { children: [
361
- /* @__PURE__ */ jsx(Typography, { sx: { fontWeight: 600, mb: 1.5 }, children: t("passport") }),
362
- /* @__PURE__ */ jsx(Passport, { user: userState.data })
363
- ] })
361
+ /* @__PURE__ */ jsxs(Box, { children: [
362
+ /* @__PURE__ */ jsx(Typography, { sx: { fontWeight: 600, mb: 1.5 }, children: isMyself ? t("myInfo") : t("hisInfo") }),
363
+ /* @__PURE__ */ jsx(UserInfo, { user: userState.data, isMySelf: isMyself })
364
+ ] }),
365
+ isMyself ? /* @__PURE__ */ jsxs(Box, { children: [
366
+ /* @__PURE__ */ jsx(Typography, { sx: { fontWeight: 600, mb: 1.5 }, children: t("passport") }),
367
+ /* @__PURE__ */ jsx(Passport, { user: userState.data })
364
368
  ] }) : null
365
369
  ]
366
370
  }
@@ -370,7 +374,14 @@ export default function UserCenter({
370
374
  ] });
371
375
  }, [userState, userCenterTabs, isMyself, currentActiveTab, privacyState, currentTab, stickySidebar]);
372
376
  if (!disableAutoRedirect && !currentTab && formattedBlocklet?.navigation?.userCenter?.length > 0) {
373
- window.location.replace(formattedBlocklet?.navigation?.userCenter[0]?.link);
377
+ const firstUserCenterUrl = formattedBlocklet?.navigation?.userCenter[0]?.link;
378
+ if (firstUserCenterUrl) {
379
+ window.location.replace(
380
+ withQuery(firstUserCenterUrl, {
381
+ did: isMyself ? void 0 : currentDid
382
+ })
383
+ );
384
+ }
374
385
  return null;
375
386
  }
376
387
  return /* @__PURE__ */ jsxs(
@@ -1,5 +1,6 @@
1
1
  import type { BoxProps } from '@mui/material';
2
2
  import type { User } from '../../../@types';
3
- export default function UserInfo({ user, ...rest }: {
3
+ export default function UserInfo({ user, isMySelf, ...rest }: {
4
4
  user: User;
5
+ isMySelf: boolean;
5
6
  } & BoxProps): import("react").JSX.Element;
@@ -15,6 +15,7 @@ import { translations } from "../../libs/locales.js";
15
15
  import UserInfoItem from "./user-info-item.js";
16
16
  export default function UserInfo({
17
17
  user,
18
+ isMySelf = false,
18
19
  ...rest
19
20
  }) {
20
21
  const { locale } = useLocaleContext();
@@ -25,31 +26,39 @@ export default function UserInfo({
25
26
  return user?.sourceProvider && LOGIN_PROVIDER_NAME[user.sourceProvider] || t("unknown");
26
27
  }, [user?.sourceProvider]);
27
28
  const userInfoListData = [];
28
- userInfoListData.push({
29
- icon: /* @__PURE__ */ jsx(Icon, { fontSize: 16, icon: MailOutlineRoundedIcon }),
30
- title: t("email"),
31
- content: user?.email || t("emptyField")
32
- });
33
- userInfoListData.push({
34
- icon: /* @__PURE__ */ jsx(Icon, { fontSize: 16, icon: ScheduleOutlineRoundedIcon }),
35
- title: t("lastLoginAt"),
36
- content: user?.lastLoginAt ? /* @__PURE__ */ jsx(RelativeTime, { locale, value: user?.lastLoginAt, relativeRange: 3 * 86400 * 1e3 }) : t("unknown")
37
- });
38
- userInfoListData.push({
39
- icon: /* @__PURE__ */ jsx(Icon, { fontSize: 16, icon: SettingsInputAntennaRoundedIcon }),
40
- title: t("lastLoginIp"),
41
- content: user?.lastLoginIp || t("unknown")
42
- });
29
+ if (isMySelf) {
30
+ userInfoListData.push({
31
+ icon: /* @__PURE__ */ jsx(Icon, { fontSize: 16, icon: MailOutlineRoundedIcon }),
32
+ title: t("email"),
33
+ content: user?.email || t("emptyField")
34
+ });
35
+ userInfoListData.push({
36
+ icon: /* @__PURE__ */ jsx(Icon, { fontSize: 16, icon: ScheduleOutlineRoundedIcon }),
37
+ title: t("lastLoginAt"),
38
+ content: user?.lastLoginAt ? (
39
+ // @ts-ignore
40
+ /* @__PURE__ */ jsx(RelativeTime, { locale, value: user?.lastLoginAt, relativeRange: 3 * 86400 * 1e3 })
41
+ ) : t("unknown")
42
+ });
43
+ userInfoListData.push({
44
+ icon: /* @__PURE__ */ jsx(Icon, { fontSize: 16, icon: SettingsInputAntennaRoundedIcon }),
45
+ title: t("lastLoginIp"),
46
+ content: user?.lastLoginIp || t("unknown")
47
+ });
48
+ }
43
49
  userInfoListData.push({
44
50
  icon: /* @__PURE__ */ jsx(Icon, { fontSize: 16, icon: MoreTimeRoundedIcon }),
45
51
  title: t("createdAt"),
52
+ // @ts-ignore
46
53
  content: user?.createdAt ? /* @__PURE__ */ jsx(RelativeTime, { locale, value: user?.createdAt }) : t("unknown")
47
54
  });
48
- userInfoListData.push({
49
- icon: /* @__PURE__ */ jsx(Icon, { fontSize: 16, icon: CaptivePortalRoundedIcon }),
50
- title: t("registerFrom"),
51
- content: readableProvider
52
- });
55
+ if (isMySelf) {
56
+ userInfoListData.push({
57
+ icon: /* @__PURE__ */ jsx(Icon, { fontSize: 16, icon: CaptivePortalRoundedIcon }),
58
+ title: t("registerFrom"),
59
+ content: readableProvider
60
+ });
61
+ }
53
62
  return /* @__PURE__ */ jsx(
54
63
  Box,
55
64
  {
@@ -42,6 +42,7 @@ export declare const translations: {
42
42
  switchProfile: string;
43
43
  userInfo: string;
44
44
  myInfo: string;
45
+ hisInfo: string;
45
46
  loginNow: string;
46
47
  viewAfterLogin: string;
47
48
  sessionManagement: string;
@@ -127,6 +128,7 @@ export declare const translations: {
127
128
  switchProfile: string;
128
129
  userInfo: string;
129
130
  myInfo: string;
131
+ hisInfo: string;
130
132
  loginNow: string;
131
133
  viewAfterLogin: string;
132
134
  sessionManagement: string;
@@ -8,7 +8,7 @@ export const translations = {
8
8
  lastLogin: "\u4E0A\u6B21\u767B\u5F55",
9
9
  lastLoginAt: "\u4E0A\u6B21\u767B\u5F55\u65F6\u95F4",
10
10
  lastLoginIp: "\u4E0A\u6B21\u767B\u5F55\u5730\u5740",
11
- createdAt: "\u521B\u5EFA\u65F6\u95F4",
11
+ createdAt: "\u52A0\u5165\u65F6\u95F4",
12
12
  registerFrom: "\u6CE8\u518C\u6765\u6E90",
13
13
  unknown: "\u672A\u77E5",
14
14
  walletNotification: "\u94B1\u5305\u901A\u77E5",
@@ -42,6 +42,7 @@ export const translations = {
42
42
  switchProfile: "\u5207\u6362",
43
43
  userInfo: "\u4E2A\u4EBA\u4FE1\u606F",
44
44
  myInfo: "\u6211\u7684\u4FE1\u606F",
45
+ hisInfo: "TA \u7684\u4FE1\u606F",
45
46
  loginNow: "\u7ACB\u5373\u767B\u5F55",
46
47
  viewAfterLogin: "\u767B\u5F55\u540E\u624D\u53EF\u4EE5\u67E5\u770B",
47
48
  sessionManagement: "\u4F1A\u8BDD\u7BA1\u7406",
@@ -93,7 +94,7 @@ export const translations = {
93
94
  lastLogin: "Last Login & IP",
94
95
  lastLoginAt: "Last Login",
95
96
  lastLoginIp: "Last IP",
96
- createdAt: "Created At",
97
+ createdAt: "Member Since",
97
98
  registerFrom: "Register From",
98
99
  unknown: "Unknown",
99
100
  walletNotification: "DID Wallet notification",
@@ -127,6 +128,7 @@ export const translations = {
127
128
  switchProfile: "Switch",
128
129
  userInfo: "User Info",
129
130
  myInfo: "My Info",
131
+ hisInfo: "His/Her Info",
130
132
  loginNow: "Login",
131
133
  viewAfterLogin: "View after login",
132
134
  sessionManagement: "Session Management",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/ui-react",
3
- "version": "2.10.2",
3
+ "version": "2.10.4",
4
4
  "description": "Some useful front-end web components that can be used in Blocklets.",
5
5
  "keywords": [
6
6
  "react",
@@ -32,8 +32,8 @@
32
32
  "url": "https://github.com/ArcBlock/ux/issues"
33
33
  },
34
34
  "dependencies": {
35
- "@arcblock/bridge": "^2.10.2",
36
- "@arcblock/react-hooks": "^2.10.2",
35
+ "@arcblock/bridge": "^2.10.4",
36
+ "@arcblock/react-hooks": "^2.10.4",
37
37
  "@iconify-icons/logos": "^1.2.36",
38
38
  "@iconify-icons/material-symbols": "^1.2.58",
39
39
  "@iconify/react": "^4.1.1",
@@ -78,5 +78,5 @@
78
78
  "jest": "^28.1.3",
79
79
  "unbuild": "^2.0.0"
80
80
  },
81
- "gitHead": "01c29cd905a2baf029d7be25d41c79a261ebd959"
81
+ "gitHead": "57248b30b2ad697026249b768894c44345c37ed3"
82
82
  }
@@ -112,3 +112,40 @@ export type CreatePassportProps = {
112
112
  width?: string;
113
113
  height?: string;
114
114
  };
115
+
116
+ export type BlockletMetaProps = {
117
+ appLogo?: React.ReactNode;
118
+ appName?: string;
119
+ theme?: {
120
+ background?: string;
121
+ };
122
+ enableConnect?: boolean;
123
+ enableLocale?: boolean;
124
+ navigation?: Array<{
125
+ title?: string | object;
126
+ link?: string | object;
127
+ icon?: string;
128
+ items?: Array<{
129
+ title?: string | object;
130
+ link?: string | object;
131
+ }>;
132
+ }>;
133
+ };
134
+
135
+ export type SessionManagerProps = {
136
+ showText?: boolean;
137
+ showRole?: boolean;
138
+ switchDid?: boolean;
139
+ switchProfile?: boolean;
140
+ switchPassport?: boolean;
141
+ disableLogout?: boolean;
142
+ onLogin?: () => void;
143
+ onLogout?: () => void;
144
+ onSwitchDid?: () => void;
145
+ onSwitchProfile?: () => void;
146
+ onSwitchPassport?: () => void;
147
+ menu?: any[];
148
+ menuRender?: () => void;
149
+ dark?: boolean;
150
+ size?: number;
151
+ };
@@ -1,5 +1,4 @@
1
1
  import { useMemo } from 'react';
2
- import PropTypes from 'prop-types';
3
2
  import { withErrorBoundary } from 'react-error-boundary';
4
3
  import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
5
4
  import { styled } from '@arcblock/ux/lib/Theme';
@@ -11,19 +10,19 @@ import omit from 'lodash/omit';
11
10
 
12
11
  import Icon from '../Icon';
13
12
  import OverridableThemeProvider from '../common/overridable-theme-provider';
14
- import { BlockletMetaProps, SessionManagerProps } from '../types';
15
13
  import { mapRecursive, flatRecursive, matchPaths } from '../utils';
16
14
  import { publicPath, formatBlockletInfo, getLocalizedNavigation } from '../blocklets';
17
15
  import HeaderAddons from '../common/header-addons';
18
16
  import { useWalletHiddenTopbar } from '../common/wallet-hidden-topbar';
17
+ import { BlockletMetaProps, SessionManagerProps } from '../@types';
19
18
 
20
19
  // blocklet meta 中的 navigation 数据 => NavMenu 组件的 items
21
- const parseNavigation = (navigation) => {
20
+ const parseNavigation = (navigation: any) => {
22
21
  if (!navigation?.length) {
23
22
  return { navItems: [], activeId: null };
24
23
  }
25
24
  let counter = 1;
26
- const parseItem = (item) => {
25
+ const parseItem = (item: any) => {
27
26
  const icon = item.icon ? <Icon icon={item.icon} /> : null;
28
27
  if (item.items) {
29
28
  return {
@@ -62,7 +61,25 @@ const parseNavigation = (navigation) => {
62
61
  */
63
62
  // eslint-disable-next-line no-shadow
64
63
 
65
- function Header({ meta, addons, sessionManagerProps, homeLink, theme: themeOverrides, hideNavMenu, ...rest }) {
64
+ function Header({
65
+ meta = {},
66
+ addons,
67
+ sessionManagerProps = {
68
+ showRole: true,
69
+ },
70
+ homeLink = publicPath,
71
+ theme: themeOverrides,
72
+ hideNavMenu = false,
73
+ ...rest
74
+ }: {
75
+ meta: BlockletMetaProps;
76
+ addons: Function | JSX.Element;
77
+ sessionManagerProps: SessionManagerProps;
78
+ homeLink: string;
79
+ theme: object;
80
+ hideNavMenu: boolean;
81
+ bordered?: boolean;
82
+ }) {
66
83
  useWalletHiddenTopbar();
67
84
  const { locale } = useLocaleContext() || {};
68
85
  const formattedBlocklet = useMemo(() => {
@@ -85,8 +102,11 @@ function Header({ meta, addons, sessionManagerProps, homeLink, theme: themeOverr
85
102
 
86
103
  // eslint-disable-next-line @typescript-eslint/naming-convention
87
104
  const _addons =
88
- typeof addons === 'function' ? (builtInAddons) => addons(builtInAddons, { navigation: parsedNavigation }) : addons;
105
+ typeof addons === 'function'
106
+ ? (builtInAddons: any) => addons(builtInAddons, { navigation: parsedNavigation })
107
+ : addons;
89
108
  const headerAddons = (
109
+ // @ts-ignore
90
110
  <HeaderAddons formattedBlocklet={formattedBlocklet} addons={_addons} sessionManagerProps={sessionManagerProps} />
91
111
  );
92
112
 
@@ -102,7 +122,8 @@ function Header({ meta, addons, sessionManagerProps, homeLink, theme: themeOverr
102
122
  {/* blocklet.yml 没有配置 navigation 时, 则为 children 传入 null, 此时 ResponsiveHeader 会渲染普通的不带 menu 的 Header */}
103
123
  {hideNavMenu || !navItems?.length
104
124
  ? null
105
- : ({ isMobile }) => (
125
+ : ({ isMobile }: { isMobile: boolean }) => (
126
+ // @ts-ignore
106
127
  <NavMenu
107
128
  mode={isMobile ? 'inline' : 'horizontal'}
108
129
  activeId={activeId}
@@ -117,32 +138,6 @@ function Header({ meta, addons, sessionManagerProps, homeLink, theme: themeOverr
117
138
  );
118
139
  }
119
140
 
120
- Header.propTypes = {
121
- meta: BlockletMetaProps,
122
-
123
- // 需要考虑 定制的 addons 与内置的 连接钱包/选择语言 addons 共存的情况
124
- // - PropTypes.func: 可以把自定义 addons 插在 session-manager 或 locale-selector (如果存在的话) 前/中/后
125
- // - PropTypes.node: 将 addons 原样传给 UX Header 组件
126
- addons: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
127
-
128
- sessionManagerProps: SessionManagerProps,
129
- homeLink: PropTypes.string,
130
- // 允许覆盖 header 内置的 theme
131
- theme: PropTypes.object,
132
- hideNavMenu: PropTypes.bool,
133
- };
134
-
135
- Header.defaultProps = {
136
- meta: {},
137
- addons: null,
138
- sessionManagerProps: {
139
- showRole: true,
140
- },
141
- homeLink: publicPath,
142
- theme: null,
143
- hideNavMenu: false,
144
- };
145
-
146
141
  const StyledUxHeader = styled(ResponsiveHeader)`
147
142
  ${({ $bgcolor }) => `background-color: ${$bgcolor || '#fff'};`}
148
143
  font-family: Inter, Avenir, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,
@@ -1,13 +1,23 @@
1
1
  import 'iconify-icon';
2
2
 
3
- import PropTypes from 'prop-types';
4
- import Avatar from '@mui/material/Avatar';
3
+ import { Avatar } from '@mui/material';
4
+ import type { AvatarProps, BoxProps } from '@mui/material';
5
+ import { Icon as IconifyIcon } from '@iconify/react';
5
6
  import { isUrl, isIconifyString } from '../utils';
6
7
 
7
8
  /**
8
9
  * Icon 组件, 基于 mui Avatar 组件扩展对 iconify 的支持
9
10
  */
10
- export default function Icon({ icon, size, sx, ...rest }) {
11
+ export default function Icon({
12
+ icon,
13
+ size,
14
+ sx,
15
+ ...rest
16
+ }: {
17
+ icon: string;
18
+ size?: number;
19
+ sx?: BoxProps['sx'];
20
+ } & AvatarProps) {
11
21
  // eslint-disable-next-line @typescript-eslint/naming-convention
12
22
  const _sx = [...(Array.isArray(sx) ? sx : [sx])];
13
23
  if (size) {
@@ -30,14 +40,14 @@ export default function Icon({ icon, size, sx, ...rest }) {
30
40
  });
31
41
  }
32
42
  if (isUrl(icon)) {
33
- return <Avatar as="span" {...rest} src={icon} sx={_sx} />;
43
+ return <Avatar component="span" {...rest} src={icon} sx={_sx} />;
34
44
  }
35
45
  if (isIconifyString(icon)) {
36
46
  // y = 0.6 * x + 4
37
47
  const height = size ? 0.6 * size + 4 : 0;
38
48
  return (
39
- <Avatar as="span" {...rest} sx={_sx}>
40
- <iconify-icon icon={icon} height={height || undefined} />
49
+ <Avatar component="span" {...rest} sx={_sx}>
50
+ <IconifyIcon icon={icon} height={height || undefined} />
41
51
  </Avatar>
42
52
  );
43
53
  }
@@ -50,24 +60,10 @@ export default function Icon({ icon, size, sx, ...rest }) {
50
60
  },
51
61
  });
52
62
  return (
53
- <Avatar as="span" {...rest} sx={_sx}>
63
+ <Avatar component="span" {...rest} sx={_sx}>
54
64
  {Array.from(icon)[0]}
55
65
  </Avatar>
56
66
  );
57
67
  }
58
68
  return null;
59
69
  }
60
-
61
- Icon.propTypes = {
62
- // icon 支持 2 种形式:
63
- // 1. iconify icon name: <prefix>:<name>
64
- // 2. url
65
- icon: PropTypes.string.isRequired,
66
- size: PropTypes.number,
67
- sx: PropTypes.oneOfType([PropTypes.array, PropTypes.func, PropTypes.object]),
68
- };
69
-
70
- Icon.defaultProps = {
71
- size: null,
72
- sx: null,
73
- };
@@ -14,8 +14,8 @@ const languages = [
14
14
  { label: '中文', value: 'zh' },
15
15
  ];
16
16
 
17
- export default function ConfigProfile({ user }: { user: User }) {
18
- const { locale } = useLocaleContext();
17
+ export default function ConfigProfile({ user, onSave }: { user: User; onSave: (type: 'profile') => void }) {
18
+ const { locale, changeLocale } = useLocaleContext();
19
19
  const t = useMemoizedFn((key, data = {}) => {
20
20
  return translate(translations, key, locale, 'en', data);
21
21
  });
@@ -34,6 +34,8 @@ export default function ConfigProfile({ user }: { user: User }) {
34
34
  }),
35
35
  sleep(350),
36
36
  ]);
37
+ await onSave('profile');
38
+ changeLocale(value);
37
39
  currentState.locale = value;
38
40
  } finally {
39
41
  currentState.loading = false;
@@ -17,7 +17,13 @@ type PrivacyConfig = {
17
17
  value: boolean;
18
18
  };
19
19
 
20
- export default function Privacy({ configList, onSave }: { configList: PrivacyConfig[]; onSave: () => void }) {
20
+ export default function Privacy({
21
+ configList,
22
+ onSave,
23
+ }: {
24
+ configList: PrivacyConfig[];
25
+ onSave: (type: 'privacy') => void;
26
+ }) {
21
27
  const [dataList, setDataList] = useState(configList);
22
28
  const { locale } = useLocaleContext();
23
29
  const t = useMemoizedFn((key, data = {}) => {
@@ -38,7 +44,7 @@ export default function Privacy({ configList, onSave }: { configList: PrivacyCon
38
44
  })
39
45
  );
40
46
  Toast.success(t('saveSuccess'));
41
- onSave();
47
+ onSave('privacy');
42
48
  } catch (err) {
43
49
  Toast.error(formatAxiosError(err as AxiosError));
44
50
  }
@@ -79,29 +85,31 @@ export default function Privacy({ configList, onSave }: { configList: PrivacyCon
79
85
  },
80
86
  },
81
87
  }}>
82
- {dataList.map((item) => (
83
- <Switch
84
- key={item.key}
85
- checked={!item.value}
86
- labelProps={{
87
- label: (
88
- <Typography
89
- color="text.primary"
90
- sx={{
91
- fontSize: 14,
92
- display: 'flex',
93
- flexFlow: 'wrap',
94
- columnGap: 1,
95
- flex: 1,
96
- }}>
97
- {t('toPublic', { name: item.name })}
98
- </Typography>
99
- ),
100
- }}
101
- size="small"
102
- onChange={(event: ChangeEvent<HTMLInputElement>) => handleChangeSwitch(item.key, event.target.checked)}
103
- />
104
- ))}
88
+ {dataList.map((item) => {
89
+ return (
90
+ <Switch
91
+ key={item.key}
92
+ checked={!item.value}
93
+ labelProps={{
94
+ label: (
95
+ <Typography
96
+ color="text.primary"
97
+ sx={{
98
+ fontSize: 14,
99
+ display: 'flex',
100
+ flexFlow: 'wrap',
101
+ columnGap: 1,
102
+ flex: 1,
103
+ }}>
104
+ {t('toPublic', { name: item.name })}
105
+ </Typography>
106
+ ),
107
+ }}
108
+ size="small"
109
+ onChange={(event: ChangeEvent<HTMLInputElement>) => handleChangeSwitch(item.key, event.target.checked)}
110
+ />
111
+ );
112
+ })}
105
113
  </Box>
106
114
  );
107
115
  }
@@ -22,7 +22,7 @@ export default function Settings({
22
22
  ...rest
23
23
  }: {
24
24
  user: User;
25
- onSave: () => void;
25
+ onSave: (type: 'privacy' | 'profile') => void;
26
26
  settings: {
27
27
  userCenterTabs: UserCenterTab[];
28
28
  };
@@ -46,7 +46,7 @@ export default function Settings({
46
46
  {
47
47
  label: t('commonSetting.title'),
48
48
  value: 'common',
49
- content: <ConfigProfile user={user} />,
49
+ content: <ConfigProfile user={user} onSave={onSave} />,
50
50
  },
51
51
  {
52
52
  label: t('notificationManagement'),
@@ -180,13 +180,19 @@ export default function UserCenter({
180
180
  <Settings
181
181
  user={userState.data as User}
182
182
  settings={{ userCenterTabs }}
183
- onSave={async () => {
184
- await privacyState.runAsync();
185
- return privacyState.data;
183
+ onSave={async (type: 'privacy' | 'profile') => {
184
+ if (type === 'privacy') {
185
+ await privacyState.runAsync();
186
+ return privacyState.data;
187
+ }
188
+ if (type === 'profile') {
189
+ await session.refresh();
190
+ }
191
+ return null;
186
192
  }}
187
193
  />
188
194
  );
189
- }, [userState.data]);
195
+ }, [userState.data, userCenterTabs, privacyState.data, privacyState.runAsync]);
190
196
 
191
197
  const openSettings = useMemoizedFn(() => {
192
198
  confirmApi.open({
@@ -383,17 +389,15 @@ export default function UserCenter({
383
389
  }}
384
390
  />
385
391
 
392
+ <Box>
393
+ <Typography sx={{ fontWeight: 600, mb: 1.5 }}>{isMyself ? t('myInfo') : t('hisInfo')}</Typography>
394
+ <UserInfo user={userState.data as User} isMySelf={isMyself} />
395
+ </Box>
386
396
  {isMyself ? (
387
- <>
388
- <Box>
389
- <Typography sx={{ fontWeight: 600, mb: 1.5 }}>{t('myInfo')}</Typography>
390
- <UserInfo user={userState.data as User} />
391
- </Box>
392
- <Box>
393
- <Typography sx={{ fontWeight: 600, mb: 1.5 }}>{t('passport')}</Typography>
394
- <Passport user={userState.data as User} />
395
- </Box>
396
- </>
397
+ <Box>
398
+ <Typography sx={{ fontWeight: 600, mb: 1.5 }}>{t('passport')}</Typography>
399
+ <Passport user={userState.data as User} />
400
+ </Box>
397
401
  ) : null}
398
402
  </Box>
399
403
  </Box>
@@ -402,7 +406,14 @@ export default function UserCenter({
402
406
  }, [userState, userCenterTabs, isMyself, currentActiveTab, privacyState, currentTab, stickySidebar]);
403
407
 
404
408
  if (!disableAutoRedirect && !currentTab && formattedBlocklet?.navigation?.userCenter?.length > 0) {
405
- window.location.replace(formattedBlocklet?.navigation?.userCenter[0]?.link);
409
+ const firstUserCenterUrl = formattedBlocklet?.navigation?.userCenter[0]?.link;
410
+ if (firstUserCenterUrl) {
411
+ window.location.replace(
412
+ withQuery(firstUserCenterUrl, {
413
+ did: isMyself ? undefined : currentDid,
414
+ })
415
+ );
416
+ }
406
417
  return null;
407
418
  }
408
419
 
@@ -18,9 +18,11 @@ import type { User } from '../../../@types';
18
18
 
19
19
  export default function UserInfo({
20
20
  user,
21
+ isMySelf = false,
21
22
  ...rest
22
23
  }: {
23
24
  user: User;
25
+ isMySelf: boolean;
24
26
  } & BoxProps) {
25
27
  const { locale } = useLocaleContext();
26
28
  const t = useMemoizedFn((key, data = {}) => {
@@ -32,35 +34,41 @@ export default function UserInfo({
32
34
  }, [user?.sourceProvider]);
33
35
 
34
36
  const userInfoListData = [];
35
- userInfoListData.push({
36
- icon: <Icon fontSize={16} icon={MailOutlineRoundedIcon} />,
37
- title: t('email'),
38
- content: user?.email || t('emptyField'),
39
- });
40
- userInfoListData.push({
41
- icon: <Icon fontSize={16} icon={ScheduleOutlineRoundedIcon} />,
42
- title: t('lastLoginAt'),
43
- content: user?.lastLoginAt ? (
44
- <RelativeTime locale={locale} value={user?.lastLoginAt} relativeRange={3 * 86400 * 1000} />
45
- ) : (
46
- t('unknown')
47
- ),
48
- });
49
- userInfoListData.push({
50
- icon: <Icon fontSize={16} icon={SettingsInputAntennaRoundedIcon} />,
51
- title: t('lastLoginIp'),
52
- content: user?.lastLoginIp || t('unknown'),
53
- });
37
+ if (isMySelf) {
38
+ userInfoListData.push({
39
+ icon: <Icon fontSize={16} icon={MailOutlineRoundedIcon} />,
40
+ title: t('email'),
41
+ content: user?.email || t('emptyField'),
42
+ });
43
+ userInfoListData.push({
44
+ icon: <Icon fontSize={16} icon={ScheduleOutlineRoundedIcon} />,
45
+ title: t('lastLoginAt'),
46
+ content: user?.lastLoginAt ? (
47
+ // @ts-ignore
48
+ <RelativeTime locale={locale} value={user?.lastLoginAt} relativeRange={3 * 86400 * 1000} />
49
+ ) : (
50
+ t('unknown')
51
+ ),
52
+ });
53
+ userInfoListData.push({
54
+ icon: <Icon fontSize={16} icon={SettingsInputAntennaRoundedIcon} />,
55
+ title: t('lastLoginIp'),
56
+ content: user?.lastLoginIp || t('unknown'),
57
+ });
58
+ }
54
59
  userInfoListData.push({
55
60
  icon: <Icon fontSize={16} icon={MoreTimeRoundedIcon} />,
56
61
  title: t('createdAt'),
62
+ // @ts-ignore
57
63
  content: user?.createdAt ? <RelativeTime locale={locale} value={user?.createdAt} /> : t('unknown'),
58
64
  });
59
- userInfoListData.push({
60
- icon: <Icon fontSize={16} icon={CaptivePortalRoundedIcon} />,
61
- title: t('registerFrom'),
62
- content: readableProvider,
63
- });
65
+ if (isMySelf) {
66
+ userInfoListData.push({
67
+ icon: <Icon fontSize={16} icon={CaptivePortalRoundedIcon} />,
68
+ title: t('registerFrom'),
69
+ content: readableProvider,
70
+ });
71
+ }
64
72
 
65
73
  return (
66
74
  <Box
@@ -8,7 +8,7 @@ export const translations = {
8
8
  lastLogin: '上次登录',
9
9
  lastLoginAt: '上次登录时间',
10
10
  lastLoginIp: '上次登录地址',
11
- createdAt: '创建时间',
11
+ createdAt: '加入时间',
12
12
  registerFrom: '注册来源',
13
13
  unknown: '未知',
14
14
  walletNotification: '钱包通知',
@@ -42,6 +42,7 @@ export const translations = {
42
42
  switchProfile: '切换',
43
43
  userInfo: '个人信息',
44
44
  myInfo: '我的信息',
45
+ hisInfo: 'TA 的信息',
45
46
  loginNow: '立即登录',
46
47
  viewAfterLogin: '登录后才可以查看',
47
48
  sessionManagement: '会话管理',
@@ -94,7 +95,7 @@ export const translations = {
94
95
  lastLogin: 'Last Login & IP',
95
96
  lastLoginAt: 'Last Login',
96
97
  lastLoginIp: 'Last IP',
97
- createdAt: 'Created At',
98
+ createdAt: 'Member Since',
98
99
  registerFrom: 'Register From',
99
100
  unknown: 'Unknown',
100
101
  walletNotification: 'DID Wallet notification',
@@ -128,6 +129,7 @@ export const translations = {
128
129
  switchProfile: 'Switch',
129
130
  userInfo: 'User Info',
130
131
  myInfo: 'My Info',
132
+ hisInfo: 'His/Her Info',
131
133
  loginNow: 'Login',
132
134
  viewAfterLogin: 'View after login',
133
135
  sessionManagement: 'Session Management',