Zeke Zhang ffc2982380
feat(acl): add support for configuring mobile menu permissions (#5174)
* 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>
2024-09-21 23:04:26 +08:00

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>;
};