mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
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>
This commit is contained in:
parent
c83b578905
commit
ffc2982380
@ -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() {
|
||||
|
@ -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: (
|
||||
<TabLayout>
|
||||
<MenuItemsProvider>
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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": "데스크톱 메뉴"
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
"Permissions": "权限",
|
||||
"Roles & Permissions": "角色和权限",
|
||||
"General": "通用",
|
||||
"Menu": "菜单",
|
||||
"Desktop menu": "桌面端菜单",
|
||||
"Plugin settings": "插件设置",
|
||||
"Data sources": "数据源"
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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: (
|
||||
<TabLayout>
|
||||
<DataSourcePermissionManager role={role} />
|
||||
<DataSourcePermissionManager role={activeRole} />
|
||||
</TabLayout>
|
||||
),
|
||||
}));
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
name: uid(),
|
||||
'x-component': 'FormV2',
|
||||
'x-component-props': {
|
||||
form,
|
||||
},
|
||||
properties: {
|
||||
allowNewMobileMenu: {
|
||||
title: t('Menu permissions'),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
'x-content': t('New menu items are allowed to be accessed by default.'),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Table
|
||||
className={style}
|
||||
loading={loading}
|
||||
rowKey={'id'}
|
||||
pagination={false}
|
||||
expandable={{
|
||||
defaultExpandAllRows: true,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
dataIndex: 'title',
|
||||
title: t('Menu item title'),
|
||||
},
|
||||
{
|
||||
dataIndex: 'accessible',
|
||||
title: (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={allChecked}
|
||||
onChange={async (value) => {
|
||||
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 <Checkbox checked={checked} onChange={() => 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 (
|
||||
<MobileRoutesProvider action="list" refreshRef={refreshRef} manual>
|
||||
{children}
|
||||
</MobileRoutesProvider>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<ErrorBlock
|
||||
status="empty"
|
||||
fullPage
|
||||
title={t('No accessible pages found')}
|
||||
description={t('This might be due to permission configuration issues')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
@ -28,7 +28,7 @@ const app = mockApp({
|
||||
},
|
||||
plugins: [DemoPlugin],
|
||||
apis: {
|
||||
'mobileRoutes:list': {
|
||||
'mobileRoutes:listAccessible': {
|
||||
data: [
|
||||
{
|
||||
id: 10,
|
||||
|
@ -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: [{}],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -38,7 +38,7 @@ const app = mockApp({
|
||||
'applicationPlugins:update': {
|
||||
data: {},
|
||||
},
|
||||
'mobileRoutes:list': {
|
||||
'mobileRoutes:listAccessible': {
|
||||
data: [
|
||||
{
|
||||
id: 10,
|
||||
|
@ -38,7 +38,7 @@ const app = mockApp({
|
||||
'applicationPlugins:update': {
|
||||
data: {},
|
||||
},
|
||||
'mobileRoutes:list': {
|
||||
'mobileRoutes:listAccessible': {
|
||||
data: [
|
||||
{
|
||||
id: 10,
|
||||
|
@ -43,7 +43,7 @@ const app = mockApp({
|
||||
'applicationPlugins:update': {
|
||||
data: {},
|
||||
},
|
||||
'mobileRoutes:list': {
|
||||
'mobileRoutes:listAccessible': {
|
||||
data: [
|
||||
{
|
||||
id: 10,
|
||||
|
@ -48,7 +48,7 @@ const app = mockApp({
|
||||
schemaSettings: [mobileTabBarLinkSettings],
|
||||
designable: true,
|
||||
apis: {
|
||||
'mobileRoutes:list': {
|
||||
'mobileRoutes:listAccessible': {
|
||||
data: [],
|
||||
},
|
||||
'mobileRoutes:update': {
|
||||
|
@ -46,7 +46,7 @@ const app = mockApp({
|
||||
schemaSettings: [mobileTabBarPageSettings],
|
||||
designable: true,
|
||||
apis: {
|
||||
'mobileRoutes:list': {
|
||||
'mobileRoutes:listAccessible': {
|
||||
data: [],
|
||||
},
|
||||
'mobileRoutes:update': {
|
||||
|
@ -45,7 +45,7 @@ const app = mockApp({
|
||||
},
|
||||
designable: true,
|
||||
apis: {
|
||||
'mobileRoutes:list': {
|
||||
'mobileRoutes:listAccessible': {
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
|
@ -28,7 +28,7 @@ const app = mockApp({
|
||||
},
|
||||
plugins: [DemoPlugin],
|
||||
apis: {
|
||||
'mobileRoutes:list': {
|
||||
'mobileRoutes:listAccessible': {
|
||||
data: [
|
||||
{
|
||||
id: 28,
|
||||
|
@ -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',
|
||||
|
@ -25,7 +25,7 @@ const app = mockApp({
|
||||
},
|
||||
plugins: [DemoPlugin],
|
||||
apis: {
|
||||
'mobileRoutes:list': {
|
||||
'mobileRoutes:listAccessible': {
|
||||
data: [],
|
||||
},
|
||||
},
|
||||
|
@ -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': {
|
||||
|
@ -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': {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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<DesktopModeContentProps> = ({ children }) =>
|
||||
overflow: 'auto',
|
||||
padding: 80,
|
||||
}}
|
||||
className={css`
|
||||
.adm-error-block-full-page {
|
||||
padding-top: 100px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Resizable
|
||||
style={{
|
||||
|
@ -43,11 +43,16 @@ import {
|
||||
useMobileNavigationBarLink,
|
||||
} from './pages';
|
||||
|
||||
import PluginACLClient from '@nocobase/plugin-acl/client';
|
||||
import { MenuPermissions, MobileAllRoutesProvider } from './MenuPermissions';
|
||||
|
||||
// 导出 JSBridge,会挂在到 window 上
|
||||
import './js-bridge';
|
||||
import { MobileSettings } from './mobile-blocks/settings-block/MobileSettings';
|
||||
import { MobileSettingsBlockInitializer } from './mobile-blocks/settings-block/MobileSettingsBlockInitializer';
|
||||
import { MobileSettingsBlockSchemaSettings } from './mobile-blocks/settings-block/schemaSettings';
|
||||
// @ts-ignore
|
||||
import pkg from './../../package.json';
|
||||
|
||||
export * from './desktop-mode';
|
||||
export * from './mobile';
|
||||
@ -105,6 +110,7 @@ export class PluginMobileClient extends Plugin {
|
||||
this.addInitializers();
|
||||
this.addSettings();
|
||||
this.addScopes();
|
||||
this.addPermissionsSettingsUI();
|
||||
|
||||
this.app.pluginSettingsManager.add('mobile', {
|
||||
title: generatePluginTranslationTemplate('Mobile'),
|
||||
@ -240,6 +246,32 @@ export class PluginMobileClient extends Plugin {
|
||||
getRouterComponent() {
|
||||
return this.mobileRouter.getRouterComponent();
|
||||
}
|
||||
|
||||
addPermissionsSettingsUI() {
|
||||
this.app.pm.get(PluginACLClient)?.settingsUI.addPermissionsTab(({ t, TabLayout, activeKey, currentUserRole }) => {
|
||||
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: (
|
||||
<TabLayout>
|
||||
<MobileAllRoutesProvider active={activeKey === 'mobile-menu'}>
|
||||
<MenuPermissions active={activeKey === 'mobile-menu'} />
|
||||
</MobileAllRoutesProvider>
|
||||
</TabLayout>
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginMobileClient;
|
||||
|
@ -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' });
|
||||
|
@ -69,9 +69,9 @@ export const MobileTabBar: FC<MobileTabBarProps> & {
|
||||
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<MobileTabBarProps> & {
|
||||
</DndContext>
|
||||
<MobileTabBarInitializer />
|
||||
</div>
|
||||
|
||||
<SafeArea position="bottom" />
|
||||
</div>
|
||||
);
|
||||
|
@ -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': {
|
||||
|
@ -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<MobileProvidersProps> = ({ children }) => {
|
||||
|
||||
return (
|
||||
<MobileTitleProvider>
|
||||
<MobileRoutesProvider>{children}</MobileRoutesProvider>
|
||||
<MobileRoutesProvider>
|
||||
<ShowTipWhenNoPages>{children}</ShowTipWhenNoPages>
|
||||
</MobileRoutesProvider>
|
||||
</MobileTitleProvider>
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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": "这可能是权限配置的问题"
|
||||
}
|
||||
|
@ -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',
|
||||
},
|
||||
],
|
||||
});
|
@ -294,6 +294,13 @@ export default defineCollection({
|
||||
title: 'sort',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'roles',
|
||||
through: 'rolesMobileRoutes',
|
||||
target: 'roles',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
],
|
||||
category: [],
|
||||
logging: true,
|
||||
|
@ -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<any>('roles.mobileRoutes', role.get('name')).add({
|
||||
tk: mobileRouteIds,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async down() {}
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user