@blocklet/ui-react 2.4.24 → 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 +97 -53
- package/lib/Header/index.js +14 -45
- package/lib/blocklets.js +36 -4
- package/lib/common/header-addons.js +128 -0
- package/lib/types.js +2 -2
- package/package.json +4 -4
- package/src/Dashboard/index.js +80 -51
- package/src/Header/index.js +8 -39
- package/src/blocklets.js +29 -2
- package/src/common/header-addons.js +88 -0
- package/src/types.js +2 -2
package/lib/Dashboard/index.js
CHANGED
|
@@ -7,6 +7,8 @@ exports.default = void 0;
|
|
|
7
7
|
|
|
8
8
|
var _react = require("react");
|
|
9
9
|
|
|
10
|
+
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
11
|
+
|
|
10
12
|
var _Session = require("@arcblock/did-connect/lib/Session");
|
|
11
13
|
|
|
12
14
|
var _context = require("@arcblock/ux/lib/Locale/context");
|
|
@@ -23,9 +25,11 @@ var _utils = require("../utils");
|
|
|
23
25
|
|
|
24
26
|
var _blocklets = require("../blocklets");
|
|
25
27
|
|
|
28
|
+
var _headerAddons2 = _interopRequireDefault(require("../common/header-addons"));
|
|
29
|
+
|
|
26
30
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
27
31
|
|
|
28
|
-
const _excluded = ["meta"];
|
|
32
|
+
const _excluded = ["meta", "fallbackUrl", "invalidPathFallback", "headerAddons", "sessionManagerProps"];
|
|
29
33
|
|
|
30
34
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
31
35
|
|
|
@@ -39,43 +43,25 @@ function _objectWithoutProperties(source, excluded) { if (source == null) return
|
|
|
39
43
|
|
|
40
44
|
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
|
|
41
45
|
|
|
42
|
-
/**
|
|
43
|
-
* 根据 role 筛选 nav, 规则:
|
|
44
|
-
* - 如果是枝结点, 必须至少存在一个符合条件的子结点
|
|
45
|
-
* - role 未定义, 符合条件
|
|
46
|
-
* - role 定义且包括当前的 userRole, 符合条件
|
|
47
|
-
*
|
|
48
|
-
* @param {object[]} nav 导航菜单数据
|
|
49
|
-
* @param {string} userRole 当前用户 role
|
|
50
|
-
* @returns 符合 role 权限的导航菜单数据
|
|
51
|
-
*/
|
|
52
|
-
function filterNavByRole(nav, userRole) {
|
|
53
|
-
return (0, _utils.filterRecursive)(nav, (item, context) => {
|
|
54
|
-
const isRoleMatched = !item.role || userRole && item.role.includes(userRole);
|
|
55
|
-
|
|
56
|
-
if (!context.isLeaf) {
|
|
57
|
-
var _context$filteredChil;
|
|
58
|
-
|
|
59
|
-
return isRoleMatched && ((_context$filteredChil = context.filteredChildren) === null || _context$filteredChil === void 0 ? void 0 : _context$filteredChil.length);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return isRoleMatched;
|
|
63
|
-
}, 'items');
|
|
64
|
-
}
|
|
65
46
|
/**
|
|
66
47
|
* 专门用于 (composable) blocklet 的 Dashboard 组件, 解析 blocklet meta 中 section 为 dashboard 的 navigation 数据, 渲染一个 UX Dashboard
|
|
67
48
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
49
|
+
// eslint-disable-next-line no-shadow
|
|
70
50
|
function Dashboard(_ref) {
|
|
71
|
-
var
|
|
51
|
+
var _sessionCtx$session;
|
|
72
52
|
|
|
73
53
|
let {
|
|
74
|
-
meta
|
|
54
|
+
meta,
|
|
55
|
+
fallbackUrl,
|
|
56
|
+
invalidPathFallback,
|
|
57
|
+
headerAddons,
|
|
58
|
+
sessionManagerProps
|
|
75
59
|
} = _ref,
|
|
76
60
|
rest = _objectWithoutProperties(_ref, _excluded);
|
|
77
61
|
|
|
78
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;
|
|
79
65
|
const {
|
|
80
66
|
locale
|
|
81
67
|
} = (0, _context.useLocaleContext)() || {};
|
|
@@ -88,6 +74,63 @@ function Dashboard(_ref) {
|
|
|
88
74
|
return blocklet;
|
|
89
75
|
}
|
|
90
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]);
|
|
91
134
|
|
|
92
135
|
if (!blocklet.appName) {
|
|
93
136
|
return null;
|
|
@@ -98,28 +141,12 @@ function Dashboard(_ref) {
|
|
|
98
141
|
appLogo,
|
|
99
142
|
appName
|
|
100
143
|
} = formattedBlocklet;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
url: item.link,
|
|
108
|
-
icon: item.icon ? /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
|
|
109
|
-
className: "iconify",
|
|
110
|
-
"data-icon": item.icon
|
|
111
|
-
}) : null,
|
|
112
|
-
// https://github.com/ArcBlock/ux/issues/755#issuecomment-1208692620
|
|
113
|
-
external: true,
|
|
114
|
-
children: item.items
|
|
115
|
-
}), 'items'); // 展平后使用 matchPaths 检测 link#active 状态
|
|
116
|
-
|
|
117
|
-
const flattened = (0, _utils.flatRecursive)(localizedNav).filter(item => !!item.url);
|
|
118
|
-
const matchedIndex = (0, _utils.matchPaths)(flattened.map(item => item.url));
|
|
119
|
-
|
|
120
|
-
if (flattened !== null && flattened !== void 0 && flattened.length) {
|
|
121
|
-
flattened[matchedIndex === -1 ? 0 : matchedIndex].active = true;
|
|
122
|
-
}
|
|
144
|
+
|
|
145
|
+
const _headerAddons = /*#__PURE__*/(0, _jsxRuntime.jsx)(_headerAddons2.default, {
|
|
146
|
+
formattedBlocklet: formattedBlocklet,
|
|
147
|
+
addons: headerAddons,
|
|
148
|
+
sessionManagerProps: sessionManagerProps
|
|
149
|
+
});
|
|
123
150
|
|
|
124
151
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_dashboard.default, _objectSpread(_objectSpread({
|
|
125
152
|
title: appName,
|
|
@@ -149,17 +176,34 @@ function Dashboard(_ref) {
|
|
|
149
176
|
src: appLogo,
|
|
150
177
|
alt: "logo"
|
|
151
178
|
})
|
|
152
|
-
})
|
|
179
|
+
}),
|
|
180
|
+
addons: _headerAddons
|
|
153
181
|
}, rest.headerProps),
|
|
154
182
|
links: localizedNav
|
|
155
183
|
}));
|
|
156
184
|
}
|
|
157
185
|
|
|
158
186
|
Dashboard.propTypes = {
|
|
159
|
-
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
|
|
160
194
|
};
|
|
161
195
|
Dashboard.defaultProps = {
|
|
162
|
-
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
|
+
}
|
|
163
207
|
};
|
|
164
208
|
var _default = Dashboard;
|
|
165
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.formatNavigation = 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,9 +93,12 @@ 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
|
};
|
|
@@ -136,7 +139,9 @@ const parseNavigation = navigation => {
|
|
|
136
139
|
// 对应 footer 底部 links
|
|
137
140
|
bottom: [],
|
|
138
141
|
// 对应 dashboard#sidenav 导航
|
|
139
|
-
dashboard: []
|
|
142
|
+
dashboard: [],
|
|
143
|
+
// session manager menus
|
|
144
|
+
sessionManager: []
|
|
140
145
|
}; // 对 navigation 顶层元素按 section 分组
|
|
141
146
|
|
|
142
147
|
formattedNav.forEach(item => {
|
|
@@ -177,5 +182,32 @@ const formatBlockletInfo = blockletInfo => {
|
|
|
177
182
|
formatted.navigation = parseNavigation(formatted.navigation);
|
|
178
183
|
return formatted;
|
|
179
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
|
+
};
|
|
180
212
|
|
|
181
|
-
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/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,42 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable no-shadow */
|
|
2
|
+
import { useMemo, useLayoutEffect, useContext } from 'react';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
2
4
|
import { SessionContext } from '@arcblock/did-connect/lib/Session';
|
|
3
5
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
6
|
import UxDashboard from '@arcblock/ux/lib/Layout/dashboard';
|
|
5
7
|
import DidAddress from '@arcblock/did-connect/lib/Address';
|
|
6
8
|
import DidAvatar from '@arcblock/did-connect/lib/Avatar';
|
|
7
|
-
import { blockletMetaProps } from '../types';
|
|
8
|
-
import { mapRecursive, flatRecursive, matchPaths
|
|
9
|
-
import { publicPath, formatBlockletInfo, getLocalizedNavigation } from '../blocklets';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 根据 role 筛选 nav, 规则:
|
|
13
|
-
* - 如果是枝结点, 必须至少存在一个符合条件的子结点
|
|
14
|
-
* - role 未定义, 符合条件
|
|
15
|
-
* - role 定义且包括当前的 userRole, 符合条件
|
|
16
|
-
*
|
|
17
|
-
* @param {object[]} nav 导航菜单数据
|
|
18
|
-
* @param {string} userRole 当前用户 role
|
|
19
|
-
* @returns 符合 role 权限的导航菜单数据
|
|
20
|
-
*/
|
|
21
|
-
function filterNavByRole(nav, userRole) {
|
|
22
|
-
return filterRecursive(
|
|
23
|
-
nav,
|
|
24
|
-
(item, context) => {
|
|
25
|
-
const isRoleMatched = !item.role || (userRole && item.role.includes(userRole));
|
|
26
|
-
if (!context.isLeaf) {
|
|
27
|
-
return isRoleMatched && context.filteredChildren?.length;
|
|
28
|
-
}
|
|
29
|
-
return isRoleMatched;
|
|
30
|
-
},
|
|
31
|
-
'items'
|
|
32
|
-
);
|
|
33
|
-
}
|
|
9
|
+
import { blockletMetaProps, sessionManagerProps } from '../types';
|
|
10
|
+
import { mapRecursive, flatRecursive, matchPaths } from '../utils';
|
|
11
|
+
import { publicPath, formatBlockletInfo, getLocalizedNavigation, filterNavByRole } from '../blocklets';
|
|
12
|
+
import HeaderAddons from '../common/header-addons';
|
|
34
13
|
|
|
35
14
|
/**
|
|
36
15
|
* 专门用于 (composable) blocklet 的 Dashboard 组件, 解析 blocklet meta 中 section 为 dashboard 的 navigation 数据, 渲染一个 UX Dashboard
|
|
37
16
|
*/
|
|
38
|
-
|
|
17
|
+
// eslint-disable-next-line no-shadow
|
|
18
|
+
function Dashboard({ meta, fallbackUrl, invalidPathFallback, headerAddons, sessionManagerProps, ...rest }) {
|
|
39
19
|
const sessionCtx = useContext(SessionContext);
|
|
20
|
+
const user = sessionCtx?.session?.user;
|
|
21
|
+
const userRole = user?.role;
|
|
40
22
|
const { locale } = useLocaleContext() || {};
|
|
41
23
|
const blocklet = Object.assign({}, window.blocklet, meta);
|
|
42
24
|
const formattedBlocklet = useMemo(() => {
|
|
@@ -47,34 +29,64 @@ function Dashboard({ meta, ...rest }) {
|
|
|
47
29
|
return blocklet;
|
|
48
30
|
}
|
|
49
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]);
|
|
50
78
|
|
|
51
79
|
if (!blocklet.appName) {
|
|
52
80
|
return null;
|
|
53
81
|
}
|
|
54
|
-
|
|
55
82
|
const { did, appLogo, appName } = formattedBlocklet;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
(item) => ({
|
|
63
|
-
title: item.title,
|
|
64
|
-
url: item.link,
|
|
65
|
-
icon: item.icon ? <span className="iconify" data-icon={item.icon} /> : null,
|
|
66
|
-
// https://github.com/ArcBlock/ux/issues/755#issuecomment-1208692620
|
|
67
|
-
external: true,
|
|
68
|
-
children: item.items,
|
|
69
|
-
}),
|
|
70
|
-
'items'
|
|
83
|
+
const _headerAddons = (
|
|
84
|
+
<HeaderAddons
|
|
85
|
+
formattedBlocklet={formattedBlocklet}
|
|
86
|
+
addons={headerAddons}
|
|
87
|
+
sessionManagerProps={sessionManagerProps}
|
|
88
|
+
/>
|
|
71
89
|
);
|
|
72
|
-
// 展平后使用 matchPaths 检测 link#active 状态
|
|
73
|
-
const flattened = flatRecursive(localizedNav).filter((item) => !!item.url);
|
|
74
|
-
const matchedIndex = matchPaths(flattened.map((item) => item.url));
|
|
75
|
-
if (flattened?.length) {
|
|
76
|
-
flattened[matchedIndex === -1 ? 0 : matchedIndex].active = true;
|
|
77
|
-
}
|
|
78
90
|
|
|
79
91
|
return (
|
|
80
92
|
<UxDashboard
|
|
@@ -100,6 +112,7 @@ function Dashboard({ meta, ...rest }) {
|
|
|
100
112
|
<img src={appLogo} alt="logo" />
|
|
101
113
|
</a>
|
|
102
114
|
),
|
|
115
|
+
addons: _headerAddons,
|
|
103
116
|
...rest.headerProps,
|
|
104
117
|
}}
|
|
105
118
|
links={localizedNav}
|
|
@@ -109,10 +122,26 @@ function Dashboard({ meta, ...rest }) {
|
|
|
109
122
|
|
|
110
123
|
Dashboard.propTypes = {
|
|
111
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,
|
|
112
131
|
};
|
|
113
132
|
|
|
114
133
|
Dashboard.defaultProps = {
|
|
115
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
|
+
},
|
|
116
145
|
};
|
|
117
146
|
|
|
118
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,7 +61,8 @@ 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'
|
|
@@ -105,6 +106,8 @@ export const parseNavigation = (navigation) => {
|
|
|
105
106
|
bottom: [],
|
|
106
107
|
// 对应 dashboard#sidenav 导航
|
|
107
108
|
dashboard: [],
|
|
109
|
+
// session manager menus
|
|
110
|
+
sessionManager: [],
|
|
108
111
|
};
|
|
109
112
|
|
|
110
113
|
// 对 navigation 顶层元素按 section 分组
|
|
@@ -140,3 +143,27 @@ export const formatBlockletInfo = (blockletInfo) => {
|
|
|
140
143
|
formatted.navigation = parseNavigation(formatted.navigation);
|
|
141
144
|
return formatted;
|
|
142
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
|
})
|