mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
* refactor(plugin-acl): extensible support for role permissions configuration UI * feat: complete the configuration UI * feat: complete the backend section * chore: update unit tests * chore: add translation * chore: change 'Menu' to 'Desktop menu' * refactor: use 'extendCollection' instead of 'this.db.extendCollection' * chore: update acl e2e test * test: add e2e tests * fix: should refresh data when changing tab * fix(menu): should hide children when children only have one * feat: show tip when no pages find * feat(tabBar): supports left and right swiping * refactor: improve code * chore: make e2e test pass * chore: add migration * fix: should use tk instead of values * chore: nothing * fix: improve * refactor: rename mobileMenuUiSchemas to mobileRoutes * refactor: add onDelete * fix: change snippet to 'pm.mobile' from 'pm.mobile.roles' * refactor: extract nested loop to outside * refactor: use db.on('mobileRoutes:afterCreate') * refactor: simplify code logic * chore: fix build * fix: improve code * chore: fix build * feat: hide menu configuration UI when no permission --------- Co-authored-by: chenos <chenlinxh@gmail.com>
140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
/**
|
|
* This file is part of the NocoBase (R) project.
|
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
* Authors: NocoBase Team.
|
|
*
|
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
*/
|
|
|
|
import { APIClient, useAPIClient, useRequest } from '@nocobase/client';
|
|
import { Spin } from 'antd';
|
|
import React, { createContext, FC, useContext, useEffect, useMemo } from 'react';
|
|
import { useLocation } from 'react-router-dom';
|
|
|
|
import type { IResource } from '@nocobase/sdk';
|
|
|
|
import { useMobileTitle } from './MobileTitle';
|
|
|
|
export interface MobileRouteItem {
|
|
id: number;
|
|
schemaUid?: string;
|
|
type: 'page' | 'link' | 'tabs';
|
|
options?: any;
|
|
title?: string;
|
|
icon?: string;
|
|
parentId?: number;
|
|
children?: MobileRouteItem[];
|
|
}
|
|
|
|
export const MobileRoutesContext = createContext<MobileRoutesContextValue>(null);
|
|
|
|
export interface MobileRoutesContextValue {
|
|
routeList?: MobileRouteItem[];
|
|
refresh: () => Promise<any>;
|
|
resource: IResource;
|
|
schemaResource: IResource;
|
|
activeTabBarItem?: MobileRouteItem;
|
|
activeTabItem?: MobileRouteItem;
|
|
api: APIClient;
|
|
}
|
|
MobileRoutesContext.displayName = 'MobileRoutesContext';
|
|
|
|
export const useMobileRoutes = () => {
|
|
return useContext(MobileRoutesContext);
|
|
};
|
|
|
|
function useActiveTabBar(routeList: MobileRouteItem[]) {
|
|
const { pathname } = useLocation();
|
|
const urlMap = routeList.reduce<Record<string, MobileRouteItem>>((map, item) => {
|
|
const url = item.schemaUid ? `/${item.type}/${item.schemaUid}` : item.options?.url;
|
|
if (url) {
|
|
map[url] = item;
|
|
}
|
|
if (item.children) {
|
|
item.children.forEach((child) => {
|
|
const childUrl = child.schemaUid ? `${url}/${child.type}/${child.schemaUid}` : child.options?.url;
|
|
if (childUrl) {
|
|
map[childUrl] = child;
|
|
}
|
|
});
|
|
}
|
|
return map;
|
|
}, {});
|
|
const activeTabBarItem = Object.values(urlMap).find((item) => {
|
|
if (item.schemaUid) {
|
|
return pathname.includes(`/${item.schemaUid}`);
|
|
}
|
|
if (item.options.url) {
|
|
return pathname.includes(item.options.url);
|
|
}
|
|
return false;
|
|
});
|
|
|
|
return {
|
|
activeTabBarItem, // 第一层
|
|
activeTabItem: urlMap[pathname] || activeTabBarItem, // 任意层
|
|
};
|
|
}
|
|
|
|
function useTitle(activeTabBar: MobileRouteItem) {
|
|
const context = useMobileTitle();
|
|
useEffect(() => {
|
|
if (!context) return;
|
|
if (activeTabBar) {
|
|
context.setTitle(activeTabBar.title);
|
|
document.title = activeTabBar.title;
|
|
}
|
|
}, [activeTabBar, context]);
|
|
}
|
|
|
|
export const MobileRoutesProvider: FC<{
|
|
/**
|
|
* list: return all route data, and only administrators can access;
|
|
* listAccessible: return the route data that the current user can access;
|
|
*
|
|
* @default 'listAccessible'
|
|
*/
|
|
action?: 'list' | 'listAccessible';
|
|
refreshRef?: any;
|
|
manual?: boolean;
|
|
}> = ({ children, refreshRef, manual, action = 'listAccessible' }) => {
|
|
const api = useAPIClient();
|
|
const resource = useMemo(() => api.resource('mobileRoutes'), [api]);
|
|
const schemaResource = useMemo(() => api.resource('uiSchemas'), [api]);
|
|
const {
|
|
data,
|
|
runAsync: refresh,
|
|
loading,
|
|
} = useRequest<{ data: MobileRouteItem[] }>(
|
|
() => resource[action]({ tree: true, sort: 'sort' }).then((res) => res.data),
|
|
{
|
|
manual,
|
|
},
|
|
);
|
|
|
|
if (refreshRef) {
|
|
refreshRef.current = refresh;
|
|
}
|
|
|
|
const routeList = useMemo(() => data?.data || [], [data]);
|
|
const { activeTabBarItem, activeTabItem } = useActiveTabBar(routeList);
|
|
|
|
useTitle(activeTabBarItem);
|
|
|
|
const value = useMemo(
|
|
() => ({ api, activeTabBarItem, activeTabItem, routeList, refresh, resource, schemaResource }),
|
|
[activeTabBarItem, activeTabItem, api, refresh, resource, routeList, schemaResource],
|
|
);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div data-testid="mobile-loading" style={{ textAlign: 'center', margin: '20px 0' }}>
|
|
<Spin />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <MobileRoutesContext.Provider value={value}>{children}</MobileRoutesContext.Provider>;
|
|
};
|