@blocklet/ui-react 2.4.23 → 2.4.26
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/Dashboard/index.js +100 -25
- package/lib/Header/index.js +14 -45
- package/lib/blocklets.js +58 -5
- package/lib/common/header-addons.js +128 -0
- package/lib/types.js +2 -2
- package/lib/utils.js +24 -2
- package/package.json +4 -4
- package/src/Dashboard/index.js +81 -23
- package/src/Header/index.js +8 -39
- package/src/blocklets.js +53 -3
- package/src/common/header-addons.js +88 -0
- package/src/types.js +2 -2
- package/src/utils.js +16 -0
package/lib/Dashboard/index.js
CHANGED
|
@@ -7,6 +7,10 @@ exports.default = void 0;
|
|
|
7
7
|
|
|
8
8
|
var _react = require("react");
|
|
9
9
|
|
|
10
|
+
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
11
|
+
|
|
12
|
+
var _Session = require("@arcblock/did-connect/lib/Session");
|
|
13
|
+
|
|
10
14
|
var _context = require("@arcblock/ux/lib/Locale/context");
|
|
11
15
|
|
|
12
16
|
var _dashboard = _interopRequireDefault(require("@arcblock/ux/lib/Layout/dashboard"));
|
|
@@ -21,9 +25,11 @@ var _utils = require("../utils");
|
|
|
21
25
|
|
|
22
26
|
var _blocklets = require("../blocklets");
|
|
23
27
|
|
|
28
|
+
var _headerAddons2 = _interopRequireDefault(require("../common/header-addons"));
|
|
29
|
+
|
|
24
30
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
25
31
|
|
|
26
|
-
const _excluded = ["meta"];
|
|
32
|
+
const _excluded = ["meta", "fallbackUrl", "invalidPathFallback", "headerAddons", "sessionManagerProps"];
|
|
27
33
|
|
|
28
34
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
29
35
|
|
|
@@ -40,14 +46,22 @@ function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) r
|
|
|
40
46
|
/**
|
|
41
47
|
* 专门用于 (composable) blocklet 的 Dashboard 组件, 解析 blocklet meta 中 section 为 dashboard 的 navigation 数据, 渲染一个 UX Dashboard
|
|
42
48
|
*/
|
|
49
|
+
// eslint-disable-next-line no-shadow
|
|
43
50
|
function Dashboard(_ref) {
|
|
44
|
-
var
|
|
51
|
+
var _sessionCtx$session;
|
|
45
52
|
|
|
46
53
|
let {
|
|
47
|
-
meta
|
|
54
|
+
meta,
|
|
55
|
+
fallbackUrl,
|
|
56
|
+
invalidPathFallback,
|
|
57
|
+
headerAddons,
|
|
58
|
+
sessionManagerProps
|
|
48
59
|
} = _ref,
|
|
49
60
|
rest = _objectWithoutProperties(_ref, _excluded);
|
|
50
61
|
|
|
62
|
+
const sessionCtx = (0, _react.useContext)(_Session.SessionContext);
|
|
63
|
+
const user = sessionCtx === null || sessionCtx === void 0 ? void 0 : (_sessionCtx$session = sessionCtx.session) === null || _sessionCtx$session === void 0 ? void 0 : _sessionCtx$session.user;
|
|
64
|
+
const userRole = user === null || user === void 0 ? void 0 : user.role;
|
|
51
65
|
const {
|
|
52
66
|
locale
|
|
53
67
|
} = (0, _context.useLocaleContext)() || {};
|
|
@@ -60,6 +74,63 @@ function Dashboard(_ref) {
|
|
|
60
74
|
return blocklet;
|
|
61
75
|
}
|
|
62
76
|
}, [blocklet]);
|
|
77
|
+
const {
|
|
78
|
+
localizedNav,
|
|
79
|
+
flattened,
|
|
80
|
+
matchedIndex
|
|
81
|
+
} = (0, _react.useMemo)(() => {
|
|
82
|
+
var _formattedBlocklet$na;
|
|
83
|
+
|
|
84
|
+
let localizedNav = (0, _blocklets.getLocalizedNavigation)(formattedBlocklet === null || formattedBlocklet === void 0 ? void 0 : (_formattedBlocklet$na = formattedBlocklet.navigation) === null || _formattedBlocklet$na === void 0 ? void 0 : _formattedBlocklet$na.dashboard, locale) || []; // 根据 role 筛选 nav 数据
|
|
85
|
+
|
|
86
|
+
localizedNav = (0, _blocklets.filterNavByRole)(localizedNav, userRole); // 将 nav 数据处理成 ux dashboard 需要的格式
|
|
87
|
+
|
|
88
|
+
localizedNav = (0, _utils.mapRecursive)(localizedNav, item => ({
|
|
89
|
+
title: item.title,
|
|
90
|
+
url: item.link,
|
|
91
|
+
icon: item.icon ? /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
|
|
92
|
+
className: "iconify",
|
|
93
|
+
"data-icon": item.icon
|
|
94
|
+
}) : null,
|
|
95
|
+
// https://github.com/ArcBlock/ux/issues/755#issuecomment-1208692620
|
|
96
|
+
external: true,
|
|
97
|
+
children: item.items
|
|
98
|
+
}), 'items'); // 展平后使用 matchPaths 检测 link#active 状态
|
|
99
|
+
|
|
100
|
+
const flattened = (0, _utils.flatRecursive)(localizedNav).filter(item => !!item.url);
|
|
101
|
+
const matchedIndex = (0, _utils.matchPaths)(flattened.map(item => item.url));
|
|
102
|
+
|
|
103
|
+
if (flattened !== null && flattened !== void 0 && flattened.length) {
|
|
104
|
+
flattened[matchedIndex === -1 ? 0 : matchedIndex].active = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
localizedNav,
|
|
109
|
+
flattened,
|
|
110
|
+
matchedIndex
|
|
111
|
+
};
|
|
112
|
+
}, [formattedBlocklet, locale, userRole]); // 页面初始化时, 如果当前用户没有权限访问任何导航菜单 (比如登录时未提供 VC 导致无权限), 则跳转到 fallbackUrl
|
|
113
|
+
// 未认证 (user 为空) 时不做处理, 这种情况的页面跳转逻辑一般由应用自行处理
|
|
114
|
+
|
|
115
|
+
(0, _react.useLayoutEffect)(() => {
|
|
116
|
+
if (!!user && !(flattened !== null && flattened !== void 0 && flattened.length) && fallbackUrl) {
|
|
117
|
+
window.location.href = fallbackUrl;
|
|
118
|
+
} // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
|
+
|
|
120
|
+
}, [fallbackUrl]); // 导航菜单变动且存在可用菜单但无匹配项时 (如切换 passport), 跳转到首个菜单项
|
|
121
|
+
|
|
122
|
+
(0, _react.useLayoutEffect)(() => {
|
|
123
|
+
if (!!user && !!(flattened !== null && flattened !== void 0 && flattened.length) && matchedIndex === -1) {
|
|
124
|
+
if (invalidPathFallback) {
|
|
125
|
+
invalidPathFallback();
|
|
126
|
+
} else {
|
|
127
|
+
var _flattened$;
|
|
128
|
+
|
|
129
|
+
window.location.href = ((_flattened$ = flattened[0]) === null || _flattened$ === void 0 ? void 0 : _flattened$.url) || _blocklets.publicPath;
|
|
130
|
+
}
|
|
131
|
+
} // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
|
+
|
|
133
|
+
}, [invalidPathFallback]);
|
|
63
134
|
|
|
64
135
|
if (!blocklet.appName) {
|
|
65
136
|
return null;
|
|
@@ -70,25 +141,12 @@ function Dashboard(_ref) {
|
|
|
70
141
|
appLogo,
|
|
71
142
|
appName
|
|
72
143
|
} = formattedBlocklet;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"data-icon": item.icon
|
|
80
|
-
}) : null,
|
|
81
|
-
// https://github.com/ArcBlock/ux/issues/755#issuecomment-1208692620
|
|
82
|
-
external: true,
|
|
83
|
-
children: item.items
|
|
84
|
-
}), 'items'); // 展平后使用 matchPaths 检测 link#active 状态
|
|
85
|
-
|
|
86
|
-
const flattened = (0, _utils.flatRecursive)(localizedNav).filter(item => !!item.url);
|
|
87
|
-
const matchedIndex = (0, _utils.matchPaths)(flattened.map(item => item.url));
|
|
88
|
-
|
|
89
|
-
if (flattened !== null && flattened !== void 0 && flattened.length) {
|
|
90
|
-
flattened[matchedIndex === -1 ? 0 : matchedIndex].active = true;
|
|
91
|
-
}
|
|
144
|
+
|
|
145
|
+
const _headerAddons = /*#__PURE__*/(0, _jsxRuntime.jsx)(_headerAddons2.default, {
|
|
146
|
+
formattedBlocklet: formattedBlocklet,
|
|
147
|
+
addons: headerAddons,
|
|
148
|
+
sessionManagerProps: sessionManagerProps
|
|
149
|
+
});
|
|
92
150
|
|
|
93
151
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_dashboard.default, _objectSpread(_objectSpread({
|
|
94
152
|
title: appName,
|
|
@@ -118,17 +176,34 @@ function Dashboard(_ref) {
|
|
|
118
176
|
src: appLogo,
|
|
119
177
|
alt: "logo"
|
|
120
178
|
})
|
|
121
|
-
})
|
|
179
|
+
}),
|
|
180
|
+
addons: _headerAddons
|
|
122
181
|
}, rest.headerProps),
|
|
123
182
|
links: localizedNav
|
|
124
183
|
}));
|
|
125
184
|
}
|
|
126
185
|
|
|
127
186
|
Dashboard.propTypes = {
|
|
128
|
-
meta: _types.blockletMetaProps
|
|
187
|
+
meta: _types.blockletMetaProps,
|
|
188
|
+
// 如果当前用户没有权限访问任何导航菜单, 则自动跳转到 fallbackUrl, 默认值为 publicPath, 设置为 null 表示禁用自动跳转
|
|
189
|
+
fallbackUrl: _propTypes.default.string,
|
|
190
|
+
// 当前路径未匹配任何 nav links 时的 fallback, 默认行为跳转到首个可用的 nav link
|
|
191
|
+
invalidPathFallback: _propTypes.default.func,
|
|
192
|
+
headerAddons: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.node]),
|
|
193
|
+
sessionManagerProps: _types.sessionManagerProps
|
|
129
194
|
};
|
|
130
195
|
Dashboard.defaultProps = {
|
|
131
|
-
meta: {}
|
|
196
|
+
meta: {},
|
|
197
|
+
fallbackUrl: _blocklets.publicPath,
|
|
198
|
+
invalidPathFallback: null,
|
|
199
|
+
headerAddons: undefined,
|
|
200
|
+
sessionManagerProps: {
|
|
201
|
+
showRole: true,
|
|
202
|
+
// dashboard 默认退出登录行为: 跳转到 (root) blocklet 首页
|
|
203
|
+
onLogout: () => {
|
|
204
|
+
window.location.href = _blocklets.publicPath;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
132
207
|
};
|
|
133
208
|
var _default = Dashboard;
|
|
134
209
|
exports.default = _default;
|
package/lib/Header/index.js
CHANGED
|
@@ -7,8 +7,6 @@ exports.default = void 0;
|
|
|
7
7
|
|
|
8
8
|
var _react = require("react");
|
|
9
9
|
|
|
10
|
-
var _jsxRuntime = require("react/jsx-runtime");
|
|
11
|
-
|
|
12
10
|
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
13
11
|
|
|
14
12
|
var _reactErrorBoundary = require("react-error-boundary");
|
|
@@ -21,12 +19,6 @@ var _Header = require("@arcblock/ux/lib/Header");
|
|
|
21
19
|
|
|
22
20
|
var _NavMenu = _interopRequireDefault(require("@arcblock/ux/lib/NavMenu"));
|
|
23
21
|
|
|
24
|
-
var _Session = require("@arcblock/did-connect/lib/Session");
|
|
25
|
-
|
|
26
|
-
var _SessionManager = _interopRequireDefault(require("@arcblock/did-connect/lib/SessionManager"));
|
|
27
|
-
|
|
28
|
-
var _selector = _interopRequireDefault(require("@arcblock/ux/lib/Locale/selector"));
|
|
29
|
-
|
|
30
22
|
var _context = require("@arcblock/ux/lib/Locale/context");
|
|
31
23
|
|
|
32
24
|
var _Address = _interopRequireDefault(require("@arcblock/did-connect/lib/Address"));
|
|
@@ -43,6 +35,10 @@ var _utils = require("../utils");
|
|
|
43
35
|
|
|
44
36
|
var _blocklets = require("../blocklets");
|
|
45
37
|
|
|
38
|
+
var _headerAddons = _interopRequireDefault(require("../common/header-addons"));
|
|
39
|
+
|
|
40
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
41
|
+
|
|
46
42
|
var _templateObject;
|
|
47
43
|
|
|
48
44
|
const _excluded = ["meta", "addons", "sessionManagerProps", "homeLink", "theme"];
|
|
@@ -135,7 +131,6 @@ function Header(_ref) {
|
|
|
135
131
|
} = _ref,
|
|
136
132
|
rest = _objectWithoutProperties(_ref, _excluded);
|
|
137
133
|
|
|
138
|
-
const sessionInfo = (0, _react.useContext)(_Session.SessionContext);
|
|
139
134
|
const {
|
|
140
135
|
locale
|
|
141
136
|
} = (0, _context.useLocaleContext)() || {};
|
|
@@ -157,9 +152,7 @@ function Header(_ref) {
|
|
|
157
152
|
did,
|
|
158
153
|
appLogo,
|
|
159
154
|
appName,
|
|
160
|
-
theme
|
|
161
|
-
enableConnect = true,
|
|
162
|
-
enableLocale = true
|
|
155
|
+
theme
|
|
163
156
|
} = formattedBlocklet;
|
|
164
157
|
const navigation = (0, _blocklets.getLocalizedNavigation)(formattedBlocklet === null || formattedBlocklet === void 0 ? void 0 : (_formattedBlocklet$na = formattedBlocklet.navigation) === null || _formattedBlocklet$na === void 0 ? void 0 : _formattedBlocklet$na.header, locale);
|
|
165
158
|
const parsedNavigation = parseNavigation(navigation);
|
|
@@ -168,39 +161,15 @@ function Header(_ref) {
|
|
|
168
161
|
activeId
|
|
169
162
|
} = parsedNavigation;
|
|
170
163
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return Array.isArray(addons) ? addons : [addons];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
let addonsArray = []; // 启用了多语言并且检测到了 locale context
|
|
164
|
+
const _addons = typeof addons === 'function' ? builtInAddons => addons(builtInAddons, {
|
|
165
|
+
navigation: parsedNavigation
|
|
166
|
+
}) : addons;
|
|
178
167
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (enableConnect && sessionInfo) {
|
|
187
|
-
addonsArray.push( /*#__PURE__*/(0, _jsxRuntime.jsx)(_SessionManager.default, _objectSpread({
|
|
188
|
-
session: sessionInfo.session
|
|
189
|
-
}, sessionManagerProps), "session-manager"));
|
|
190
|
-
} // 在内置 addons 基础上定制 addons
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (typeof addons === 'function') {
|
|
194
|
-
addonsArray = addons(addonsArray, {
|
|
195
|
-
navigation: parsedNavigation
|
|
196
|
-
}) || [];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return addonsArray;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const renderedAddons = renderAddons();
|
|
203
|
-
const addonList = /*#__PURE__*/(0, _react.createElement)(_jsxRuntime.Fragment, null, ...(Array.isArray(renderedAddons) ? renderedAddons : [renderedAddons]));
|
|
168
|
+
const headerAddons = /*#__PURE__*/(0, _jsxRuntime.jsx)(_headerAddons.default, {
|
|
169
|
+
formattedBlocklet: formattedBlocklet,
|
|
170
|
+
addons: _addons,
|
|
171
|
+
sessionManagerProps: sessionManagerProps
|
|
172
|
+
});
|
|
204
173
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_overridableThemeProvider.default, {
|
|
205
174
|
theme: themeOverrides,
|
|
206
175
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(StyledUxHeader, _objectSpread(_objectSpread({
|
|
@@ -226,7 +195,7 @@ function Header(_ref) {
|
|
|
226
195
|
}),
|
|
227
196
|
children: did
|
|
228
197
|
}) : null,
|
|
229
|
-
addons:
|
|
198
|
+
addons: headerAddons
|
|
230
199
|
}, rest), {}, {
|
|
231
200
|
$bgcolor: theme === null || theme === void 0 ? void 0 : (_theme$background = theme.background) === null || _theme$background === void 0 ? void 0 : _theme$background.header,
|
|
232
201
|
children: !(navItems !== null && navItems !== void 0 && navItems.length) ? null : _ref2 => {
|
package/lib/blocklets.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.publicPath = exports.parseNavigation = exports.getLocalizedNavigation = exports.formatTheme = exports.formatBlockletInfo = void 0;
|
|
6
|
+
exports.publicPath = exports.parseNavigation = exports.getLocalizedNavigation = exports.formatTheme = exports.formatNavigation = exports.formatBlockletInfo = exports.filterNavByRole = void 0;
|
|
7
7
|
|
|
8
8
|
var _utils = require("./utils");
|
|
9
9
|
|
|
@@ -93,20 +93,44 @@ const getLocalizedNavigation = function getLocalizedNavigation(navigation) {
|
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
return (0, _utils.mapRecursive)(navigation, item => {
|
|
96
|
+
var _item$items;
|
|
97
|
+
|
|
96
98
|
return _objectSpread(_objectSpread({}, item), {}, {
|
|
97
99
|
title: getTitle(item.title, locale),
|
|
98
|
-
|
|
100
|
+
// 仅对叶结点进行处理
|
|
101
|
+
link: !((_item$items = item.items) !== null && _item$items !== void 0 && _item$items.length) ? getLink(item.link, locale) : item.link
|
|
99
102
|
});
|
|
100
103
|
}, 'items');
|
|
101
104
|
};
|
|
105
|
+
/**
|
|
106
|
+
* 格式化 navigation
|
|
107
|
+
*
|
|
108
|
+
* - role 统一为数组形式
|
|
109
|
+
*/
|
|
110
|
+
|
|
102
111
|
|
|
103
112
|
exports.getLocalizedNavigation = getLocalizedNavigation;
|
|
104
113
|
|
|
114
|
+
const formatNavigation = navigation => {
|
|
115
|
+
return (0, _utils.mapRecursive)(navigation, item => {
|
|
116
|
+
if (item.role) {
|
|
117
|
+
return _objectSpread(_objectSpread({}, item), {}, {
|
|
118
|
+
role: Array.isArray(item.role) ? item.role : [item.role]
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return item;
|
|
123
|
+
}, 'items');
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
exports.formatNavigation = formatNavigation;
|
|
127
|
+
|
|
105
128
|
const parseNavigation = navigation => {
|
|
106
129
|
if (!(navigation !== null && navigation !== void 0 && navigation.length)) {
|
|
107
130
|
return null;
|
|
108
131
|
}
|
|
109
132
|
|
|
133
|
+
const formattedNav = formatNavigation(navigation);
|
|
110
134
|
const sections = {
|
|
111
135
|
header: [],
|
|
112
136
|
footer: [],
|
|
@@ -115,10 +139,12 @@ const parseNavigation = navigation => {
|
|
|
115
139
|
// 对应 footer 底部 links
|
|
116
140
|
bottom: [],
|
|
117
141
|
// 对应 dashboard#sidenav 导航
|
|
118
|
-
dashboard: []
|
|
142
|
+
dashboard: [],
|
|
143
|
+
// session manager menus
|
|
144
|
+
sessionManager: []
|
|
119
145
|
}; // 对 navigation 顶层元素按 section 分组
|
|
120
146
|
|
|
121
|
-
|
|
147
|
+
formattedNav.forEach(item => {
|
|
122
148
|
// item#section 为空时, 表示只存在于 header
|
|
123
149
|
if (!item.section) {
|
|
124
150
|
sections.header.push(item); // item 出现在指定几个 section 中 (array)
|
|
@@ -156,5 +182,32 @@ const formatBlockletInfo = blockletInfo => {
|
|
|
156
182
|
formatted.navigation = parseNavigation(formatted.navigation);
|
|
157
183
|
return formatted;
|
|
158
184
|
};
|
|
185
|
+
/**
|
|
186
|
+
* 根据 role 筛选 nav, 规则:
|
|
187
|
+
* - 如果是枝结点, 必须至少存在一个符合条件的子结点
|
|
188
|
+
* - role 未定义, 符合条件
|
|
189
|
+
* - role 定义且包括当前的 userRole, 符合条件
|
|
190
|
+
*
|
|
191
|
+
* @param {object[]} nav 导航菜单数据
|
|
192
|
+
* @param {string} userRole 当前用户 role
|
|
193
|
+
* @returns 符合 role 权限的导航菜单数据
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
exports.formatBlockletInfo = formatBlockletInfo;
|
|
198
|
+
|
|
199
|
+
const filterNavByRole = (nav, userRole) => {
|
|
200
|
+
return (0, _utils.filterRecursive)(nav, (item, context) => {
|
|
201
|
+
const isRoleMatched = !item.role || userRole && item.role.includes(userRole);
|
|
202
|
+
|
|
203
|
+
if (!context.isLeaf) {
|
|
204
|
+
var _context$filteredChil;
|
|
205
|
+
|
|
206
|
+
return isRoleMatched && ((_context$filteredChil = context.filteredChildren) === null || _context$filteredChil === void 0 ? void 0 : _context$filteredChil.length);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return isRoleMatched;
|
|
210
|
+
}, 'items');
|
|
211
|
+
};
|
|
159
212
|
|
|
160
|
-
exports.
|
|
213
|
+
exports.filterNavByRole = filterNavByRole;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = HeaderAddons;
|
|
7
|
+
|
|
8
|
+
var _react = require("react");
|
|
9
|
+
|
|
10
|
+
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
11
|
+
|
|
12
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
|
+
|
|
14
|
+
var _Session = require("@arcblock/did-connect/lib/Session");
|
|
15
|
+
|
|
16
|
+
var _SessionManager = _interopRequireDefault(require("@arcblock/did-connect/lib/SessionManager"));
|
|
17
|
+
|
|
18
|
+
var _selector = _interopRequireDefault(require("@arcblock/ux/lib/Locale/selector"));
|
|
19
|
+
|
|
20
|
+
var _context = require("@arcblock/ux/lib/Locale/context");
|
|
21
|
+
|
|
22
|
+
var _types = require("../types");
|
|
23
|
+
|
|
24
|
+
var _blocklets = require("../blocklets");
|
|
25
|
+
|
|
26
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
27
|
+
|
|
28
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
29
|
+
|
|
30
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
31
|
+
|
|
32
|
+
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line no-shadow
|
|
35
|
+
function HeaderAddons(_ref) {
|
|
36
|
+
var _sessionCtx$session, _formattedBlocklet$na, _sessionCtx$session2, _sessionCtx$session2$;
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
formattedBlocklet,
|
|
40
|
+
addons,
|
|
41
|
+
sessionManagerProps
|
|
42
|
+
} = _ref;
|
|
43
|
+
const sessionCtx = (0, _react.useContext)(_Session.SessionContext);
|
|
44
|
+
const {
|
|
45
|
+
locale
|
|
46
|
+
} = (0, _context.useLocaleContext)() || {};
|
|
47
|
+
const {
|
|
48
|
+
enableConnect = true,
|
|
49
|
+
enableLocale = true
|
|
50
|
+
} = formattedBlocklet;
|
|
51
|
+
const authenticated = !!(sessionCtx !== null && sessionCtx !== void 0 && (_sessionCtx$session = sessionCtx.session) !== null && _sessionCtx$session !== void 0 && _sessionCtx$session.user);
|
|
52
|
+
let localizedNav = (0, _blocklets.getLocalizedNavigation)(formattedBlocklet === null || formattedBlocklet === void 0 ? void 0 : (_formattedBlocklet$na = formattedBlocklet.navigation) === null || _formattedBlocklet$na === void 0 ? void 0 : _formattedBlocklet$na.sessionManager, locale) || []; // 根据 role 筛选 nav 数据
|
|
53
|
+
|
|
54
|
+
localizedNav = (0, _blocklets.filterNavByRole)(localizedNav, sessionCtx === null || sessionCtx === void 0 ? void 0 : (_sessionCtx$session2 = sessionCtx.session) === null || _sessionCtx$session2 === void 0 ? void 0 : (_sessionCtx$session2$ = _sessionCtx$session2.user) === null || _sessionCtx$session2$ === void 0 ? void 0 : _sessionCtx$session2$.role);
|
|
55
|
+
|
|
56
|
+
const renderAddons = () => {
|
|
57
|
+
// 不关心内置的 session manager 和 locale selector, 直接覆盖 UX Header 的 addons
|
|
58
|
+
if (addons && typeof addons !== 'function') {
|
|
59
|
+
return Array.isArray(addons) ? addons : [addons];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let addonsArray = []; // 启用了多语言并且检测到了 locale context
|
|
63
|
+
|
|
64
|
+
if (enableLocale && locale) {
|
|
65
|
+
addonsArray.push( /*#__PURE__*/(0, _jsxRuntime.jsx)(_selector.default, {
|
|
66
|
+
showText: false
|
|
67
|
+
}, "locale-selector"));
|
|
68
|
+
} // 启用了连接钱包并且检测到了 session context
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if (enableConnect && sessionCtx) {
|
|
72
|
+
var _localizedNav;
|
|
73
|
+
|
|
74
|
+
const menu = [];
|
|
75
|
+
|
|
76
|
+
if (authenticated && (_localizedNav = localizedNav) !== null && _localizedNav !== void 0 && _localizedNav.length) {
|
|
77
|
+
localizedNav.forEach(item => {
|
|
78
|
+
menu.push({
|
|
79
|
+
label: item.title,
|
|
80
|
+
icon: item.icon ? /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
|
|
81
|
+
className: "iconify",
|
|
82
|
+
"data-icon": item.icon,
|
|
83
|
+
"data-height": "24",
|
|
84
|
+
style: {
|
|
85
|
+
marginRight: 16
|
|
86
|
+
}
|
|
87
|
+
}) : null,
|
|
88
|
+
component: 'a',
|
|
89
|
+
href: item.link,
|
|
90
|
+
key: item.link
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
addonsArray.push( /*#__PURE__*/(0, _jsxRuntime.jsx)(_SessionManager.default, _objectSpread({
|
|
96
|
+
session: sessionCtx.session,
|
|
97
|
+
locale: locale,
|
|
98
|
+
menu: menu
|
|
99
|
+
}, sessionManagerProps), "session-manager"));
|
|
100
|
+
} // 在内置 addons 基础上定制 addons
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if (typeof addons === 'function') {
|
|
104
|
+
addonsArray = addons(addonsArray) || [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return addonsArray;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const renderedAddons = renderAddons();
|
|
111
|
+
const addonList = /*#__PURE__*/(0, _react.createElement)(_jsxRuntime.Fragment, null, ...(Array.isArray(renderedAddons) ? renderedAddons : [renderedAddons]));
|
|
112
|
+
return addonList;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
HeaderAddons.propTypes = {
|
|
116
|
+
formattedBlocklet: _propTypes.default.object.isRequired,
|
|
117
|
+
// 需要考虑 定制的 addons 与内置的 连接钱包/选择语言 addons 共存的情况
|
|
118
|
+
// - PropTypes.func: 可以把自定义 addons 插在 session-manager 或 locale-selector (如果存在的话) 前/中/后
|
|
119
|
+
// - PropTypes.node: 将 addons 原样传给 UX Header 组件
|
|
120
|
+
addons: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.node]),
|
|
121
|
+
sessionManagerProps: _types.sessionManagerProps
|
|
122
|
+
};
|
|
123
|
+
HeaderAddons.defaultProps = {
|
|
124
|
+
addons: null,
|
|
125
|
+
sessionManagerProps: {
|
|
126
|
+
showRole: true
|
|
127
|
+
}
|
|
128
|
+
};
|
package/lib/types.js
CHANGED
|
@@ -20,11 +20,11 @@ const blockletMetaProps = _propTypes.default.shape({
|
|
|
20
20
|
enableLocale: _propTypes.default.bool,
|
|
21
21
|
navigation: _propTypes.default.arrayOf(_propTypes.default.shape({
|
|
22
22
|
title: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.object]),
|
|
23
|
-
link: _propTypes.default.string,
|
|
23
|
+
link: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.object]),
|
|
24
24
|
icon: _propTypes.default.string,
|
|
25
25
|
items: _propTypes.default.arrayOf(_propTypes.default.shape({
|
|
26
26
|
title: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.object]),
|
|
27
|
-
link: _propTypes.default.string
|
|
27
|
+
link: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.object])
|
|
28
28
|
}))
|
|
29
29
|
}))
|
|
30
30
|
});
|
package/lib/utils.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.matchPaths = exports.matchPath = exports.mapRecursive = exports.isUrl = exports.flatRecursive = exports.countRecursive = void 0;
|
|
6
|
+
exports.matchPaths = exports.matchPath = exports.mapRecursive = exports.isUrl = exports.flatRecursive = exports.filterRecursive = exports.countRecursive = void 0;
|
|
7
7
|
|
|
8
8
|
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
9
9
|
|
|
@@ -42,11 +42,33 @@ const countRecursive = function countRecursive(array) {
|
|
|
42
42
|
let counter = 0;
|
|
43
43
|
mapRecursive(array, () => counter++, childrenKey);
|
|
44
44
|
return counter;
|
|
45
|
-
}; //
|
|
45
|
+
}; // 对有层级结构的 array 进行 filter 处理
|
|
46
|
+
// 因为是 DFS 遍历, 可以借助 context.filteredChildren 在过滤/保留子结的同时保持父子结构 (即使父结点不满足筛选条件)
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
exports.countRecursive = countRecursive;
|
|
49
50
|
|
|
51
|
+
const filterRecursive = function filterRecursive(array, predicate) {
|
|
52
|
+
let childrenKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'children';
|
|
53
|
+
return array.map(item => _objectSpread({}, item)).filter(item => {
|
|
54
|
+
const children = item[childrenKey];
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(children)) {
|
|
57
|
+
const filtered = filterRecursive(children, predicate, childrenKey);
|
|
58
|
+
item[childrenKey] = filtered !== null && filtered !== void 0 && filtered.length ? filtered : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const context = {
|
|
62
|
+
filteredChildren: item[childrenKey],
|
|
63
|
+
isLeaf: !(children !== null && children !== void 0 && children.length)
|
|
64
|
+
};
|
|
65
|
+
return predicate(item, context);
|
|
66
|
+
});
|
|
67
|
+
}; // "http://", "https://" 2 种情况
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
exports.filterRecursive = filterRecursive;
|
|
71
|
+
|
|
50
72
|
const isUrl = str => {
|
|
51
73
|
return /^https?:\/\//.test(str);
|
|
52
74
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/ui-react",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.26",
|
|
4
4
|
"description": "Some useful front-end web components that can be used in Blocklets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"url": "https://github.com/ArcBlock/ux/issues"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@arcblock/did-connect": "^2.4.
|
|
34
|
-
"@arcblock/ux": "^2.4.
|
|
33
|
+
"@arcblock/did-connect": "^2.4.26",
|
|
34
|
+
"@arcblock/ux": "^2.4.26",
|
|
35
35
|
"@emotion/react": "^11.10.0",
|
|
36
36
|
"@emotion/styled": "^11.10.0",
|
|
37
37
|
"@iconify/iconify": "^2.2.1",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
54
54
|
"jest": "^28.1.3"
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "40365e41139ba7dd6749e7a395703f25ae454c07"
|
|
57
57
|
}
|
package/src/Dashboard/index.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable no-shadow */
|
|
2
|
+
import { useMemo, useLayoutEffect, useContext } from 'react';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import { SessionContext } from '@arcblock/did-connect/lib/Session';
|
|
2
5
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
6
|
import UxDashboard from '@arcblock/ux/lib/Layout/dashboard';
|
|
4
7
|
import DidAddress from '@arcblock/did-connect/lib/Address';
|
|
5
8
|
import DidAvatar from '@arcblock/did-connect/lib/Avatar';
|
|
6
|
-
import { blockletMetaProps } from '../types';
|
|
9
|
+
import { blockletMetaProps, sessionManagerProps } from '../types';
|
|
7
10
|
import { mapRecursive, flatRecursive, matchPaths } from '../utils';
|
|
8
|
-
import { publicPath, formatBlockletInfo, getLocalizedNavigation } from '../blocklets';
|
|
11
|
+
import { publicPath, formatBlockletInfo, getLocalizedNavigation, filterNavByRole } from '../blocklets';
|
|
12
|
+
import HeaderAddons from '../common/header-addons';
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* 专门用于 (composable) blocklet 的 Dashboard 组件, 解析 blocklet meta 中 section 为 dashboard 的 navigation 数据, 渲染一个 UX Dashboard
|
|
12
16
|
*/
|
|
13
|
-
|
|
17
|
+
// eslint-disable-next-line no-shadow
|
|
18
|
+
function Dashboard({ meta, fallbackUrl, invalidPathFallback, headerAddons, sessionManagerProps, ...rest }) {
|
|
19
|
+
const sessionCtx = useContext(SessionContext);
|
|
20
|
+
const user = sessionCtx?.session?.user;
|
|
21
|
+
const userRole = user?.role;
|
|
14
22
|
const { locale } = useLocaleContext() || {};
|
|
15
23
|
const blocklet = Object.assign({}, window.blocklet, meta);
|
|
16
24
|
const formattedBlocklet = useMemo(() => {
|
|
@@ -21,31 +29,64 @@ function Dashboard({ meta, ...rest }) {
|
|
|
21
29
|
return blocklet;
|
|
22
30
|
}
|
|
23
31
|
}, [blocklet]);
|
|
32
|
+
const { localizedNav, flattened, matchedIndex } = useMemo(() => {
|
|
33
|
+
let localizedNav = getLocalizedNavigation(formattedBlocklet?.navigation?.dashboard, locale) || [];
|
|
34
|
+
// 根据 role 筛选 nav 数据
|
|
35
|
+
localizedNav = filterNavByRole(localizedNav, userRole);
|
|
36
|
+
// 将 nav 数据处理成 ux dashboard 需要的格式
|
|
37
|
+
localizedNav = mapRecursive(
|
|
38
|
+
localizedNav,
|
|
39
|
+
(item) => ({
|
|
40
|
+
title: item.title,
|
|
41
|
+
url: item.link,
|
|
42
|
+
icon: item.icon ? <span className="iconify" data-icon={item.icon} /> : null,
|
|
43
|
+
// https://github.com/ArcBlock/ux/issues/755#issuecomment-1208692620
|
|
44
|
+
external: true,
|
|
45
|
+
children: item.items,
|
|
46
|
+
}),
|
|
47
|
+
'items'
|
|
48
|
+
);
|
|
49
|
+
// 展平后使用 matchPaths 检测 link#active 状态
|
|
50
|
+
const flattened = flatRecursive(localizedNav).filter((item) => !!item.url);
|
|
51
|
+
const matchedIndex = matchPaths(flattened.map((item) => item.url));
|
|
52
|
+
if (flattened?.length) {
|
|
53
|
+
flattened[matchedIndex === -1 ? 0 : matchedIndex].active = true;
|
|
54
|
+
}
|
|
55
|
+
return { localizedNav, flattened, matchedIndex };
|
|
56
|
+
}, [formattedBlocklet, locale, userRole]);
|
|
57
|
+
|
|
58
|
+
// 页面初始化时, 如果当前用户没有权限访问任何导航菜单 (比如登录时未提供 VC 导致无权限), 则跳转到 fallbackUrl
|
|
59
|
+
// 未认证 (user 为空) 时不做处理, 这种情况的页面跳转逻辑一般由应用自行处理
|
|
60
|
+
useLayoutEffect(() => {
|
|
61
|
+
if (!!user && !flattened?.length && fallbackUrl) {
|
|
62
|
+
window.location.href = fallbackUrl;
|
|
63
|
+
}
|
|
64
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
|
+
}, [fallbackUrl]);
|
|
66
|
+
|
|
67
|
+
// 导航菜单变动且存在可用菜单但无匹配项时 (如切换 passport), 跳转到首个菜单项
|
|
68
|
+
useLayoutEffect(() => {
|
|
69
|
+
if (!!user && !!flattened?.length && matchedIndex === -1) {
|
|
70
|
+
if (invalidPathFallback) {
|
|
71
|
+
invalidPathFallback();
|
|
72
|
+
} else {
|
|
73
|
+
window.location.href = flattened[0]?.url || publicPath;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
+
}, [invalidPathFallback]);
|
|
24
78
|
|
|
25
79
|
if (!blocklet.appName) {
|
|
26
80
|
return null;
|
|
27
81
|
}
|
|
28
|
-
|
|
29
82
|
const { did, appLogo, appName } = formattedBlocklet;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
icon: item.icon ? <span className="iconify" data-icon={item.icon} /> : null,
|
|
37
|
-
// https://github.com/ArcBlock/ux/issues/755#issuecomment-1208692620
|
|
38
|
-
external: true,
|
|
39
|
-
children: item.items,
|
|
40
|
-
}),
|
|
41
|
-
'items'
|
|
83
|
+
const _headerAddons = (
|
|
84
|
+
<HeaderAddons
|
|
85
|
+
formattedBlocklet={formattedBlocklet}
|
|
86
|
+
addons={headerAddons}
|
|
87
|
+
sessionManagerProps={sessionManagerProps}
|
|
88
|
+
/>
|
|
42
89
|
);
|
|
43
|
-
// 展平后使用 matchPaths 检测 link#active 状态
|
|
44
|
-
const flattened = flatRecursive(localizedNav).filter((item) => !!item.url);
|
|
45
|
-
const matchedIndex = matchPaths(flattened.map((item) => item.url));
|
|
46
|
-
if (flattened?.length) {
|
|
47
|
-
flattened[matchedIndex === -1 ? 0 : matchedIndex].active = true;
|
|
48
|
-
}
|
|
49
90
|
|
|
50
91
|
return (
|
|
51
92
|
<UxDashboard
|
|
@@ -71,6 +112,7 @@ function Dashboard({ meta, ...rest }) {
|
|
|
71
112
|
<img src={appLogo} alt="logo" />
|
|
72
113
|
</a>
|
|
73
114
|
),
|
|
115
|
+
addons: _headerAddons,
|
|
74
116
|
...rest.headerProps,
|
|
75
117
|
}}
|
|
76
118
|
links={localizedNav}
|
|
@@ -80,10 +122,26 @@ function Dashboard({ meta, ...rest }) {
|
|
|
80
122
|
|
|
81
123
|
Dashboard.propTypes = {
|
|
82
124
|
meta: blockletMetaProps,
|
|
125
|
+
// 如果当前用户没有权限访问任何导航菜单, 则自动跳转到 fallbackUrl, 默认值为 publicPath, 设置为 null 表示禁用自动跳转
|
|
126
|
+
fallbackUrl: PropTypes.string,
|
|
127
|
+
// 当前路径未匹配任何 nav links 时的 fallback, 默认行为跳转到首个可用的 nav link
|
|
128
|
+
invalidPathFallback: PropTypes.func,
|
|
129
|
+
headerAddons: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
|
|
130
|
+
sessionManagerProps,
|
|
83
131
|
};
|
|
84
132
|
|
|
85
133
|
Dashboard.defaultProps = {
|
|
86
134
|
meta: {},
|
|
135
|
+
fallbackUrl: publicPath,
|
|
136
|
+
invalidPathFallback: null,
|
|
137
|
+
headerAddons: undefined,
|
|
138
|
+
sessionManagerProps: {
|
|
139
|
+
showRole: true,
|
|
140
|
+
// dashboard 默认退出登录行为: 跳转到 (root) blocklet 首页
|
|
141
|
+
onLogout: () => {
|
|
142
|
+
window.location.href = publicPath;
|
|
143
|
+
},
|
|
144
|
+
},
|
|
87
145
|
};
|
|
88
146
|
|
|
89
147
|
export default Dashboard;
|
package/src/Header/index.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
// FIXME: 直接从 react 中 import Fragment 可能会在 vite 下出错,先暂时从 react/jsx-runtime 导入 Fragment 来跳过这个问题
|
|
3
|
-
import { Fragment } from 'react/jsx-runtime';
|
|
1
|
+
import { useMemo } from 'react';
|
|
4
2
|
import PropTypes from 'prop-types';
|
|
5
3
|
import { withErrorBoundary } from 'react-error-boundary';
|
|
6
4
|
import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
|
|
7
5
|
import { styled } from '@arcblock/ux/lib/Theme';
|
|
8
6
|
import { ResponsiveHeader } from '@arcblock/ux/lib/Header';
|
|
9
7
|
import NavMenu from '@arcblock/ux/lib/NavMenu';
|
|
10
|
-
import { SessionContext } from '@arcblock/did-connect/lib/Session';
|
|
11
|
-
import SessionManager from '@arcblock/did-connect/lib/SessionManager';
|
|
12
|
-
import LocaleSelector from '@arcblock/ux/lib/Locale/selector';
|
|
13
8
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
14
9
|
import DidAddress from '@arcblock/did-connect/lib/Address';
|
|
15
10
|
import DidAvatar from '@arcblock/did-connect/lib/Avatar';
|
|
@@ -19,6 +14,7 @@ import OverridableThemeProvider from '../common/overridable-theme-provider';
|
|
|
19
14
|
import { blockletMetaProps, sessionManagerProps } from '../types';
|
|
20
15
|
import { mapRecursive, flatRecursive, matchPaths } from '../utils';
|
|
21
16
|
import { publicPath, formatBlockletInfo, getLocalizedNavigation } from '../blocklets';
|
|
17
|
+
import HeaderAddons from '../common/header-addons';
|
|
22
18
|
|
|
23
19
|
// blocklet meta 中的 navigation 数据 => NavMenu 组件的 items
|
|
24
20
|
const parseNavigation = (navigation) => {
|
|
@@ -65,7 +61,6 @@ const parseNavigation = (navigation) => {
|
|
|
65
61
|
*/
|
|
66
62
|
// eslint-disable-next-line no-shadow
|
|
67
63
|
function Header({ meta, addons, sessionManagerProps, homeLink, theme: themeOverrides, ...rest }) {
|
|
68
|
-
const sessionInfo = useContext(SessionContext);
|
|
69
64
|
const { locale } = useLocaleContext() || {};
|
|
70
65
|
const blocklet = Object.assign({}, window.blocklet, meta);
|
|
71
66
|
const formattedBlocklet = useMemo(() => {
|
|
@@ -80,41 +75,15 @@ function Header({ meta, addons, sessionManagerProps, homeLink, theme: themeOverr
|
|
|
80
75
|
if (!blocklet.appName) {
|
|
81
76
|
return null;
|
|
82
77
|
}
|
|
83
|
-
const { did, appLogo, appName, theme
|
|
78
|
+
const { did, appLogo, appName, theme } = formattedBlocklet;
|
|
84
79
|
const navigation = getLocalizedNavigation(formattedBlocklet?.navigation?.header, locale);
|
|
85
80
|
const parsedNavigation = parseNavigation(navigation);
|
|
86
81
|
const { navItems, activeId } = parsedNavigation;
|
|
87
82
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
let addonsArray = [];
|
|
94
|
-
// 启用了多语言并且检测到了 locale context
|
|
95
|
-
if (enableLocale && locale) {
|
|
96
|
-
addonsArray.push(<LocaleSelector key="locale-selector" showText={false} />);
|
|
97
|
-
}
|
|
98
|
-
// 启用了连接钱包并且检测到了 session context
|
|
99
|
-
if (enableConnect && sessionInfo) {
|
|
100
|
-
addonsArray.push(<SessionManager key="session-manager" session={sessionInfo.session} {...sessionManagerProps} />);
|
|
101
|
-
}
|
|
102
|
-
// 在内置 addons 基础上定制 addons
|
|
103
|
-
if (typeof addons === 'function') {
|
|
104
|
-
addonsArray =
|
|
105
|
-
addons(addonsArray, {
|
|
106
|
-
navigation: parsedNavigation,
|
|
107
|
-
}) || [];
|
|
108
|
-
}
|
|
109
|
-
return addonsArray;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const renderedAddons = renderAddons();
|
|
113
|
-
|
|
114
|
-
const addonList = createElement(
|
|
115
|
-
Fragment,
|
|
116
|
-
null,
|
|
117
|
-
...(Array.isArray(renderedAddons) ? renderedAddons : [renderedAddons])
|
|
83
|
+
const _addons =
|
|
84
|
+
typeof addons === 'function' ? (builtInAddons) => addons(builtInAddons, { navigation: parsedNavigation }) : addons;
|
|
85
|
+
const headerAddons = (
|
|
86
|
+
<HeaderAddons formattedBlocklet={formattedBlocklet} addons={_addons} sessionManagerProps={sessionManagerProps} />
|
|
118
87
|
);
|
|
119
88
|
|
|
120
89
|
return (
|
|
@@ -138,7 +107,7 @@ function Header({ meta, addons, sessionManagerProps, homeLink, theme: themeOverr
|
|
|
138
107
|
</DidAddress>
|
|
139
108
|
) : null
|
|
140
109
|
}
|
|
141
|
-
addons={
|
|
110
|
+
addons={headerAddons}
|
|
142
111
|
{...rest}
|
|
143
112
|
$bgcolor={theme?.background?.header}>
|
|
144
113
|
{/* blocklet.yml 没有配置 navigation 时, 则为 children 传入 null, 此时 ResponsiveHeader 会渲染普通的不带 menu 的 Header */}
|
package/src/blocklets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapRecursive, isUrl } from './utils';
|
|
1
|
+
import { mapRecursive, filterRecursive, isUrl } from './utils';
|
|
2
2
|
|
|
3
3
|
export const publicPath = window?.blocklet?.groupPrefix || window?.blocklet?.prefix || '/';
|
|
4
4
|
|
|
@@ -61,18 +61,42 @@ export const getLocalizedNavigation = (navigation, locale = 'en') => {
|
|
|
61
61
|
return {
|
|
62
62
|
...item,
|
|
63
63
|
title: getTitle(item.title, locale),
|
|
64
|
-
|
|
64
|
+
// 仅对叶结点进行处理
|
|
65
|
+
link: !item.items?.length ? getLink(item.link, locale) : item.link,
|
|
65
66
|
};
|
|
66
67
|
},
|
|
67
68
|
'items'
|
|
68
69
|
);
|
|
69
70
|
};
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* 格式化 navigation
|
|
74
|
+
*
|
|
75
|
+
* - role 统一为数组形式
|
|
76
|
+
*/
|
|
77
|
+
export const formatNavigation = (navigation) => {
|
|
78
|
+
return mapRecursive(
|
|
79
|
+
navigation,
|
|
80
|
+
(item) => {
|
|
81
|
+
if (item.role) {
|
|
82
|
+
return {
|
|
83
|
+
...item,
|
|
84
|
+
role: Array.isArray(item.role) ? item.role : [item.role],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return item;
|
|
88
|
+
},
|
|
89
|
+
'items'
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
71
93
|
export const parseNavigation = (navigation) => {
|
|
72
94
|
if (!navigation?.length) {
|
|
73
95
|
return null;
|
|
74
96
|
}
|
|
75
97
|
|
|
98
|
+
const formattedNav = formatNavigation(navigation);
|
|
99
|
+
|
|
76
100
|
const sections = {
|
|
77
101
|
header: [],
|
|
78
102
|
footer: [],
|
|
@@ -82,10 +106,12 @@ export const parseNavigation = (navigation) => {
|
|
|
82
106
|
bottom: [],
|
|
83
107
|
// 对应 dashboard#sidenav 导航
|
|
84
108
|
dashboard: [],
|
|
109
|
+
// session manager menus
|
|
110
|
+
sessionManager: [],
|
|
85
111
|
};
|
|
86
112
|
|
|
87
113
|
// 对 navigation 顶层元素按 section 分组
|
|
88
|
-
|
|
114
|
+
formattedNav.forEach((item) => {
|
|
89
115
|
// item#section 为空时, 表示只存在于 header
|
|
90
116
|
if (!item.section) {
|
|
91
117
|
sections.header.push(item);
|
|
@@ -117,3 +143,27 @@ export const formatBlockletInfo = (blockletInfo) => {
|
|
|
117
143
|
formatted.navigation = parseNavigation(formatted.navigation);
|
|
118
144
|
return formatted;
|
|
119
145
|
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 根据 role 筛选 nav, 规则:
|
|
149
|
+
* - 如果是枝结点, 必须至少存在一个符合条件的子结点
|
|
150
|
+
* - role 未定义, 符合条件
|
|
151
|
+
* - role 定义且包括当前的 userRole, 符合条件
|
|
152
|
+
*
|
|
153
|
+
* @param {object[]} nav 导航菜单数据
|
|
154
|
+
* @param {string} userRole 当前用户 role
|
|
155
|
+
* @returns 符合 role 权限的导航菜单数据
|
|
156
|
+
*/
|
|
157
|
+
export const filterNavByRole = (nav, userRole) => {
|
|
158
|
+
return filterRecursive(
|
|
159
|
+
nav,
|
|
160
|
+
(item, context) => {
|
|
161
|
+
const isRoleMatched = !item.role || (userRole && item.role.includes(userRole));
|
|
162
|
+
if (!context.isLeaf) {
|
|
163
|
+
return isRoleMatched && context.filteredChildren?.length;
|
|
164
|
+
}
|
|
165
|
+
return isRoleMatched;
|
|
166
|
+
},
|
|
167
|
+
'items'
|
|
168
|
+
);
|
|
169
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useContext, createElement } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
// FIXME: 直接从 react 中 import Fragment 可能会在 vite 下出错,先暂时从 react/jsx-runtime 导入 Fragment 来跳过这个问题
|
|
4
|
+
import { Fragment } from 'react/jsx-runtime';
|
|
5
|
+
import { SessionContext } from '@arcblock/did-connect/lib/Session';
|
|
6
|
+
import SessionManager from '@arcblock/did-connect/lib/SessionManager';
|
|
7
|
+
import LocaleSelector from '@arcblock/ux/lib/Locale/selector';
|
|
8
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
9
|
+
import { sessionManagerProps } from '../types';
|
|
10
|
+
import { getLocalizedNavigation, filterNavByRole } from '../blocklets';
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line no-shadow
|
|
13
|
+
export default function HeaderAddons({ formattedBlocklet, addons, sessionManagerProps }) {
|
|
14
|
+
const sessionCtx = useContext(SessionContext);
|
|
15
|
+
const { locale } = useLocaleContext() || {};
|
|
16
|
+
const { enableConnect = true, enableLocale = true } = formattedBlocklet;
|
|
17
|
+
const authenticated = !!sessionCtx?.session?.user;
|
|
18
|
+
let localizedNav = getLocalizedNavigation(formattedBlocklet?.navigation?.sessionManager, locale) || [];
|
|
19
|
+
// 根据 role 筛选 nav 数据
|
|
20
|
+
localizedNav = filterNavByRole(localizedNav, sessionCtx?.session?.user?.role);
|
|
21
|
+
|
|
22
|
+
const renderAddons = () => {
|
|
23
|
+
// 不关心内置的 session manager 和 locale selector, 直接覆盖 UX Header 的 addons
|
|
24
|
+
if (addons && typeof addons !== 'function') {
|
|
25
|
+
return Array.isArray(addons) ? addons : [addons];
|
|
26
|
+
}
|
|
27
|
+
let addonsArray = [];
|
|
28
|
+
// 启用了多语言并且检测到了 locale context
|
|
29
|
+
if (enableLocale && locale) {
|
|
30
|
+
addonsArray.push(<LocaleSelector key="locale-selector" showText={false} />);
|
|
31
|
+
}
|
|
32
|
+
// 启用了连接钱包并且检测到了 session context
|
|
33
|
+
if (enableConnect && sessionCtx) {
|
|
34
|
+
const menu = [];
|
|
35
|
+
if (authenticated && localizedNav?.length) {
|
|
36
|
+
localizedNav.forEach((item) => {
|
|
37
|
+
menu.push({
|
|
38
|
+
label: item.title,
|
|
39
|
+
icon: item.icon ? (
|
|
40
|
+
<span className="iconify" data-icon={item.icon} data-height="24" style={{ marginRight: 16 }} />
|
|
41
|
+
) : null,
|
|
42
|
+
component: 'a',
|
|
43
|
+
href: item.link,
|
|
44
|
+
key: item.link,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
addonsArray.push(
|
|
49
|
+
<SessionManager
|
|
50
|
+
key="session-manager"
|
|
51
|
+
session={sessionCtx.session}
|
|
52
|
+
locale={locale}
|
|
53
|
+
menu={menu}
|
|
54
|
+
{...sessionManagerProps}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
// 在内置 addons 基础上定制 addons
|
|
59
|
+
if (typeof addons === 'function') {
|
|
60
|
+
addonsArray = addons(addonsArray) || [];
|
|
61
|
+
}
|
|
62
|
+
return addonsArray;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const renderedAddons = renderAddons();
|
|
66
|
+
const addonList = createElement(
|
|
67
|
+
Fragment,
|
|
68
|
+
null,
|
|
69
|
+
...(Array.isArray(renderedAddons) ? renderedAddons : [renderedAddons])
|
|
70
|
+
);
|
|
71
|
+
return addonList;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
HeaderAddons.propTypes = {
|
|
75
|
+
formattedBlocklet: PropTypes.object.isRequired,
|
|
76
|
+
// 需要考虑 定制的 addons 与内置的 连接钱包/选择语言 addons 共存的情况
|
|
77
|
+
// - PropTypes.func: 可以把自定义 addons 插在 session-manager 或 locale-selector (如果存在的话) 前/中/后
|
|
78
|
+
// - PropTypes.node: 将 addons 原样传给 UX Header 组件
|
|
79
|
+
addons: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
|
|
80
|
+
sessionManagerProps,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
HeaderAddons.defaultProps = {
|
|
84
|
+
addons: null,
|
|
85
|
+
sessionManagerProps: {
|
|
86
|
+
showRole: true,
|
|
87
|
+
},
|
|
88
|
+
};
|
package/src/types.js
CHANGED
|
@@ -12,12 +12,12 @@ export const blockletMetaProps = PropTypes.shape({
|
|
|
12
12
|
navigation: PropTypes.arrayOf(
|
|
13
13
|
PropTypes.shape({
|
|
14
14
|
title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
|
15
|
-
link: PropTypes.string,
|
|
15
|
+
link: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
|
16
16
|
icon: PropTypes.string,
|
|
17
17
|
items: PropTypes.arrayOf(
|
|
18
18
|
PropTypes.shape({
|
|
19
19
|
title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
|
20
|
-
link: PropTypes.string,
|
|
20
|
+
link: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
|
21
21
|
})
|
|
22
22
|
),
|
|
23
23
|
})
|
package/src/utils.js
CHANGED
|
@@ -24,6 +24,22 @@ export const countRecursive = (array, childrenKey = 'children') => {
|
|
|
24
24
|
return counter;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
// 对有层级结构的 array 进行 filter 处理
|
|
28
|
+
// 因为是 DFS 遍历, 可以借助 context.filteredChildren 在过滤/保留子结的同时保持父子结构 (即使父结点不满足筛选条件)
|
|
29
|
+
export const filterRecursive = (array, predicate, childrenKey = 'children') => {
|
|
30
|
+
return array
|
|
31
|
+
.map((item) => ({ ...item }))
|
|
32
|
+
.filter((item) => {
|
|
33
|
+
const children = item[childrenKey];
|
|
34
|
+
if (Array.isArray(children)) {
|
|
35
|
+
const filtered = filterRecursive(children, predicate, childrenKey);
|
|
36
|
+
item[childrenKey] = filtered?.length ? filtered : undefined;
|
|
37
|
+
}
|
|
38
|
+
const context = { filteredChildren: item[childrenKey], isLeaf: !children?.length };
|
|
39
|
+
return predicate(item, context);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
27
43
|
// "http://", "https://" 2 种情况
|
|
28
44
|
export const isUrl = (str) => {
|
|
29
45
|
return /^https?:\/\//.test(str);
|