From ffc298238084188d79ae6d6cb92d815223afda1f Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Sat, 21 Sep 2024 23:04:26 +0800 Subject: [PATCH] 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 --- .../relation-repository.ts | 9 +- .../plugin-acl/src/client/ACLSettingsUI.tsx | 11 +- .../src/client/__e2e__/menu.test.ts | 4 +- .../src/client/permissions/Permissions.tsx | 11 +- .../plugin-acl/src/locale/en-US.json | 3 +- .../plugin-acl/src/locale/ko_KR.json | 3 +- .../plugin-acl/src/locale/zh-CN.json | 2 +- .../plugin-data-source-manager/package.json | 3 +- .../src/client/index.tsx | 4 +- .../@nocobase/plugin-mobile/package.json | 11 +- .../src/client/MenuPermissions.tsx | 278 ++++++++++++++++++ .../src/client/ShowTipWhenNoPages.tsx | 35 +++ .../src/client/__e2e__/permissions.test.ts | 118 ++++++++ .../src/client/demos/Mobile-basic.tsx | 2 +- .../demos/MobileRoutesProvider-basic.tsx | 62 ++-- .../src/client/demos/MobileTabBar-basic.tsx | 2 +- .../src/client/demos/MobileTabBar-false.tsx | 2 +- .../client/demos/MobileTabBar-inner-page.tsx | 2 +- .../demos/MobileTabBar.Link-settings.tsx | 2 +- .../demos/MobileTabBar.Page-settings.tsx | 2 +- .../demos/pages-dynamic-page-schema.tsx | 2 +- .../src/client/demos/pages-home-basic.tsx | 2 +- .../src/client/demos/pages-home-custom.tsx | 4 +- .../src/client/demos/pages-home-null.tsx | 2 +- .../client/demos/pages-page-content-404.tsx | 4 +- .../client/demos/pages-page-content-basic.tsx | 4 +- .../demos/pages-page-content-first-route.tsx | 4 +- .../client/demos/pages-page-tabs-false.tsx | 8 +- .../src/client/demos/pages-page-tabs.tsx | 8 +- .../src/client/desktop-mode/Content.tsx | 8 +- .../plugin-mobile/src/client/index.tsx | 32 ++ .../plugin-mobile/src/client/locale.ts | 2 +- .../mobile-tab-bar/MobileTabBar.tsx | 7 +- .../mobile-layout/mobile-tab-bar/styles.ts | 1 + .../mobile-providers/MobileProviders.tsx | 6 +- .../mobile-providers/context/MobileRoutes.tsx | 26 +- .../plugin-mobile/src/locale/en-US.json | 5 +- .../plugin-mobile/src/locale/zh-CN.json | 5 +- .../src/server/collections/extendRoleField.ts | 27 ++ .../src/server/collections/mobileRoutes.ts | 7 + .../202409131546-set-mobileMenuUiSchemas.ts | 46 +++ .../plugin-mobile/src/server/plugin.ts | 70 ++++- packages/presets/nocobase/package.json | 1 + 43 files changed, 753 insertions(+), 94 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/MenuPermissions.tsx create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/ShowTipWhenNoPages.tsx create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/__e2e__/permissions.test.ts create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/server/collections/extendRoleField.ts create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/server/migrations/202409131546-set-mobileMenuUiSchemas.ts diff --git a/packages/core/database/src/relation-repository/relation-repository.ts b/packages/core/database/src/relation-repository/relation-repository.ts index f7ddd1363d..df933c0019 100644 --- a/packages/core/database/src/relation-repository/relation-repository.ts +++ b/packages/core/database/src/relation-repository/relation-repository.ts @@ -115,7 +115,7 @@ export abstract class RelationRepository { convertTk(options: any) { let tk = options; - if (typeof options === 'object' && options['tk']) { + if (typeof options === 'object' && 'tk' in options) { tk = options['tk']; } return tk; @@ -126,7 +126,12 @@ export abstract class RelationRepository { if (typeof tk === 'string') { tk = tk.split(','); } - return lodash.castArray(tk); + + if (tk) { + return lodash.castArray(tk); + } + + return []; } targetKey() { diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/ACLSettingsUI.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/ACLSettingsUI.tsx index ec6cc22b23..e7b5d4abde 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/client/ACLSettingsUI.tsx +++ b/packages/plugins/@nocobase/plugin-acl/src/client/ACLSettingsUI.tsx @@ -9,6 +9,7 @@ import { TabsProps } from 'antd/es/tabs/index'; import React from 'react'; +import { TFunction } from 'react-i18next'; import { GeneralPermissions } from './permissions/GeneralPermissions'; import { MenuItemsProvider } from './permissions/MenuItemsProvider'; import { MenuPermissions } from './permissions/MenuPermissions'; @@ -22,11 +23,15 @@ interface PermissionsTabsProps { /** * the currently selected role */ - role: Role; + activeRole: null | Role; + /** + * the current user's role + */ + currentUserRole: null | Role; /** * translation function */ - t: (key: string) => string; + t: TFunction; /** * used to constrain the size of the container in the Tab */ @@ -53,7 +58,7 @@ export class ACLSettingsUI { }), ({ activeKey, t, TabLayout }) => ({ key: 'menu', - label: t('Menu'), + label: t('Desktop menu'), children: ( diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts index cbcfb67da5..85f43bb6fb 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts @@ -37,7 +37,7 @@ test('menu permission ', async ({ page, mockPage, mockRole, updateRole }) => { .locator('span') .nth(1) .click(); - await page.getByRole('tab').getByText('Menu').click(); + await page.getByRole('tab').getByText('Desktop menu').click(); await page.waitForTimeout(1000); await expect(page.getByRole('row', { name: 'page1' }).locator('.ant-checkbox-input').last()).toBeChecked({ checked: true, @@ -57,7 +57,7 @@ test('menu permission ', async ({ page, mockPage, mockRole, updateRole }) => { .locator('span') .nth(1) .click(); - await page.getByRole('tab').getByText('Menu').click(); + await page.getByRole('tab').getByText('Desktop menu').click(); await page.waitForTimeout(1000); await expect(page.getByRole('row', { name: 'page1' }).locator('.ant-checkbox-input').last()).toBeChecked({ checked: false, diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/Permissions.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/Permissions.tsx index d286a68006..878b6f7b9b 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/Permissions.tsx +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/Permissions.tsx @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { useAPIClient, usePlugin, useRequest } from '@nocobase/client'; +import { useACLRoleContext, useAPIClient, usePlugin, useRequest } from '@nocobase/client'; import { Tabs } from 'antd'; import React, { useContext, useEffect, useMemo } from 'react'; import PluginACLClient from '..'; @@ -24,10 +24,13 @@ export const Permissions: React.FC<{ active: boolean }> = ({ active }) => { const [activeKey, setActiveKey] = React.useState('general'); const { role, setRole } = useContext(RolesManagerContext); const pluginACLClient = usePlugin(PluginACLClient); - + const currentUserRole = useACLRoleContext(); const items = useMemo( - () => pluginACLClient.settingsUI.getPermissionsTabs({ t, activeKey, TabLayout, role }), - [activeKey, pluginACLClient.settingsUI, role, t], + () => + pluginACLClient.settingsUI + .getPermissionsTabs({ t, activeKey, TabLayout, activeRole: role, currentUserRole }) + .filter(Boolean), + [activeKey, pluginACLClient.settingsUI, role, t, currentUserRole], ); const api = useAPIClient(); diff --git a/packages/plugins/@nocobase/plugin-acl/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-acl/src/locale/en-US.json index 5b1313d5d4..500381f771 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/locale/en-US.json +++ b/packages/plugins/@nocobase/plugin-acl/src/locale/en-US.json @@ -2,5 +2,6 @@ "The current user has no roles. Please try another account.": "The current user has no roles. Please try another account.", "The user role does not exist. Please try signing in again": "The user role does not exist. Please try signing in again", "New role": "New role", - "Permissions": "Permissions" + "Permissions": "Permissions", + "Desktop menu": "Desktop menu" } diff --git a/packages/plugins/@nocobase/plugin-acl/src/locale/ko_KR.json b/packages/plugins/@nocobase/plugin-acl/src/locale/ko_KR.json index f8cff3ebb3..e3ac5590f2 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/locale/ko_KR.json +++ b/packages/plugins/@nocobase/plugin-acl/src/locale/ko_KR.json @@ -1,4 +1,5 @@ { "The current user has no roles. Please try another account.": "현재 사용자에게 역할이 없습니다. 다른 계정을 시도해주세요.", - "The user role does not exist. Please try signing in again": "사용자 역할이 존재하지 않습니다. 다시 로그인을 시도해주세요." + "The user role does not exist. Please try signing in again": "사용자 역할이 존재하지 않습니다. 다시 로그인을 시도해주세요.", + "Desktop menu": "데스크톱 메뉴" } diff --git a/packages/plugins/@nocobase/plugin-acl/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-acl/src/locale/zh-CN.json index 0a13115b3d..0e3432e017 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-acl/src/locale/zh-CN.json @@ -5,7 +5,7 @@ "Permissions": "权限", "Roles & Permissions": "角色和权限", "General": "通用", - "Menu": "菜单", + "Desktop menu": "桌面端菜单", "Plugin settings": "插件设置", "Data sources": "数据源" } diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/package.json b/packages/plugins/@nocobase/plugin-data-source-manager/package.json index 6ae30a3bd2..f28926b37c 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/package.json +++ b/packages/plugins/@nocobase/plugin-data-source-manager/package.json @@ -12,7 +12,8 @@ "@nocobase/client": "1.x", "@nocobase/plugin-acl": "1.x", "@nocobase/server": "1.x", - "@nocobase/test": "1.x" + "@nocobase/test": "1.x", + "@nocobase/plugin-acl": "1.x" }, "keywords": [ "Data model tools" diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/index.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/index.tsx index 770f4b582c..50da2caef2 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/index.tsx @@ -35,12 +35,12 @@ export class PluginDataSourceManagerClient extends Plugin { async load() { // register a configuration item in the Users & Permissions management page - this.app.pm.get(PluginACLClient).settingsUI.addPermissionsTab(({ t, TabLayout, role }) => ({ + this.app.pm.get(PluginACLClient).settingsUI.addPermissionsTab(({ t, TabLayout, activeRole }) => ({ key: 'dataSource', label: t('Data sources'), children: ( - + ), })); diff --git a/packages/plugins/@nocobase/plugin-mobile/package.json b/packages/plugins/@nocobase/plugin-mobile/package.json index 869253975c..33600c2a9a 100644 --- a/packages/plugins/@nocobase/plugin-mobile/package.json +++ b/packages/plugins/@nocobase/plugin-mobile/package.json @@ -12,7 +12,8 @@ "peerDependencies": { "@nocobase/client": "1.x", "@nocobase/server": "1.x", - "@nocobase/test": "1.x" + "@nocobase/test": "1.x", + "@nocobase/plugin-acl": "1.x" }, "devDependencies": { "@ant-design/icons": "5.x", @@ -23,6 +24,12 @@ "@types/react-dom": "17.x", "antd-mobile": "^5.36.1", "re-resizable": "6.6.0", - "react-device-detect": "2.2.3" + "react-device-detect": "2.2.3", + "@emotion/css": "11.x", + "ahooks": "3.x", + "lodash": "4.x", + "@formily/core": "2.x", + "antd": "5.x", + "react-i18next": "11.x" } } diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/MenuPermissions.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/MenuPermissions.tsx new file mode 100644 index 0000000000..1d036a2f35 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/MenuPermissions.tsx @@ -0,0 +1,278 @@ +/** + * 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 { css } from '@emotion/css'; +import { createForm, Form, onFormValuesChange } from '@formily/core'; +import { uid } from '@formily/shared'; +import { SchemaComponent, useAPIClient, useRequest } from '@nocobase/client'; +import { RolesManagerContext } from '@nocobase/plugin-acl/client'; +import { useMemoizedFn } from 'ahooks'; +import { Checkbox, message, Table } from 'antd'; +import _, { uniq } from 'lodash'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MobileRoutesProvider, useMobileRoutes } from './mobile-providers'; + +interface MenuItem { + title: string; + id: number; + children?: MenuItem[]; + parent?: MenuItem; +} + +const style = css` + .ant-table-cell { + > .ant-space-horizontal { + .ant-space-item-split:has(+ .ant-space-item:empty) { + display: none; + } + } + } +`; + +const translateTitle = (menus: any[], t) => { + return menus.map((menu) => { + const title = t(menu.title); + if (menu.children) { + return { + ...menu, + title, + children: translateTitle(menu.children, t), + }; + } + return { + ...menu, + title, + }; + }); +}; + +const findIDList = (items) => { + if (!Array.isArray(items)) { + return []; + } + const IDList = []; + for (const item of items) { + IDList.push(item.id); + if (item.hideChildren && !_.isNil(item.firstTabId)) { + IDList.push(item.firstTabId); + } + IDList.push(...findIDList(item.children)); + } + return IDList; +}; + +const toItems = (items, parent?: MenuItem): MenuItem[] => { + if (!Array.isArray(items)) { + return []; + } + + return items.map((item) => { + const children = toItems(item.children, item); + const hideChildren = children.length <= 1; + + return { + title: item.title, + id: item.id, + children: hideChildren ? null : children, + hideChildren, + firstTabId: children[0]?.id, + parent, + }; + }); +}; + +export const MenuPermissions: React.FC<{ + active: boolean; +}> = ({ active }) => { + const { routeList } = useMobileRoutes(); + const items = toItems(routeList); + const { role, setRole } = useContext(RolesManagerContext); + const api = useAPIClient(); + const { t } = useTranslation(); + const allIDList = findIDList(items); + const [IDList, setIDList] = useState([]); + const { loading, refresh } = useRequest( + { + resource: 'roles.mobileRoutes', + resourceOf: role.name, + action: 'list', + params: { + paginate: false, + }, + }, + { + ready: !!role && active, + refreshDeps: [role?.name], + onSuccess(data) { + setIDList(data?.data?.map((item) => item['id']) || []); + }, + }, + ); + const resource = api.resource('roles.mobileRoutes', role.name); + const allChecked = allIDList.length === IDList.length; + + const handleChange = async (checked, menuItem) => { + if (checked) { + let newIDList = IDList.filter((id) => id !== menuItem.id); + const shouldRemove = [menuItem.id]; + + if (menuItem.parent) { + const selectedChildren = menuItem.parent.children.filter((item) => newIDList.includes(item.id)); + if (selectedChildren.length === 0) { + newIDList = newIDList.filter((id) => id !== menuItem.parent.id); + shouldRemove.push(menuItem.parent.id); + } + } + + if (menuItem.children) { + newIDList = newIDList.filter((id) => !menuItem.children.map((item) => item.id).includes(id)); + shouldRemove.push(...menuItem.children.map((item) => item.id)); + } + + if (menuItem.hideChildren && !_.isNil(menuItem.firstTabId)) { + shouldRemove.push(menuItem.firstTabId); + newIDList = newIDList.filter((id) => id !== menuItem.firstTabId); + } + + setIDList(newIDList); + await resource.remove({ + values: shouldRemove, + }); + } else { + const newIDList = [...IDList, menuItem.id]; + const shouldAdd = [menuItem.id]; + + if (menuItem.parent) { + if (!newIDList.includes(menuItem.parent.id)) { + newIDList.push(menuItem.parent.id); + shouldAdd.push(menuItem.parent.id); + } + } + + if (menuItem.children) { + const childrenIDList = menuItem.children.map((item) => item.id); + newIDList.push(...childrenIDList); + shouldAdd.push(...childrenIDList); + } + + if (menuItem.hideChildren && !_.isNil(menuItem.firstTabId)) { + shouldAdd.push(menuItem.firstTabId); + newIDList.push(menuItem.firstTabId); + } + + setIDList(uniq(newIDList)); + await resource.add({ + values: shouldAdd, + }); + } + message.success(t('Saved successfully')); + }; + + const update = useMemoizedFn(async (form: Form) => { + await api.resource('roles').update({ + filterByTk: role.name, + values: form.values, + }); + setRole({ ...role, ...form.values }); + message.success(t('Saved successfully')); + }); + const form = useMemo(() => { + return createForm({ + values: role, + effects() { + onFormValuesChange(async (form) => { + await update(form); + }); + }, + }); + }, [role, update]); + return ( + <> + + + { + if (allChecked) { + await resource.set({ + values: [], + }); + } else { + await resource.set({ + values: allIDList, + }); + } + refresh(); + message.success(t('Saved successfully')); + }} + />{' '} + {t('Accessible')} + + ), + render: (_, schema) => { + const checked = IDList.includes(schema.id); + return handleChange(checked, schema)} />; + }, + }, + ]} + dataSource={translateTitle(items, t)} + /> + + ); +}; + +export const MobileAllRoutesProvider: React.FC<{ active: boolean }> = ({ children, active }) => { + const refreshRef = React.useRef(() => {}); + + useEffect(() => { + if (active) { + refreshRef.current?.(); + } + }, [active]); + + return ( + + {children} + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/ShowTipWhenNoPages.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/ShowTipWhenNoPages.tsx new file mode 100644 index 0000000000..afbe8c7b9a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/ShowTipWhenNoPages.tsx @@ -0,0 +1,35 @@ +/** + * 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 { useDesignable } from '@nocobase/client'; +import { ErrorBlock } from 'antd-mobile'; +import _ from 'lodash'; +import React, { FC } from 'react'; +import { isMobile } from 'react-device-detect'; +import { usePluginTranslation } from './locale'; +import { useMobileRoutes } from './mobile-providers/context'; + +export const ShowTipWhenNoPages: FC = ({ children }) => { + const { designable } = useDesignable(); + const { routeList } = useMobileRoutes(); + const { t } = usePluginTranslation(); + + if ((!designable || isMobile) && _.isEmpty(routeList)) { + return ( + + ); + } + + return <>{children}; +}; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/__e2e__/permissions.test.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/__e2e__/permissions.test.ts new file mode 100644 index 0000000000..a8d0811ba7 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/__e2e__/permissions.test.ts @@ -0,0 +1,118 @@ +/** + * 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 { expect, Page, test } from '@nocobase/test/e2e'; + +const createNewPage = async (title: string, page: Page) => { + await page.getByTestId('schema-initializer-MobileTabBar').hover(); + await page.getByRole('menuitem', { name: 'Page' }).click(); + await page.getByLabel('block-item-Input-Title').getByRole('textbox').fill(title); + await page.getByRole('button', { name: 'Select icon' }).click(); + await page.getByRole('tooltip').getByLabel('account-book').locator('svg').click(); + await page.getByLabel('action-Action-Submit').click(); +}; + +const createNewTab = async (title: string, page: Page) => { + await page.getByLabel('action-Action-undefined').click(); + await page.getByLabel('block-item-Input-Title').getByRole('textbox').fill(title); + await page.getByLabel('action-Action-Submit').click(); +}; + +const deletePage = async (title: string, page: Page) => { + await page.getByLabel('block-item-MobileTabBar.Page').filter({ hasText: title }).hover(); + await page.getByTestId('mobile-tab-bar').getByRole('button', { name: 'designer-schema-settings-' }).hover(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); +}; + +test.describe('mobile permissions', () => { + test('menu permission ', async ({ page }) => { + await page.goto('/m'); + + // by default, the user's role is root, which has all permissions + // create a new page + await createNewPage('root', page); + + // expect: the new page should be visible + await expect(page.getByLabel('block-item-MobileTabBar.Page').filter({ hasText: 'root' })).toBeVisible(); + + // change the user's role to admin + await page.evaluate(() => { + window.localStorage.setItem('NOCOBASE_ROLE', 'admin'); + }); + await page.reload(); + await createNewPage('admin', page); + + await page.getByLabel('block-item-MobilePageProvider').hover(); + await page.getByLabel('designer-schema-settings-MobilePageProvider-mobile:page').hover(); + await page.getByRole('menuitem', { name: 'Display tabs' }).getByRole('switch').check(); + + // create a new tab + await createNewTab('tab456', page); + await expect(page.getByRole('tab', { name: 'tab456', exact: true })).toBeVisible(); + + // change tab title 'Unnamed' to 'tab123' + await page.getByRole('tab', { name: 'Unnamed', exact: true }).hover(); + await page.getByTestId('mobile-page-tabs-Unnamed').getByLabel('designer-schema-settings-').hover(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByLabel('block-item-Input-Title').getByRole('textbox').fill('tab123'); + await page.getByRole('button', { name: 'Submit' }).click(); + + // expect: the new page should be visible + await expect(page.getByLabel('block-item-MobileTabBar.Page').filter({ hasText: 'admin' })).toBeVisible(); + + // change the user's role to admin, and then change the menu permission + await page.evaluate(() => { + window.localStorage.setItem('NOCOBASE_ROLE', 'admin'); + }); + await page.reload(); + await page.goto('/admin/settings/users-permissions/roles'); + await page.getByRole('tab', { name: 'Mobile menu' }).click(); + await page.getByRole('row', { name: 'Collapse row admin' }).getByLabel('', { exact: true }).uncheck(); + // the children of the admin tabs should be unchecked + await expect(page.getByRole('row', { name: 'tab123' }).getByLabel('', { exact: true })).toBeChecked({ + checked: false, + }); + await expect(page.getByRole('row', { name: 'tab456' }).getByLabel('', { exact: true })).toBeChecked({ + checked: false, + }); + + // the admin tab should be checked when the child is checked + await page.getByRole('row', { name: 'tab123' }).getByLabel('', { exact: true }).check(); + await expect(page.getByRole('row', { name: 'Collapse row admin' }).getByLabel('', { exact: true })).toBeChecked({ + checked: true, + }); + + // the admin tab should be unchecked when the children is all unchecked + await page.getByRole('row', { name: 'tab123' }).getByLabel('', { exact: true }).uncheck(); + await expect(page.getByRole('row', { name: 'Collapse row admin' }).getByLabel('', { exact: true })).toBeChecked({ + checked: false, + }); + + // to the mobile, the admin page should be hidden + await page.goto('/m'); + await expect(page.getByLabel('block-item-MobileTabBar.Page').filter({ hasText: 'admin' })).not.toBeVisible(); + + // go back to the configuration page, and check one child of the admin + await page.goto('/admin/settings/users-permissions/roles'); + await page.getByRole('tab', { name: 'Mobile menu' }).click(); + await page.getByRole('row', { name: 'tab123' }).getByLabel('', { exact: true }).check(); + + // to the mobile, the admin page should be visible, and the tab123 should be visible, and the tab456 should be hidden + await page.goto('/m'); + await expect(page.getByLabel('block-item-MobileTabBar.Page').filter({ hasText: 'admin' })).toBeVisible(); + await page.getByLabel('block-item-MobileTabBar.Page').filter({ hasText: 'admin' }).click(); + await expect(page.getByRole('tab', { name: 'tab123', exact: true })).toBeVisible(); + await expect(page.getByRole('tab', { name: 'tab456', exact: true })).not.toBeVisible(); + + // delete the pages + await deletePage('root', page); + await deletePage('admin', page); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/Mobile-basic.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/Mobile-basic.tsx index 75623efac1..fc650a8399 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/Mobile-basic.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/Mobile-basic.tsx @@ -28,7 +28,7 @@ const app = mockApp({ }, plugins: [DemoPlugin], apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { id: 10, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx index 44d0d4d88b..615b399a9d 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx @@ -1,12 +1,12 @@ -import React from 'react'; import { Plugin } from '@nocobase/client'; import { mockApp } from '@nocobase/client/demo-utils'; import { - MobileTitleProvider, - useMobileTitle, MobileRoutesProvider, + MobileTitleProvider, useMobileRoutes, + useMobileTitle, } from '@nocobase/plugin-mobile/client'; +import React from 'react'; const InnerPage = () => { const { routeList } = useMobileRoutes(); @@ -43,39 +43,37 @@ const app = mockApp({ }, plugins: [DemoPlugin], apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { - "id": 10, - "createdAt": "2024-07-08T13:22:33.763Z", - "updatedAt": "2024-07-08T13:22:33.763Z", - "parentId": null, - "title": "Test1", - "icon": "AppstoreOutlined", - "schemaUid": "test", - "type": "page", - "options": null, - "sort": 1, - "createdById": 1, - "updatedById": 1, + id: 10, + createdAt: '2024-07-08T13:22:33.763Z', + updatedAt: '2024-07-08T13:22:33.763Z', + parentId: null, + title: 'Test1', + icon: 'AppstoreOutlined', + schemaUid: 'test', + type: 'page', + options: null, + sort: 1, + createdById: 1, + updatedById: 1, }, { - "id": 13, - "createdAt": "2024-07-08T13:23:01.929Z", - "updatedAt": "2024-07-08T13:23:12.433Z", - "parentId": null, - "title": "Test2", - "icon": "aliwangwangoutlined", - "schemaUid": null, - "type": "link", - "options": { - "schemaUid": null, - "url": "https://github.com", - "params": [ - {} - ] - } - } + id: 13, + createdAt: '2024-07-08T13:23:01.929Z', + updatedAt: '2024-07-08T13:23:12.433Z', + parentId: null, + title: 'Test2', + icon: 'aliwangwangoutlined', + schemaUid: null, + type: 'link', + options: { + schemaUid: null, + url: 'https://github.com', + params: [{}], + }, + }, ], }, }, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-basic.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-basic.tsx index 0487c29a05..f573204375 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-basic.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-basic.tsx @@ -38,7 +38,7 @@ const app = mockApp({ 'applicationPlugins:update': { data: {}, }, - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { id: 10, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-false.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-false.tsx index 7904e1b0e2..404cffbed1 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-false.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-false.tsx @@ -38,7 +38,7 @@ const app = mockApp({ 'applicationPlugins:update': { data: {}, }, - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { id: 10, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-inner-page.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-inner-page.tsx index 0cdcdc85c0..f017cd8a5c 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-inner-page.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar-inner-page.tsx @@ -43,7 +43,7 @@ const app = mockApp({ 'applicationPlugins:update': { data: {}, }, - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { id: 10, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx index 773aebd89b..f0e93fd9a2 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx @@ -48,7 +48,7 @@ const app = mockApp({ schemaSettings: [mobileTabBarLinkSettings], designable: true, apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [], }, 'mobileRoutes:update': { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-settings.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-settings.tsx index b67d470a9c..74c331b826 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-settings.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-settings.tsx @@ -46,7 +46,7 @@ const app = mockApp({ schemaSettings: [mobileTabBarPageSettings], designable: true, apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [], }, 'mobileRoutes:update': { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-dynamic-page-schema.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-dynamic-page-schema.tsx index 5652c26c63..953e9b5c8e 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-dynamic-page-schema.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-dynamic-page-schema.tsx @@ -45,7 +45,7 @@ const app = mockApp({ }, designable: true, apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { id: 1, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-basic.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-basic.tsx index 3b21f16ccf..8404e953cc 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-basic.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-basic.tsx @@ -28,7 +28,7 @@ const app = mockApp({ }, plugins: [DemoPlugin], apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { id: 28, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-custom.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-custom.tsx index eff0c83bb4..202961cfd2 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-custom.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-custom.tsx @@ -2,8 +2,8 @@ * iframe: true */ import { Plugin } from '@nocobase/client'; -import PluginMobileClient, { Mobile } from '@nocobase/plugin-mobile/client'; import { mockApp } from '@nocobase/client/demo-utils'; +import PluginMobileClient, { Mobile } from '@nocobase/plugin-mobile/client'; import React from 'react'; class DemoPlugin extends Plugin { @@ -40,7 +40,7 @@ const app = mockApp({ DemoPlugin, ], apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { id: '1', diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-null.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-null.tsx index 13cee4def3..5febd80078 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-null.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-home-null.tsx @@ -25,7 +25,7 @@ const app = mockApp({ }, plugins: [DemoPlugin], apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [], }, }, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-404.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-404.tsx index 73030d6382..d88839f272 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-404.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-404.tsx @@ -1,6 +1,6 @@ -import React from 'react'; import { Plugin } from '@nocobase/client'; import { MobileNotFoundPage, MobilePageContent, MobileRoutesProvider } from '@nocobase/plugin-mobile/client'; +import React from 'react'; import { mockApp } from '@nocobase/client/demo-utils'; @@ -40,7 +40,7 @@ const app = mockApp({ }, plugins: [DemoPlugin], apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [], }, 'uiSchemas:getJsonSchema/tab1': { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-basic.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-basic.tsx index 4c615a945e..04b7f4d0c1 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-basic.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-basic.tsx @@ -1,6 +1,6 @@ -import React from 'react'; import { Plugin } from '@nocobase/client'; import { MobilePageContent, MobileRoutesProvider } from '@nocobase/plugin-mobile/client'; +import React from 'react'; import { mockApp } from '@nocobase/client/demo-utils'; @@ -37,7 +37,7 @@ const app = mockApp({ }, plugins: [DemoPlugin], apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [], }, 'uiSchemas:getJsonSchema/tab1': { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-first-route.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-first-route.tsx index daea74ce56..231bbf51dd 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-first-route.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-content-first-route.tsx @@ -1,6 +1,6 @@ -import React from 'react'; import { Plugin } from '@nocobase/client'; import { MobilePageContent, MobileRoutesProvider } from '@nocobase/plugin-mobile/client'; +import React from 'react'; import { mockApp } from '@nocobase/client/demo-utils'; @@ -31,7 +31,7 @@ const app = mockApp({ }, plugins: [DemoPlugin], apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { id: 28, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs-false.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs-false.tsx index d37fad8e09..e7f88f2f12 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs-false.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs-false.tsx @@ -1,7 +1,5 @@ -import React from 'react'; -import { useLocation } from 'react-router-dom'; +import { Plugin } from '@nocobase/client'; import { mockApp } from '@nocobase/client/demo-utils'; -import { SchemaComponent, Plugin } from '@nocobase/client'; import { MobilePageNavigationBar, MobilePageProvider, @@ -9,6 +7,8 @@ import { MobileRoutesProvider, MobileTitleProvider, } from '@nocobase/plugin-mobile/client'; +import React from 'react'; +import { useLocation } from 'react-router-dom'; const Demo = () => { const { pathname } = useLocation(); @@ -51,7 +51,7 @@ const app = mockApp({ }, plugins: [DemoPlugin], apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { id: 1, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs.tsx index 702dcbb98c..b8f159cc3b 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs.tsx @@ -1,7 +1,5 @@ -import React from 'react'; -import { useLocation } from 'react-router-dom'; +import { Plugin } from '@nocobase/client'; import { mockApp } from '@nocobase/client/demo-utils'; -import { SchemaComponent, Plugin } from '@nocobase/client'; import { MobilePageNavigationBar, MobilePageProvider, @@ -9,6 +7,8 @@ import { MobileRoutesProvider, MobileTitleProvider, } from '@nocobase/plugin-mobile/client'; +import React from 'react'; +import { useLocation } from 'react-router-dom'; const Demo = () => { const { pathname } = useLocation(); @@ -52,7 +52,7 @@ const app = mockApp({ }, plugins: [DemoPlugin], apis: { - 'mobileRoutes:list': { + 'mobileRoutes:listAccessible': { data: [ { id: 1, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/desktop-mode/Content.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/desktop-mode/Content.tsx index a684e7ff44..8f1e83ac95 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/desktop-mode/Content.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/desktop-mode/Content.tsx @@ -7,8 +7,9 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React, { FC } from 'react'; +import { css } from '@emotion/css'; import { Resizable } from 're-resizable'; +import React, { FC } from 'react'; import { useSize } from './sizeContext'; interface DesktopModeContentProps { @@ -28,6 +29,11 @@ export const DesktopModeContent: FC = ({ children }) => overflow: 'auto', padding: 80, }} + className={css` + .adm-error-block-full-page { + padding-top: 100px; + } + `} > { + if ( + currentUserRole && + ((!currentUserRole.snippets.includes('pm.mobile') && !currentUserRole.snippets.includes('pm.*')) || + currentUserRole.snippets.includes('!pm.mobile')) + ) { + return null; + } + + return { + key: 'mobile-menu', + label: t('Mobile menu', { + ns: pkg.name, + }), + children: ( + + + + + + ), + }; + }); + } } export default PluginMobileClient; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/locale.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/locale.ts index 1a2233ddde..ac61f76b75 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/locale.ts +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/locale.ts @@ -7,9 +7,9 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { useTranslation } from 'react-i18next'; // @ts-ignore import pkg from './../../package.json'; -import { useTranslation } from 'react-i18next'; export function usePluginTranslation() { return useTranslation([pkg.name, 'client'], { nsMode: 'fallback' }); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.tsx index 46184dfd42..41d7012378 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.tsx @@ -69,9 +69,9 @@ export const MobileTabBar: FC & { styles.mobileTabBarList, css({ maxWidth: designable ? 'calc(100% - 58px)' : '100%', - '.nb-block-item': { - maxWidth: `${100 / routeList.length}%`, - }, + // '.nb-block-item': { + // maxWidth: `${100 / routeList.length}%`, + // }, }), )} > @@ -82,7 +82,6 @@ export const MobileTabBar: FC & { - ); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/styles.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/styles.ts index b1ec925057..8ea9531cbb 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/styles.ts +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/styles.ts @@ -33,6 +33,7 @@ export const useStyles = createStyles(() => ({ justifyContent: 'space-around', flex: 1, alignItems: 'center', + overflowX: 'auto', '.adm-tab-bar-item': { maxWidth: '100%', '.adm-tab-bar-item-title': { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/MobileProviders.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/MobileProviders.tsx index d048f29fba..0e9196ffc6 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/MobileProviders.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/MobileProviders.tsx @@ -8,7 +8,7 @@ */ import React, { FC, useEffect } from 'react'; - +import { ShowTipWhenNoPages } from '../ShowTipWhenNoPages'; import { MobileRoutesProvider, MobileTitleProvider } from './context'; export interface MobileProvidersProps { @@ -23,7 +23,9 @@ export const MobileProviders: FC = ({ children }) => { return ( - {children} + + {children} + ); }; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/context/MobileRoutes.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/context/MobileRoutes.tsx index fa78f45c7c..01230c0f13 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/context/MobileRoutes.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-providers/context/MobileRoutes.tsx @@ -9,7 +9,7 @@ import { APIClient, useAPIClient, useRequest } from '@nocobase/client'; import { Spin } from 'antd'; -import React, { createContext, useContext, useEffect, useMemo } from 'react'; +import React, { createContext, FC, useContext, useEffect, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import type { IResource } from '@nocobase/sdk'; @@ -88,7 +88,17 @@ function useTitle(activeTabBar: MobileRouteItem) { }, [activeTabBar, context]); } -export const MobileRoutesProvider = ({ children }) => { +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]); @@ -96,9 +106,17 @@ export const MobileRoutesProvider = ({ children }) => { data, runAsync: refresh, loading, - } = useRequest<{ data: MobileRouteItem[] }>(() => - resource.list({ tree: true, sort: 'sort' }).then((res) => res.data), + } = 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); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-mobile/src/locale/en-US.json index 43c0b120ef..86e7e880c8 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/locale/en-US.json +++ b/packages/plugins/@nocobase/plugin-mobile/src/locale/en-US.json @@ -21,5 +21,8 @@ "Icon field is required": "Icon field is required", "Desktop data blocks": "Desktop data blocks", "Other desktop blocks": "Other desktop blocks", - "Settings": "Settings" + "Settings": "Settings", + "Mobile menu": "Mobile menu", + "No accessible pages found": "No accessible pages found", + "This might be due to permission configuration issues": "This might be due to permission configuration issues" } diff --git a/packages/plugins/@nocobase/plugin-mobile/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-mobile/src/locale/zh-CN.json index f24a8fedd5..bb762b9813 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-mobile/src/locale/zh-CN.json @@ -22,5 +22,8 @@ "Icon field is required": "图标必填", "Desktop data blocks": "桌面端数据区块", "Other desktop blocks": "其他桌面端区块", - "Settings": "设置" + "Settings": "设置", + "Mobile menu": "移动端菜单", + "No accessible pages found": "没有找到你可以访问的页面", + "This might be due to permission configuration issues": "这可能是权限配置的问题" } diff --git a/packages/plugins/@nocobase/plugin-mobile/src/server/collections/extendRoleField.ts b/packages/plugins/@nocobase/plugin-mobile/src/server/collections/extendRoleField.ts new file mode 100644 index 0000000000..cdc13b498a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/server/collections/extendRoleField.ts @@ -0,0 +1,27 @@ +/** + * 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 { extendCollection } from '@nocobase/database'; + +export default extendCollection({ + name: 'roles', + fields: [ + { + type: 'belongsToMany', + name: 'mobileRoutes', + target: 'mobileRoutes', + through: 'rolesMobileRoutes', + onDelete: 'CASCADE', + }, + { + type: 'boolean', + name: 'allowNewMobileMenu', + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/server/collections/mobileRoutes.ts b/packages/plugins/@nocobase/plugin-mobile/src/server/collections/mobileRoutes.ts index e5592e20a0..5dff896a86 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/server/collections/mobileRoutes.ts +++ b/packages/plugins/@nocobase/plugin-mobile/src/server/collections/mobileRoutes.ts @@ -294,6 +294,13 @@ export default defineCollection({ title: 'sort', }, }, + { + type: 'belongsToMany', + name: 'roles', + through: 'rolesMobileRoutes', + target: 'roles', + onDelete: 'CASCADE', + }, ], category: [], logging: true, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/server/migrations/202409131546-set-mobileMenuUiSchemas.ts b/packages/plugins/@nocobase/plugin-mobile/src/server/migrations/202409131546-set-mobileMenuUiSchemas.ts new file mode 100644 index 0000000000..14791f6a86 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/server/migrations/202409131546-set-mobileMenuUiSchemas.ts @@ -0,0 +1,46 @@ +/** + * 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. + */ + +/* istanbul ignore file -- @preserve */ + +import { Migration } from '@nocobase/server'; + +export default class extends Migration { + appVersion = '<1.4.0-beta'; + + async up() { + await this.db.sequelize.transaction(async (transaction) => { + const mobileRoutes = await this.db.getRepository('mobileRoutes').find({ + sort: 'sort', + transaction, + }); + const roles = await this.db.getRepository('roles').find({ + filter: { + 'allowNewMobileMenu.$isFalsy': true, + }, + transaction, + }); + const mobileRouteIds = mobileRoutes.map((item) => item.get('id')); + for (const role of roles) { + // 如果是 false,不处理 + if (role.allowNewMobileMenu === false) { + continue; + } + role.allowNewMobileMenu = true; + await role.save({ transaction }); + await this.db.getRepository('roles.mobileRoutes', role.get('name')).add({ + tk: mobileRouteIds, + transaction, + }); + } + }); + } + + async down() {} +} diff --git a/packages/plugins/@nocobase/plugin-mobile/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-mobile/src/server/plugin.ts index 96250b5453..d1f0f7afde 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-mobile/src/server/plugin.ts @@ -7,21 +7,83 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { Model } from '@nocobase/database'; import { Plugin } from '@nocobase/server'; export class PluginMobileServer extends Plugin { async load() { + this.registerActionHandlers(); + this.bindNewMenuToRoles(); + this.setACL(); + } + + setACL() { this.app.acl.registerSnippet({ - name: `ui.${this.name}`, + name: `ui.mobile`, actions: ['mobileRoutes:create', 'mobileRoutes:update', 'mobileRoutes:destroy'], }); this.app.acl.registerSnippet({ - name: `pm.${this.name}`, - actions: ['mobileRoutes:list'], + name: `pm.mobile`, + actions: ['mobileRoutes:list', 'roles.mobileRoutes:*'], }); - this.app.acl.allow('mobileRoutes', 'list', 'loggedIn'); + this.app.acl.allow('mobileRoutes', 'listAccessible', 'loggedIn'); + } + + /** + * used to implement: roles with permission (allowNewMobileMenu is true) can directly access the newly created menu + */ + bindNewMenuToRoles() { + this.app.db.on('roles.beforeCreate', async (instance: Model) => { + instance.set('allowNewMobileMenu', ['admin', 'member'].includes(instance.name)); + }); + this.app.db.on('mobileRoutes.afterCreate', async (instance: Model, { transaction }) => { + const addNewMenuRoles = await this.app.db.getRepository('roles').find({ + filter: { + allowNewMobileMenu: true, + }, + transaction, + }); + + // @ts-ignore + await this.app.db.getRepository('mobileRoutes.roles', instance.id).add({ + tk: addNewMenuRoles.map((role) => role.name), + transaction, + }); + }); + } + + registerActionHandlers() { + this.app.resourceManager.registerActionHandler('mobileRoutes:listAccessible', async (ctx, next) => { + const mobileRoutesRepository = ctx.db.getRepository('mobileRoutes'); + const rolesRepository = ctx.db.getRepository('roles'); + + if (ctx.state.currentRole === 'root') { + ctx.body = await mobileRoutesRepository.find({ + tree: true, + ...ctx.query, + }); + return await next(); + } + + const role = await rolesRepository.findOne({ + filterByTk: ctx.state.currentRole, + appends: ['mobileRoutes'], + }); + + const mobileRoutesId = role.get('mobileRoutes').map((item) => item.id); + + ctx.body = await mobileRoutesRepository.find({ + tree: true, + ...ctx.query, + filter: { + id: mobileRoutesId, + }, + }); + + await next(); + }); } } diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index c585423693..1f09e3b6ec 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -102,6 +102,7 @@ "@nocobase/plugin-gantt", "@nocobase/plugin-kanban", "@nocobase/plugin-logger", + "@nocobase/plugin-mobile", "@nocobase/plugin-system-settings", "@nocobase/plugin-ui-schema-storage", "@nocobase/plugin-user-data-sync",