@blocklet/ui-react
Version:
Some useful front-end web components that can be used in Blocklets.
166 lines (154 loc) • 6 kB
JSX
import 'iconify-icon';
/* eslint-disable no-shadow */
import { useMemo, useLayoutEffect, use } from 'react';
import PropTypes from 'prop-types';
import { SessionContext } from '@arcblock/did-connect/lib/Session';
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
import UxImg from '@arcblock/ux/lib/Img';
import UxDashboard from '@arcblock/ux/lib/Layout/dashboard';
import useBlockletLogo from '@arcblock/ux/lib/hooks/use-blocklet-logo';
import { BlockletMetaProps, SessionManagerProps } from '../types';
import { mapRecursive, flatRecursive, matchPaths } from '../utils';
import { publicPath, formatBlockletInfo, getLocalizedNavigation, filterNavByRole } from '../blocklets';
import HeaderAddons from '../common/header-addons';
import { useWalletHiddenTopbar } from '../common/wallet-hidden-topbar';
/**
* 专门用于 (composable) blocklet 的 Dashboard 组件, 解析 blocklet meta 中 section 为 dashboard 的 navigation 数据, 渲染一个 UX Dashboard
*/
function Dashboard({
meta = {},
fallbackUrl = publicPath,
invalidPathFallback = null,
headerAddons = undefined,
sessionManagerProps = {
showRole: true,
// dashboard 默认退出登录行为: 跳转到 (root) blocklet 首页
onLogout: () => {
// 这里是安全的
window.location.href = publicPath;
},
},
links = [],
showDomainWarningDialog = true,
...rest
}) {
useWalletHiddenTopbar();
const sessionCtx = use(SessionContext);
const user = sessionCtx?.session?.user;
const userRole = user?.role;
const { locale } = useLocaleContext() || {};
const formattedBlocklet = useMemo(() => {
const blocklet = Object.assign({}, window.blocklet, meta);
try {
return formatBlockletInfo(blocklet);
} catch (e) {
console.error('Failed to format blocklet info', e, blocklet);
return blocklet;
}
}, [meta]);
const { localizedNav, flattened, matchedIndex } = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-shadow
let localizedNav = getLocalizedNavigation(formattedBlocklet?.navigation?.dashboard, locale) || [];
// 根据 role 筛选 nav 数据
localizedNav = filterNavByRole(localizedNav, userRole);
// 将 nav 数据处理成 ux dashboard 需要的格式
localizedNav = mapRecursive(
localizedNav,
(item) => {
let icon = null;
if (item.icon) {
if (item.icon.startsWith('http') || item.icon.startsWith('data:')) {
icon = <UxImg src={item.icon} />;
} else {
icon = <iconify-icon height="100%" width="100%" icon={item.icon} />;
}
}
return {
id: item.id,
title: item.title,
url: item.link,
icon,
// https://github.com/ArcBlock/ux/issues/755#issuecomment-1208692620
external: true,
children: item.items,
};
},
'items'
);
// 展平后使用 matchPaths 检测 link#active 状态
// eslint-disable-next-line @typescript-eslint/no-shadow
const flattened = flatRecursive(localizedNav).filter((item) => !!item.url);
// eslint-disable-next-line @typescript-eslint/no-shadow
const matchedIndex = matchPaths(flattened.map((item) => item.url));
if (matchedIndex !== -1) {
flattened[matchedIndex].active = true;
}
return { localizedNav, flattened, matchedIndex };
}, [formattedBlocklet, locale, userRole]);
const allLinks = typeof links === 'function' ? links(localizedNav) : [...localizedNav, ...links];
const appLogo = useBlockletLogo({
meta,
});
// 页面初始化时, 如果当前用户没有权限访问任何导航菜单 (比如登录时未提供 VC 导致无权限), 则跳转到 fallbackUrl
// 未认证 (user 为空) 时不做处理, 这种情况的页面跳转逻辑一般由应用自行处理
useLayoutEffect(() => {
if (!!user && !flattened?.length && fallbackUrl) {
// 这里是安全的
window.location.href = fallbackUrl;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fallbackUrl]);
// 导航菜单变动且存在可用菜单但无匹配项时 (如切换 passport), 跳转到首个菜单项
useLayoutEffect(() => {
if (!!user && !!flattened?.length && matchedIndex === -1) {
if (invalidPathFallback) {
invalidPathFallback();
} else {
// FIXME: @zhanghan 暂时取消跳转首个 url 的行为
// window.location.href = flattened[0]?.url || publicPath;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [invalidPathFallback, flattened, matchedIndex]);
if (!formattedBlocklet.appName) {
return null;
}
const { appName } = formattedBlocklet;
// eslint-disable-next-line @typescript-eslint/naming-convention
const _headerAddons = (
<HeaderAddons
formattedBlocklet={formattedBlocklet}
addons={headerAddons}
sessionManagerProps={sessionManagerProps}
showDomainWarningDialog={showDomainWarningDialog}
/>
);
return (
<UxDashboard
title={appName}
fullWidth
sidebarWidth={128}
legacy={false}
links={allLinks}
{...rest}
headerProps={{
homeLink: publicPath,
logo: <img src={appLogo} alt="logo" />,
addons: _headerAddons,
...rest.headerProps,
}}
/>
);
}
Dashboard.propTypes = {
meta: BlockletMetaProps,
// 如果当前用户没有权限访问任何导航菜单, 则自动跳转到 fallbackUrl, 默认值为 publicPath, 设置为 null 表示禁用自动跳转
fallbackUrl: PropTypes.string,
// 当前路径未匹配任何 nav links 时的 fallback, 默认行为跳转到首个可用的 nav link
invalidPathFallback: PropTypes.func,
headerAddons: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
sessionManagerProps: SessionManagerProps,
links: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
showDomainWarningDialog: PropTypes.bool,
};
export default Dashboard;