mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +08:00
refactor: make the menu responsive to screen width (#6331)
* chore: convert routes * chore: stash * chore: stash * feat: support to add menu item * feat: add MenuSchemaToolbar * refactor: extract EditMenuItem component * feat: add hidden option * refactor: extract HiddenMenuItem * feat: add 'Move to' option * feat: add insert options * feat: remove route * fix: children * fix: route * feat: enhance menu item rendering and group handling in admin layout * feat: add container support to MenuSchemaToolbar and fix display issue in Group * fix: add conditional check before moving routes in menu item settings * feat(navigation): add default page navigation for admin layout * chore(versions): 😊 publish v1.6.0-alpha.24 * feat: export AppNotFound component and integrate 404 handling in AdminDynamicPage * fix: update admin layout route path to use item options URL * chore(versions): 😊 publish v1.6.0-alpha.25 * refactor: rename route node retrieval functions for clarity and add legacy route compatibility * refactor: clean up layout component by removing unused styles and improving header rendering * refactor: simplify menu item components by removing unused schema insertion logic and optimizing imports * refactor: add 'x-async' property to tab schema and clean up server imports * refactor: tabs * feat: support extending frontend filter operators (#6085) * feat: operator extension * fix: bug * refactor: code improve * fix: jsonLogic --------- Co-authored-by: chenos <chenlinxh@gmail.com> * feat: add fake schema for routing in SortableItem and remove unused fieldSchema import * feat: adjust content padding in InternalAdminLayout for improved layout * refactor: remove registerOperators (#6224) * refactor(plugin-workflow): trigger workflow action settings (#6143) * refactor(plugin-workflow): move bind workflow settings to plugin * refactor(plugin-block-workbench): move component to core * refactor(plugin-block-workbench): adjust component api * fix(plugin-workflow-action-trigger): fix test cases * fix(plugin-workflow): fix component scope * fix(plugin-workflow-action-trigger): fix test cases * chore(versions): 😊 publish v1.6.0-alpha.26 * feat: support the extension of preset fields in collections (#6183) * feat: support the extension of preset fields in collections * fix: bug * fix: bug * fix: bug * refactor: create collection * fix: config * fix: test case * refactor: code improve * refactor: code improve * fix: bug * fix: bug --------- Co-authored-by: chenos <chenlinxh@gmail.com> * feat: replace SchemaComponent with RemoteSchemaComponent and add AppNotFound for empty tabs * refactor: rename useCurrentRoute to useCurrentRouteData for clarity * fix: redirect to first tab by default * feat: support for the extension of optional fields for Kanban, Calendar, and Formula Field plugins (#6076) * feat: kanban field extention * fix: bug * fix: bug * fix: bug * fix: bug * feat: calender title fields * feat: background color fields * fix: bug * fix: bug * feat: formula field expression support field * feat: preset fields * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * refactor: code improve * fix: bug * fix: bug * fix: bug * fix: bug * refactor: code improve * revert: preset fields * refactor: code improve * refactor: code improve * fix: bug * fix: bug * fix: bug * refactor: code improve * fix: bug * refactor: code improve * refactor: code improve * fix: bug * fix: locale * refactor: code improve * fix: bug * refactor: code improve * refactor: code improve * refactor: code improve * refactor: locale * fix: test * fix: bug * fix: test * fix: test --------- Co-authored-by: chenos <chenlinxh@gmail.com> * fix: enhance access permission check and clean up unused props in Page component * fix: adjust Action.Page style to set top position to 0 * fix: update redirect logic to point to the first page route in admin layout * chore(versions): 😊 publish v1.6.0-alpha.27 * fix: add link handling in MenuItem to open URLs in a new tab * fix(data-source-main): update order * fix: integrate drag-and-drop functionality in MenuItem and Page components * fix: add drag-and-drop support in admin layout and improve loading behavior * fix: header style * fix: set sider width in admin layout * fix: refactor InternalAdminLayout for improved readability and maintainability * fix: optimize token management in InternalAdminLayout for better styling consistency * fix: style * fix: avoid error * fix: update container reference in MenuSchemaToolbarWithContainer * fix: add icon style to MenuSchemaToolbar * fix: remove bottom border from header in admin layout * fix: add collapsed button render function to InternalAdminLayout * fix: update viewport meta tag for better responsiveness * fix: add MenuItemIcon component for conditional icon rendering in admin layout * fix: wrap SchemaToolbar in SiderContext.Provider to prevent style issues in collapsed state * fix: update InternalAdminLayout styles for improved menu item appearance * fix: adjust menu item spacing and height for compact mode in admin layout * fix: add collapse handling and page change logic in InternalAdminLayout * fix: add header context provider and update MenuItemIcon rendering logic in InternalAdminLayout * fix: replace Modal with App.useApp().modal in HiddenMenuItem for improved modal handling * fix: enhance click area for links in MenuItem and streamline group navigation logic * fix: refresh routes after adding link menu * fix: add mobile actions popover for improved user interaction in admin layout * fix: adjust layout width and margin for improved alignment in admin layout * style: fix the style of the top collapsed menu button dropdown * fix: add active background color for selected menu items in collapsed menu * fix: improve z-index management for modal, drawer, and page components to prevent overlap with collapsed menu button * fix: adjust position of collapsed button to prevent overlap with subpages * fix: update collapsed button rendering to handle mobile context and prevent overlap with subpages * fix: prevent schema data request for group pages in admin layout * fix: handle undefined menu titles by providing a default value * fix: add refresh functionality for desktop routes in menu permissions * fix: adjust page header padding based on route settings and token * fix: center text * fix: add tooltip support for menu items in collapsed state * fix: tooltip * fix: improve page tab routing and deletion handling * fix: adjust admin layout height to account for header * fix: improve route navigation and deletion handling in admin layout * chore: update version * fix(routing): Add Navigate import from react-router-dom * fix(e2e): locator * test: remove demo and useless test case for Page component * fix: improve page creation and routing in e2e utils * fix: improve z-index handling for embedded pages * feat(admin-layout): Add aria-label to menu item links for improved accessibility * fix(mobile-ui): Adjust toolbar and navigation bar styling * test(acl): Simplify menu item visibility test setup * test: update e2e test templates and route handling * fix: fix compatibility issues * fix(admin-layout): improve default page navigation handling * fix(acl): add optional chaining to prevent potential null/undefined error in uiButtonSchemasBlacklist check * fix: keep alive * fix(desktop-routes): enhance route retrieval with automatic child route inclusion * test: add test * fix(page): update navigation logic to use location pathname for tab routing * fix(route): export CurrentRouteProvider for better accessibility in routing context * refactor(layout): remove unused styles for cleaner code * fix(layout): integrate useGlobalTheme for consistent theming and clean up unused parameters * fix(route): improve route redirection logic --------- Co-authored-by: nocobase[bot] <179432756+nocobase[bot]@users.noreply.github.com> Co-authored-by: Katherine <katherine_15995@163.com> Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: Junyi <mytharcher@users.noreply.github.com>
This commit is contained in:
parent
7404e57d4c
commit
0d0c81cc90
@ -17,7 +17,7 @@ export default defineConfig({
|
|||||||
title: 'Loading...',
|
title: 'Loading...',
|
||||||
devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false,
|
devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false,
|
||||||
favicons: [`${appPublicPath}favicon_no_exist.ico`], // 设置一个不存在的 favicon,防止显示 Umi 默认的 favicon
|
favicons: [`${appPublicPath}favicon_no_exist.ico`], // 设置一个不存在的 favicon,防止显示 Umi 默认的 favicon
|
||||||
metas: [{ name: 'viewport', content: 'initial-scale=0.1' }],
|
metas: [{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' }],
|
||||||
links: [{ rel: 'stylesheet', href: `${appPublicPath}global.css` }],
|
links: [{ rel: 'stylesheet', href: `${appPublicPath}global.css` }],
|
||||||
headScripts: [
|
headScripts: [
|
||||||
{
|
{
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"@ahooksjs/use-url-state": "3.5.1",
|
"@ahooksjs/use-url-state": "3.5.1",
|
||||||
"@ant-design/cssinjs": "^1.11.1",
|
"@ant-design/cssinjs": "^1.11.1",
|
||||||
"@ant-design/icons": "^5.6.1",
|
"@ant-design/icons": "^5.6.1",
|
||||||
"@ant-design/pro-layout": "^7.16.11",
|
"@ant-design/pro-layout": "^7.22.1",
|
||||||
"@antv/g2plot": "^2.4.18",
|
"@antv/g2plot": "^2.4.18",
|
||||||
"@budibase/handlebars-helpers": "^0.14.0",
|
"@budibase/handlebars-helpers": "^0.14.0",
|
||||||
"@ctrl/tinycolor": "^3.6.0",
|
"@ctrl/tinycolor": "^3.6.0",
|
||||||
|
@ -321,7 +321,7 @@ export const ACLActionProvider = (props) => {
|
|||||||
() => actionPath && parseAction(actionPath, { schema, recordPkValue }),
|
() => actionPath && parseAction(actionPath, { schema, recordPkValue }),
|
||||||
[parseAction, actionPath, schema, recordPkValue],
|
[parseAction, actionPath, schema, recordPkValue],
|
||||||
);
|
);
|
||||||
if (uiButtonSchemasBlacklist.includes(currentUid)) {
|
if (uiButtonSchemasBlacklist?.includes(currentUid)) {
|
||||||
return <ACLActionParamsContext.Provider value={false}>{props.children}</ACLActionParamsContext.Provider>;
|
return <ACLActionParamsContext.Provider value={false}>{props.children}</ACLActionParamsContext.Provider>;
|
||||||
}
|
}
|
||||||
if (!actionPath) {
|
if (!actionPath) {
|
||||||
|
@ -47,6 +47,7 @@ export const CSSVariableProvider = ({ children }) => {
|
|||||||
document.body.style.setProperty('--colorSettings', token.colorSettings || defaultTheme.token.colorSettings);
|
document.body.style.setProperty('--colorSettings', token.colorSettings || defaultTheme.token.colorSettings);
|
||||||
document.body.style.setProperty('--colorBgSettingsHover', token.colorBgSettingsHover);
|
document.body.style.setProperty('--colorBgSettingsHover', token.colorBgSettingsHover);
|
||||||
document.body.style.setProperty('--colorBorderSettingsHover', token.colorBorderSettingsHover);
|
document.body.style.setProperty('--colorBorderSettingsHover', token.colorBorderSettingsHover);
|
||||||
|
document.body.style.setProperty('--colorBgMenuItemSelected', token.colorBgHeaderMenuActive);
|
||||||
|
|
||||||
// 设置登录页面的背景色
|
// 设置登录页面的背景色
|
||||||
document.body.style.setProperty('background-color', token.colorBgContainer);
|
document.body.style.setProperty('background-color', token.colorBgContainer);
|
||||||
|
@ -27,9 +27,21 @@
|
|||||||
.rc-virtual-list-scrollbar-thumb {
|
.rc-virtual-list-scrollbar-thumb {
|
||||||
background: var(--colorBgScrollBar) !important;
|
background: var(--colorBgScrollBar) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rc-virtual-list-scrollbar-thumb:hover {
|
.rc-virtual-list-scrollbar-thumb:hover {
|
||||||
background: var(--colorBgScrollBarHover) !important;
|
background: var(--colorBgScrollBarHover) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rc-virtual-list-scrollbar-thumb:active {
|
.rc-virtual-list-scrollbar-thumb:active {
|
||||||
background: var(--colorBgScrollBarActive) !important;
|
background: var(--colorBgScrollBarActive) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix the style of the top collapsed menu button dropdown
|
||||||
|
.ant-menu-submenu-popup {
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix the style of the top collapsed menu button dropdown when clicking
|
||||||
|
.ant-menu-item.ant-menu-item-only-child.ant-pro-base-menu-horizontal-menu-item:active {
|
||||||
|
background-color: var(--colorBgMenuItemSelected) !important;
|
||||||
|
}
|
||||||
|
@ -71,16 +71,14 @@ export * from './modules/blocks/data-blocks/table';
|
|||||||
export * from './modules/blocks/data-blocks/table-selector';
|
export * from './modules/blocks/data-blocks/table-selector';
|
||||||
export * from './modules/blocks/index';
|
export * from './modules/blocks/index';
|
||||||
export * from './modules/blocks/useParentRecordCommon';
|
export * from './modules/blocks/useParentRecordCommon';
|
||||||
export { getGroupMenuSchema } from './modules/menu/GroupItem';
|
export { getPageMenuSchema, useInsertPageSchema } from './modules/menu/PageMenuItem';
|
||||||
export { getLinkMenuSchema } from './modules/menu/LinkMenuItem';
|
|
||||||
export { getPageMenuSchema } from './modules/menu/PageMenuItem';
|
|
||||||
export { OpenModeProvider, useOpenModeContext } from './modules/popup/OpenModeProvider';
|
export { OpenModeProvider, useOpenModeContext } from './modules/popup/OpenModeProvider';
|
||||||
export { PopupContextProvider } from './modules/popup/PopupContextProvider';
|
export { PopupContextProvider } from './modules/popup/PopupContextProvider';
|
||||||
export { usePopupUtils } from './modules/popup/usePopupUtils';
|
export { usePopupUtils } from './modules/popup/usePopupUtils';
|
||||||
export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
|
export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||||
|
|
||||||
export { useCurrentPopupRecord } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
|
|
||||||
export { showFileName } from './modules/fields/component/FileManager/fileManagerComponentFieldSettings';
|
export { showFileName } from './modules/fields/component/FileManager/fileManagerComponentFieldSettings';
|
||||||
|
export { useCurrentPopupRecord } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||||
|
|
||||||
export { languageCodes } from './locale';
|
export { languageCodes } from './locale';
|
||||||
|
|
||||||
@ -90,5 +88,6 @@ export {
|
|||||||
IsInNocoBaseRecursionFieldContext,
|
IsInNocoBaseRecursionFieldContext,
|
||||||
NocoBaseRecursionField,
|
NocoBaseRecursionField,
|
||||||
RefreshComponentProvider,
|
RefreshComponentProvider,
|
||||||
useRefreshFieldSchema,
|
useRefreshFieldSchema
|
||||||
} from './formily/NocoBaseRecursionField';
|
} from './formily/NocoBaseRecursionField';
|
||||||
|
|
||||||
|
@ -882,5 +882,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Are you sure you want to hide this menu?",
|
"Are you sure you want to hide this menu?": "Are you sure you want to hide this menu?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.",
|
||||||
"If selected, the page will display Tab pages.": "If selected, the page will display Tab pages.",
|
"If selected, the page will display Tab pages.": "If selected, the page will display Tab pages.",
|
||||||
"If selected, the route will be displayed in the menu.": "If selected, the route will be displayed in the menu."
|
"If selected, the route will be displayed in the menu.": "If selected, the route will be displayed in the menu.",
|
||||||
|
"Are you sure you want to hide this tab?": "Are you sure you want to hide this tab?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it."
|
||||||
}
|
}
|
||||||
|
@ -799,5 +799,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "¿Estás seguro de que quieres ocultar este menú?",
|
"Are you sure you want to hide this menu?": "¿Estás seguro de que quieres ocultar este menú?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Después de ocultar, este menú ya no aparecerá en la barra de menú. Para mostrarlo de nuevo, debe ir a la página de administración de rutas para configurarlo.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Después de ocultar, este menú ya no aparecerá en la barra de menú. Para mostrarlo de nuevo, debe ir a la página de administración de rutas para configurarlo.",
|
||||||
"If selected, the page will display Tab pages.": "Si se selecciona, la página mostrará páginas de pestañas.",
|
"If selected, the page will display Tab pages.": "Si se selecciona, la página mostrará páginas de pestañas.",
|
||||||
"If selected, the route will be displayed in the menu.": "Si se selecciona, la ruta se mostrará en el menú."
|
"If selected, the route will be displayed in the menu.": "Si se selecciona, la ruta se mostrará en el menú.",
|
||||||
|
"Are you sure you want to hide this tab?": "¿Estás seguro de que quieres ocultar esta pestaña?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Después de ocultar, esta pestaña ya no aparecerá en la barra de pestañas. Para mostrarla de nuevo, deberás ir a la página de gestión de rutas para configurarla."
|
||||||
}
|
}
|
||||||
|
@ -819,5 +819,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Êtes-vous sûr de vouloir masquer ce menu ?",
|
"Are you sure you want to hide this menu?": "Êtes-vous sûr de vouloir masquer ce menu ?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Après avoir masqué, ce menu ne sera plus affiché dans la barre de menu. Pour le réafficher, vous devez aller à la page de gestion des routes pour le configurer.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Après avoir masqué, ce menu ne sera plus affiché dans la barre de menu. Pour le réafficher, vous devez aller à la page de gestion des routes pour le configurer.",
|
||||||
"If selected, the page will display Tab pages.": "Si sélectionné, la page affichera des onglets.",
|
"If selected, the page will display Tab pages.": "Si sélectionné, la page affichera des onglets.",
|
||||||
"If selected, the route will be displayed in the menu.": "Si sélectionné, la route sera affichée dans le menu."
|
"If selected, the route will be displayed in the menu.": "Si sélectionné, la route sera affichée dans le menu.",
|
||||||
|
"Are you sure you want to hide this tab?": "Êtes-vous sûr de vouloir masquer cet onglet ?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Après avoir masqué, cette tab ne sera plus affichée dans la barre de tab. Pour la montrer à nouveau, vous devez vous rendre sur la page de gestion des routes pour la configurer."
|
||||||
}
|
}
|
||||||
|
@ -1037,5 +1037,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "このメニューを非表示にしますか?",
|
"Are you sure you want to hide this menu?": "このメニューを非表示にしますか?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "非表示にすると、このメニューはメニューバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "非表示にすると、このメニューはメニューバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。",
|
||||||
"If selected, the page will display Tab pages.": "選択されている場合、ページはタブページを表示します。",
|
"If selected, the page will display Tab pages.": "選択されている場合、ページはタブページを表示します。",
|
||||||
"If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。"
|
"If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。",
|
||||||
|
"Are you sure you want to hide this tab?": "このタブを非表示にしますか?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。"
|
||||||
}
|
}
|
||||||
|
@ -910,5 +910,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "이 메뉴를 숨기시겠습니까?",
|
"Are you sure you want to hide this menu?": "이 메뉴를 숨기시겠습니까?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "숨기면 이 메뉴는 메뉴 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "숨기면 이 메뉴는 메뉴 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.",
|
||||||
"If selected, the page will display Tab pages.": "선택되면 페이지는 탭 페이지를 표시합니다.",
|
"If selected, the page will display Tab pages.": "선택되면 페이지는 탭 페이지를 표시합니다.",
|
||||||
"If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다."
|
"If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다.",
|
||||||
|
"Are you sure you want to hide this tab?": "이 탭을 숨기시겠습니까?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다."
|
||||||
}
|
}
|
||||||
|
@ -776,5 +776,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Tem certeza de que deseja ocultar este menu?",
|
"Are you sure you want to hide this menu?": "Tem certeza de que deseja ocultar este menu?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Depois de ocultar, este menu não aparecerá mais na barra de menus. Para mostrar novamente, você precisa ir à página de gerenciamento de rotas para configurá-lo.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Depois de ocultar, este menu não aparecerá mais na barra de menus. Para mostrar novamente, você precisa ir à página de gerenciamento de rotas para configurá-lo.",
|
||||||
"If selected, the page will display Tab pages.": "Se selecionado, a página exibirá páginas de abas.",
|
"If selected, the page will display Tab pages.": "Se selecionado, a página exibirá páginas de abas.",
|
||||||
"If selected, the route will be displayed in the menu.": "Se selecionado, a rota será exibida no menu."
|
"If selected, the route will be displayed in the menu.": "Se selecionado, a rota será exibida no menu.",
|
||||||
|
"Are you sure you want to hide this tab?": "Tem certeza de que deseja ocultar esta guia?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Depois de ocultar, esta guia não aparecerá mais na barra de guias. Para mostrá-la novamente, você precisa ir à página de gerenciamento de rotas para configurá-la."
|
||||||
}
|
}
|
||||||
|
@ -605,5 +605,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Вы уверены, что хотите скрыть это меню?",
|
"Are you sure you want to hide this menu?": "Вы уверены, что хотите скрыть это меню?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "После скрытия этого меню он больше не будет отображаться в меню. Чтобы снова отобразить его, вам нужно будет перейти на страницу управления маршрутами и настроить его.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "После скрытия этого меню он больше не будет отображаться в меню. Чтобы снова отобразить его, вам нужно будет перейти на страницу управления маршрутами и настроить его.",
|
||||||
"If selected, the page will display Tab pages.": "Если выбран, страница будет отображать страницы с вкладками.",
|
"If selected, the page will display Tab pages.": "Если выбран, страница будет отображать страницы с вкладками.",
|
||||||
"If selected, the route will be displayed in the menu.": "Если выбран, маршрут будет отображаться в меню."
|
"If selected, the route will be displayed in the menu.": "Если выбран, маршрут будет отображаться в меню.",
|
||||||
|
"Are you sure you want to hide this tab?": "Вы уверены, что хотите скрыть эту вкладку?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее."
|
||||||
}
|
}
|
||||||
|
@ -603,5 +603,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Bu menüyü gizlemek istediğinizden emin misiniz?",
|
"Are you sure you want to hide this menu?": "Bu menüyü gizlemek istediğinizden emin misiniz?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Gizlendikten sonra, bu menü artık menü çubuğunda görünmeyecektir. Tekrar görüntülemek için, yönlendirme yönetimi sayfasına gidip onu yapılandırmanız gerekecektir.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Gizlendikten sonra, bu menü artık menü çubuğunda görünmeyecektir. Tekrar görüntülemek için, yönlendirme yönetimi sayfasına gidip onu yapılandırmanız gerekecektir.",
|
||||||
"If selected, the page will display Tab pages.": "Seçildiğinde, sayfa Tab sayfalarını görüntüleyecektir.",
|
"If selected, the page will display Tab pages.": "Seçildiğinde, sayfa Tab sayfalarını görüntüleyecektir.",
|
||||||
"If selected, the route will be displayed in the menu.": "Seçildiğinde, yol menüde görüntülenecektir."
|
"If selected, the route will be displayed in the menu.": "Seçildiğinde, yol menüde görüntülenecektir.",
|
||||||
|
"Are you sure you want to hide this tab?": "Bu sekmeyi gizlemek istediğinizden emin misiniz?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Gizlendikten sonra, bu sekme artık sekme çubuğunda görünmeyecek. Onu tekrar göstermek için, rotayı yönetim sayfasına gidip ayarlamanız gerekiyor."
|
||||||
}
|
}
|
||||||
|
@ -819,5 +819,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Ви впевнені, що хочете приховати це меню?",
|
"Are you sure you want to hide this menu?": "Ви впевнені, що хочете приховати це меню?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Після приховування цього меню він більше не з'явиться в меню. Щоб знову показати його, вам потрібно перейти на сторінку керування маршрутами і налаштувати його.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Після приховування цього меню він більше не з'явиться в меню. Щоб знову показати його, вам потрібно перейти на сторінку керування маршрутами і налаштувати його.",
|
||||||
"If selected, the page will display Tab pages.": "Якщо вибрано, сторінка відобразить сторінки з вкладками.",
|
"If selected, the page will display Tab pages.": "Якщо вибрано, сторінка відобразить сторінки з вкладками.",
|
||||||
"If selected, the route will be displayed in the menu.": "Якщо вибрано, маршрут буде відображений в меню."
|
"If selected, the route will be displayed in the menu.": "Якщо вибрано, маршрут буде відображений в меню.",
|
||||||
|
"Are you sure you want to hide this tab?": "Ви впевнені, що хочете приховати цю вкладку?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її."
|
||||||
}
|
}
|
||||||
|
@ -1078,5 +1078,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "你确定要隐藏这个菜单吗?",
|
"Are you sure you want to hide this menu?": "你确定要隐藏这个菜单吗?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "隐藏后,这个菜单将不再出现在菜单栏中。要再次显示它,你需要到路由管理页面进行设置。",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "隐藏后,这个菜单将不再出现在菜单栏中。要再次显示它,你需要到路由管理页面进行设置。",
|
||||||
"If selected, the page will display Tab pages.": "如果选中,该页面将显示标签页。",
|
"If selected, the page will display Tab pages.": "如果选中,该页面将显示标签页。",
|
||||||
"If selected, the route will be displayed in the menu.": "如果选中,该路由将显示在菜单中。"
|
"If selected, the route will be displayed in the menu.": "如果选中,该路由将显示在菜单中。",
|
||||||
|
"Are you sure you want to hide this tab?": "你确定要隐藏该标签页吗?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隐藏后,该标签将不再显示在标签栏中。要想再次显示它,你需要到路由管理页面进行设置。"
|
||||||
}
|
}
|
||||||
|
@ -910,6 +910,8 @@
|
|||||||
"Are you sure you want to hide this menu?": "你確定要隱藏這個菜單嗎?",
|
"Are you sure you want to hide this menu?": "你確定要隱藏這個菜單嗎?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "隱藏後,這個菜單將不再出現在菜單欄中。要再次顯示它,你需要到路由管理頁面進行設置。",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "隱藏後,這個菜單將不再出現在菜單欄中。要再次顯示它,你需要到路由管理頁面進行設置。",
|
||||||
"If selected, the page will display Tab pages.": "如果選中,該頁面將顯示標籤頁。",
|
"If selected, the page will display Tab pages.": "如果選中,該頁面將顯示標籤頁。",
|
||||||
"If selected, the route will be displayed in the menu.": "如果選中,該路由將顯示在菜單中。"
|
"If selected, the route will be displayed in the menu.": "如果選中,該路由將顯示在菜單中。",
|
||||||
|
"Are you sure you want to hide this tab?": "你確定要隱藏這個標籤嗎?",
|
||||||
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -884,7 +884,7 @@ export const URLSearchParamsUseAssociationFieldValue = {
|
|||||||
linkageAction: true,
|
linkageAction: true,
|
||||||
},
|
},
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
url: '/admin/ids0d9esx8k',
|
url: '/admin/ocal3pnltf2',
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
name: 'roles',
|
name: 'roles',
|
||||||
|
@ -3824,10 +3824,11 @@ export const T4334 = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'x-uid': 'ribk031tkp8',
|
'x-uid': 'ribk031tkp8',
|
||||||
'x-async': false,
|
'x-async': true,
|
||||||
'x-index': 1,
|
'x-index': 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'x-uid': '1j5z1j5z1j5',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import { SchemaOptionsContext } from '@formily/react';
|
|||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import React, { useCallback, useContext } from 'react';
|
import React, { useCallback, useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaInitializerItem, useSchemaInitializer } from '../../application';
|
import { SchemaInitializerItem } from '../../application';
|
||||||
import { useGlobalTheme } from '../../global-theme';
|
import { useGlobalTheme } from '../../global-theme';
|
||||||
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
||||||
import {
|
import {
|
||||||
@ -25,7 +25,6 @@ import {
|
|||||||
import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers';
|
import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers';
|
||||||
|
|
||||||
export const GroupItem = () => {
|
export const GroupItem = () => {
|
||||||
const { insert } = useSchemaInitializer();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
const { theme } = useGlobalTheme();
|
const { theme } = useGlobalTheme();
|
||||||
@ -69,30 +68,13 @@ export const GroupItem = () => {
|
|||||||
const schemaUid = uid();
|
const schemaUid = uid();
|
||||||
|
|
||||||
// 创建一个路由到 desktopRoutes 表中
|
// 创建一个路由到 desktopRoutes 表中
|
||||||
const { data } = await createRoute({
|
await createRoute({
|
||||||
type: NocoBaseDesktopRouteType.group,
|
type: NocoBaseDesktopRouteType.group,
|
||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
parentId: parentRoute?.id,
|
parentId: parentRoute?.id,
|
||||||
schemaUid,
|
schemaUid,
|
||||||
});
|
});
|
||||||
|
}, [options.components, options.scope, t, theme]);
|
||||||
// 同时插入一个对应的 Schema
|
|
||||||
insert(getGroupMenuSchema({ title, icon, schemaUid, route: data?.data }));
|
|
||||||
}, [insert, options.components, options.scope, t, theme]);
|
|
||||||
return <SchemaInitializerItem title={t('Group')} onClick={handleClick} className={`${componentCls} ${hashId}`} />;
|
return <SchemaInitializerItem title={t('Group')} onClick={handleClick} className={`${componentCls} ${hashId}`} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getGroupMenuSchema({ title, icon, schemaUid, route = undefined }) {
|
|
||||||
return {
|
|
||||||
type: 'void',
|
|
||||||
title,
|
|
||||||
'x-component': 'Menu.SubMenu',
|
|
||||||
'x-decorator': 'ACLMenuItemProvider',
|
|
||||||
'x-component-props': {
|
|
||||||
icon,
|
|
||||||
},
|
|
||||||
'x-uid': schemaUid,
|
|
||||||
__route__: route,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -9,12 +9,11 @@
|
|||||||
|
|
||||||
import { FormLayout } from '@formily/antd-v5';
|
import { FormLayout } from '@formily/antd-v5';
|
||||||
import { SchemaOptionsContext } from '@formily/react';
|
import { SchemaOptionsContext } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import React, { useCallback, useContext } from 'react';
|
import React, { useCallback, useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import { SchemaInitializerItem, useSchemaInitializer } from '../../application';
|
import { SchemaInitializerItem } from '../../application';
|
||||||
import { useGlobalTheme } from '../../global-theme';
|
import { useGlobalTheme } from '../../global-theme';
|
||||||
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
||||||
import {
|
import {
|
||||||
@ -28,7 +27,6 @@ import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers
|
|||||||
import { useURLAndHTMLSchema } from '../actions/link/useURLAndHTMLSchema';
|
import { useURLAndHTMLSchema } from '../actions/link/useURLAndHTMLSchema';
|
||||||
|
|
||||||
export const LinkMenuItem = () => {
|
export const LinkMenuItem = () => {
|
||||||
const { insert } = useSchemaInitializer();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
const { theme } = useGlobalTheme();
|
const { theme } = useGlobalTheme();
|
||||||
@ -75,40 +73,19 @@ export const LinkMenuItem = () => {
|
|||||||
initialValues: {},
|
initialValues: {},
|
||||||
});
|
});
|
||||||
const { title, href, params, icon } = values;
|
const { title, href, params, icon } = values;
|
||||||
const schemaUid = uid();
|
|
||||||
|
|
||||||
// 创建一个路由到 desktopRoutes 表中
|
// 创建一个路由到 desktopRoutes 表中
|
||||||
const { data } = await createRoute({
|
await createRoute({
|
||||||
type: NocoBaseDesktopRouteType.link,
|
type: NocoBaseDesktopRouteType.link,
|
||||||
title: values.title,
|
title,
|
||||||
icon: values.icon,
|
icon,
|
||||||
parentId: parentRoute?.id,
|
parentId: parentRoute?.id,
|
||||||
schemaUid,
|
|
||||||
options: {
|
options: {
|
||||||
href,
|
href,
|
||||||
params,
|
params,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}, [options.components, options.scope, t, theme]);
|
||||||
// 同时插入一个对应的 Schema
|
|
||||||
insert(getLinkMenuSchema({ title, icon, schemaUid, href, params, route: data?.data }));
|
|
||||||
}, [insert, options.components, options.scope, t, theme]);
|
|
||||||
|
|
||||||
return <SchemaInitializerItem title={t('Link')} onClick={handleClick} className={`${componentCls} ${hashId}`} />;
|
return <SchemaInitializerItem title={t('Link')} onClick={handleClick} className={`${componentCls} ${hashId}`} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getLinkMenuSchema({ title, icon, schemaUid, href, params, route = undefined }) {
|
|
||||||
return {
|
|
||||||
type: 'void',
|
|
||||||
title,
|
|
||||||
'x-component': 'Menu.URL',
|
|
||||||
'x-decorator': 'ACLMenuItemProvider',
|
|
||||||
'x-component-props': {
|
|
||||||
icon,
|
|
||||||
href,
|
|
||||||
params,
|
|
||||||
},
|
|
||||||
'x-uid': schemaUid,
|
|
||||||
__route__: route,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -12,7 +12,8 @@ import { SchemaOptionsContext } from '@formily/react';
|
|||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import React, { useCallback, useContext } from 'react';
|
import React, { useCallback, useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaInitializerItem, useSchemaInitializer } from '../../application';
|
import { useAPIClient } from '../../api-client/hooks/useAPIClient';
|
||||||
|
import { SchemaInitializerItem } from '../../application';
|
||||||
import { useGlobalTheme } from '../../global-theme';
|
import { useGlobalTheme } from '../../global-theme';
|
||||||
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
||||||
import {
|
import {
|
||||||
@ -24,14 +25,28 @@ import {
|
|||||||
} from '../../schema-component';
|
} from '../../schema-component';
|
||||||
import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers';
|
import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers';
|
||||||
|
|
||||||
|
export const useInsertPageSchema = () => {
|
||||||
|
const api = useAPIClient();
|
||||||
|
return useCallback(
|
||||||
|
async (schema) => {
|
||||||
|
await api.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/uiSchemas:insert',
|
||||||
|
data: schema,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[api],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const PageMenuItem = () => {
|
export const PageMenuItem = () => {
|
||||||
const { insert } = useSchemaInitializer();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
const { theme } = useGlobalTheme();
|
const { theme } = useGlobalTheme();
|
||||||
const { componentCls, hashId } = useStyles();
|
const { componentCls, hashId } = useStyles();
|
||||||
const parentRoute = useParentRoute();
|
const parentRoute = useParentRoute();
|
||||||
const { createRoute } = useNocoBaseRoutes();
|
const { createRoute } = useNocoBaseRoutes();
|
||||||
|
const insertPageSchema = useInsertPageSchema();
|
||||||
|
|
||||||
const handleClick = useCallback(async () => {
|
const handleClick = useCallback(async () => {
|
||||||
const values = await FormDialog(
|
const values = await FormDialog(
|
||||||
@ -65,16 +80,13 @@ export const PageMenuItem = () => {
|
|||||||
).open({
|
).open({
|
||||||
initialValues: {},
|
initialValues: {},
|
||||||
});
|
});
|
||||||
const { title, icon } = values;
|
|
||||||
const menuSchemaUid = uid();
|
const menuSchemaUid = uid();
|
||||||
const pageSchemaUid = uid();
|
const pageSchemaUid = uid();
|
||||||
const tabSchemaUid = uid();
|
const tabSchemaUid = uid();
|
||||||
const tabSchemaName = uid();
|
const tabSchemaName = uid();
|
||||||
|
|
||||||
// 创建一个路由到 desktopRoutes 表中
|
// 创建一个路由到 desktopRoutes 表中
|
||||||
const {
|
await createRoute({
|
||||||
data: { data: route },
|
|
||||||
} = await createRoute({
|
|
||||||
type: NocoBaseDesktopRouteType.page,
|
type: NocoBaseDesktopRouteType.page,
|
||||||
title: values.title,
|
title: values.title,
|
||||||
icon: values.icon,
|
icon: values.icon,
|
||||||
@ -93,46 +105,25 @@ export const PageMenuItem = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 同时插入一个对应的 Schema
|
// 同时插入一个对应的 Schema
|
||||||
insert(getPageMenuSchema({ title, icon, pageSchemaUid, tabSchemaUid, menuSchemaUid, tabSchemaName, route }));
|
insertPageSchema(getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }));
|
||||||
}, [createRoute, insert, options?.components, options?.scope, parentRoute?.id, t, theme]);
|
}, [createRoute, insertPageSchema, options?.components, options?.scope, parentRoute?.id, t, theme]);
|
||||||
return <SchemaInitializerItem title={t('Page')} onClick={handleClick} className={`${componentCls} ${hashId}`} />;
|
return <SchemaInitializerItem title={t('Page')} onClick={handleClick} className={`${componentCls} ${hashId}`} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getPageMenuSchema({
|
export function getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }) {
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
pageSchemaUid,
|
|
||||||
tabSchemaUid,
|
|
||||||
menuSchemaUid,
|
|
||||||
tabSchemaName,
|
|
||||||
route = undefined,
|
|
||||||
}) {
|
|
||||||
return {
|
return {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title,
|
'x-component': 'Page',
|
||||||
'x-component': 'Menu.Item',
|
|
||||||
'x-decorator': 'ACLMenuItemProvider',
|
|
||||||
'x-component-props': {
|
|
||||||
icon,
|
|
||||||
},
|
|
||||||
properties: {
|
properties: {
|
||||||
page: {
|
[tabSchemaName]: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'Page',
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'page:addBlock',
|
||||||
|
properties: {},
|
||||||
|
'x-uid': tabSchemaUid,
|
||||||
'x-async': true,
|
'x-async': true,
|
||||||
properties: {
|
|
||||||
[tabSchemaName]: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Grid',
|
|
||||||
'x-initializer': 'page:addBlock',
|
|
||||||
properties: {},
|
|
||||||
'x-uid': tabSchemaUid,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'x-uid': pageSchemaUid,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'x-uid': menuSchemaUid,
|
'x-uid': pageSchemaUid,
|
||||||
__route__: route,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ test('single page', async ({ page, mockPage }) => {
|
|||||||
await mockPage({ name: pageTitle2 }).goto();
|
await mockPage({ name: pageTitle2 }).goto();
|
||||||
await page.getByRole('menu').getByText(pageTitle1).click();
|
await page.getByRole('menu').getByText(pageTitle1).click();
|
||||||
await page.getByRole('menu').getByText(pageTitle1).hover();
|
await page.getByRole('menu').getByText(pageTitle1).hover();
|
||||||
await page.getByLabel(pageTitle1).getByLabel('designer-schema-settings').hover();
|
await page.getByRole('button', { name: 'designer-schema-settings-' }).hover();
|
||||||
await page.getByRole('menuitem', { name: 'Move to' }).click();
|
await page.getByRole('menuitem', { name: 'Move to' }).click();
|
||||||
await page.getByLabel('block-item-TreeSelect-Target').locator('.ant-select').click();
|
await page.getByLabel('block-item-TreeSelect-Target').locator('.ant-select').click();
|
||||||
await page.locator('.ant-select-dropdown').getByText(pageTitle2).click();
|
await page.locator('.ant-select-dropdown').getByText(pageTitle2).click();
|
||||||
|
@ -20,14 +20,14 @@ test.describe('router', () => {
|
|||||||
await expect(page.getByText('This is tab2.')).toBeVisible();
|
await expect(page.getByText('This is tab2.')).toBeVisible();
|
||||||
|
|
||||||
// 2. 点击 tab1 应该跳转到 tab1,并使用新版 URL
|
// 2. 点击 tab1 应该跳转到 tab1,并使用新版 URL
|
||||||
await page.getByText('tab1').click();
|
await page.getByText('tab1', { exact: true }).click();
|
||||||
await expect(page.getByText('This is tab1.')).toBeVisible();
|
await expect(page.getByText('This is tab1.')).toBeVisible();
|
||||||
expect(page.url()).toMatch(new RegExp(`${pageUrl}/tabs/u4earq3d9go`));
|
expect(page.url()).toMatch(new RegExp(`${pageUrl}/tabs/xbx6zg90ij2`));
|
||||||
|
|
||||||
// 3. 点击 tab2 应该跳转到 tab2,并使用新版 URL
|
// 3. 点击 tab2 应该跳转到 tab2,并使用新版 URL
|
||||||
await page.getByText('tab2').click();
|
await page.getByText('tab2', { exact: true }).click();
|
||||||
await expect(page.getByText('This is tab2.')).toBeVisible();
|
await expect(page.getByText('This is tab2.')).toBeVisible();
|
||||||
expect(page.url()).toMatch(new RegExp(`${pageUrl}/tabs/bbch3c9b5jl`));
|
expect(page.url()).toMatch(new RegExp(`${pageUrl}/tabs/qhjdmy9nk6q`));
|
||||||
|
|
||||||
// 4. 使用不带 tab 参数的 URL,应该默认显示第一个 tab
|
// 4. 使用不带 tab 参数的 URL,应该默认显示第一个 tab
|
||||||
await nocoPage.goto();
|
await nocoPage.goto();
|
||||||
|
@ -13,12 +13,11 @@ test.describe('deleted popups', () => {
|
|||||||
test('should display error info when deleted popups', async ({ page, mockPage }) => {
|
test('should display error info when deleted popups', async ({ page, mockPage }) => {
|
||||||
const nocoPage = await mockPage().waitForInit();
|
const nocoPage = await mockPage().waitForInit();
|
||||||
const url = await nocoPage.getUrl();
|
const url = await nocoPage.getUrl();
|
||||||
|
const path =
|
||||||
await page.goto(
|
|
||||||
url +
|
url +
|
||||||
'/popups/vygn5ile3xz/filterbytk/1/popups/n24hos465bj/filterbytk/admin/sourceid/1/popups/s32h1ed5g9i/filterbytk/admin/sourceid/1',
|
'/popups/vygn5ile3xz/filterbytk/1/popups/n24hos465bj/filterbytk/admin/sourceid/1/popups/s32h1ed5g9i/filterbytk/admin/sourceid/1';
|
||||||
);
|
|
||||||
|
|
||||||
|
await page.goto(path);
|
||||||
await expect(page.getByText('Sorry, the page you visited does not exist.')).toHaveCount(3);
|
await expect(page.getByText('Sorry, the page you visited does not exist.')).toHaveCount(3);
|
||||||
|
|
||||||
// close the popups
|
// close the popups
|
||||||
|
@ -18,7 +18,7 @@ test.describe('popup router', () => {
|
|||||||
}).waitForInit();
|
}).waitForInit();
|
||||||
const url = await nocoPage.getUrl();
|
const url = await nocoPage.getUrl();
|
||||||
|
|
||||||
// 直接跳转到子页面,然后点击返回按钮,查看是否能返回到上一级页面
|
// Directly navigate to the subpage, then click the back button to check if it can return to the parent page
|
||||||
await page.goto(
|
await page.goto(
|
||||||
url +
|
url +
|
||||||
'/popups/56tsj7l3k35/filterbytk/1/popups/bd3nizznkdw/filterbytk/member/sourceid/1/popups/1ct9qd9jlbm/filterbytk/member/sourceid/1',
|
'/popups/56tsj7l3k35/filterbytk/1/popups/bd3nizznkdw/filterbytk/member/sourceid/1/popups/1ct9qd9jlbm/filterbytk/member/sourceid/1',
|
||||||
|
@ -11,6 +11,7 @@ import { DisconnectOutlined, LoadingOutlined } from '@ant-design/icons';
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { observer } from '@formily/reactive-react';
|
import { observer } from '@formily/reactive-react';
|
||||||
import { getSubAppName } from '@nocobase/sdk';
|
import { getSubAppName } from '@nocobase/sdk';
|
||||||
|
import { tval } from '@nocobase/utils/client';
|
||||||
import { Button, Modal, Result, Spin } from 'antd';
|
import { Button, Modal, Result, Spin } from 'antd';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Navigate, useNavigate } from 'react-router-dom';
|
import { Navigate, useNavigate } from 'react-router-dom';
|
||||||
@ -32,7 +33,6 @@ import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates';
|
|||||||
import { SystemSettingsPlugin } from '../system-settings';
|
import { SystemSettingsPlugin } from '../system-settings';
|
||||||
import { CurrentUserProvider, CurrentUserSettingsMenuProvider } from '../user';
|
import { CurrentUserProvider, CurrentUserSettingsMenuProvider } from '../user';
|
||||||
import { LocalePlugin } from './plugins/LocalePlugin';
|
import { LocalePlugin } from './plugins/LocalePlugin';
|
||||||
import { tval } from '@nocobase/utils/client';
|
|
||||||
|
|
||||||
const AppSpin = () => {
|
const AppSpin = () => {
|
||||||
return (
|
return (
|
||||||
@ -251,7 +251,7 @@ const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(
|
|||||||
{ displayName: 'AppMaintainingDialog' },
|
{ displayName: 'AppMaintainingDialog' },
|
||||||
);
|
);
|
||||||
|
|
||||||
const AppNotFound = () => {
|
export const AppNotFound = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<Result
|
<Result
|
||||||
|
@ -9,9 +9,12 @@
|
|||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { SchemaOptionsContext } from '@formily/react';
|
import { SchemaOptionsContext } from '@formily/react';
|
||||||
|
import { ConfigProvider, Divider } from 'antd';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useACLRoleContext } from '../acl/ACLProvider';
|
import { useACLRoleContext } from '../acl/ACLProvider';
|
||||||
|
import { UserCenter } from '../route-switch/antd/admin-layout/UserCenterButton';
|
||||||
|
import { Help } from '../user/Help';
|
||||||
import { PinnedPluginListContext } from './context';
|
import { PinnedPluginListContext } from './context';
|
||||||
|
|
||||||
export const PinnedPluginListProvider: React.FC<{ items: any }> = (props) => {
|
export const PinnedPluginListProvider: React.FC<{ items: any }> = (props) => {
|
||||||
@ -25,7 +28,8 @@ export const PinnedPluginListProvider: React.FC<{ items: any }> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pinnedPluginListClassName = css`
|
const pinnedPluginListClassName = css`
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.ant-btn {
|
.ant-btn {
|
||||||
border: 0;
|
border: 0;
|
||||||
@ -44,6 +48,12 @@ const pinnedPluginListClassName = css`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const dividerTheme = {
|
||||||
|
token: {
|
||||||
|
colorSplit: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const PinnedPluginList = React.memo(() => {
|
export const PinnedPluginList = React.memo(() => {
|
||||||
const { allowAll, snippets } = useACLRoleContext();
|
const { allowAll, snippets } = useACLRoleContext();
|
||||||
const getSnippetsAllow = (aclKey) => {
|
const getSnippetsAllow = (aclKey) => {
|
||||||
@ -61,6 +71,11 @@ export const PinnedPluginList = React.memo(() => {
|
|||||||
const Action = get(components, ctx.items[key].component);
|
const Action = get(components, ctx.items[key].component);
|
||||||
return Action ? <Action key={key} /> : null;
|
return Action ? <Action key={key} /> : null;
|
||||||
})}
|
})}
|
||||||
|
<ConfigProvider theme={dividerTheme}>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
</ConfigProvider>
|
||||||
|
<Help key="help" />
|
||||||
|
<UserCenter />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,141 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 { describe, expect, it } from 'vitest';
|
|
||||||
import { convertRoutesToSchema, NocoBaseDesktopRouteType } from '../convertRoutesToSchema';
|
|
||||||
|
|
||||||
describe('convertRoutesToSchema', () => {
|
|
||||||
it('should convert empty routes array to basic menu schema', () => {
|
|
||||||
const result = convertRoutesToSchema([]);
|
|
||||||
expect(result).toMatchObject({
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Menu',
|
|
||||||
'x-designer': 'Menu.Designer',
|
|
||||||
'x-initializer': 'MenuItemInitializers',
|
|
||||||
properties: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert single page route to menu schema', () => {
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Test Page',
|
|
||||||
type: NocoBaseDesktopRouteType.page,
|
|
||||||
icon: 'HomeOutlined',
|
|
||||||
menuSchemaUid: 'test-uid',
|
|
||||||
createdAt: '2023-01-01',
|
|
||||||
updatedAt: '2023-01-01',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = convertRoutesToSchema(routes);
|
|
||||||
expect(result.properties).toMatchObject({
|
|
||||||
[Object.keys(result.properties)[0]]: {
|
|
||||||
type: 'void',
|
|
||||||
title: 'Test Page',
|
|
||||||
'x-component': 'Menu.Item',
|
|
||||||
'x-component-props': {
|
|
||||||
icon: 'HomeOutlined',
|
|
||||||
},
|
|
||||||
'x-uid': 'test-uid',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert nested group route to menu schema', () => {
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Group',
|
|
||||||
type: NocoBaseDesktopRouteType.group,
|
|
||||||
icon: 'GroupOutlined',
|
|
||||||
schemaUid: 'group-uid',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Child Page',
|
|
||||||
type: NocoBaseDesktopRouteType.page,
|
|
||||||
icon: 'FileOutlined',
|
|
||||||
menuSchemaUid: 'child-uid',
|
|
||||||
createdAt: '2023-01-01',
|
|
||||||
updatedAt: '2023-01-01',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
createdAt: '2023-01-01',
|
|
||||||
updatedAt: '2023-01-01',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = convertRoutesToSchema(routes);
|
|
||||||
const groupSchema = result.properties[Object.keys(result.properties)[0]];
|
|
||||||
|
|
||||||
expect(groupSchema).toMatchObject({
|
|
||||||
type: 'void',
|
|
||||||
title: 'Group',
|
|
||||||
'x-component': 'Menu.SubMenu',
|
|
||||||
'x-component-props': {
|
|
||||||
icon: 'GroupOutlined',
|
|
||||||
},
|
|
||||||
'x-uid': 'group-uid',
|
|
||||||
});
|
|
||||||
|
|
||||||
const childSchema = groupSchema.properties[Object.keys(groupSchema.properties)[0]];
|
|
||||||
expect(childSchema).toMatchObject({
|
|
||||||
type: 'void',
|
|
||||||
title: 'Child Page',
|
|
||||||
'x-component': 'Menu.Item',
|
|
||||||
'x-component-props': {
|
|
||||||
icon: 'FileOutlined',
|
|
||||||
},
|
|
||||||
'x-uid': 'child-uid',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip tabs type routes', () => {
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Tabs',
|
|
||||||
type: NocoBaseDesktopRouteType.tabs,
|
|
||||||
schemaUid: 'tabs-uid',
|
|
||||||
createdAt: '2023-01-01',
|
|
||||||
updatedAt: '2023-01-01',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = convertRoutesToSchema(routes);
|
|
||||||
expect(Object.keys(result.properties)).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert link type route to menu URL schema', () => {
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'External Link',
|
|
||||||
type: NocoBaseDesktopRouteType.link,
|
|
||||||
icon: 'LinkOutlined',
|
|
||||||
schemaUid: 'link-uid',
|
|
||||||
createdAt: '2023-01-01',
|
|
||||||
updatedAt: '2023-01-01',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = convertRoutesToSchema(routes);
|
|
||||||
expect(result.properties[Object.keys(result.properties)[0]]).toMatchObject({
|
|
||||||
type: 'void',
|
|
||||||
title: 'External Link',
|
|
||||||
'x-component': 'Menu.URL',
|
|
||||||
'x-component-props': {
|
|
||||||
icon: 'LinkOutlined',
|
|
||||||
},
|
|
||||||
'x-uid': 'link-uid',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -7,10 +7,6 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ISchema } from '@formily/json-schema';
|
|
||||||
import { uid } from '@formily/shared';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export enum NocoBaseDesktopRouteType {
|
export enum NocoBaseDesktopRouteType {
|
||||||
group = 'group',
|
group = 'group',
|
||||||
page = 'page',
|
page = 'page',
|
||||||
@ -56,71 +52,3 @@ export interface NocoBaseDesktopRoute {
|
|||||||
createdBy?: any;
|
createdBy?: any;
|
||||||
updatedBy?: any;
|
updatedBy?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 为了简化菜单的重构,直接讲路由数据转换为 Schema 数据。这样就可以实现在不大改组件的前提下,完成菜单的重构。
|
|
||||||
* 注:菜单重构指的是将菜单的结构由原来的 Schema 结构改为一个树结构,并保存在 desktopRoutes 中。
|
|
||||||
* @param routes
|
|
||||||
*/
|
|
||||||
export function convertRoutesToSchema(routes: NocoBaseDesktopRoute[]) {
|
|
||||||
const routesSchemaList = routes.map((route) => convertRouteToSchema(route)).filter(Boolean);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Menu',
|
|
||||||
'x-designer': 'Menu.Designer',
|
|
||||||
'x-initializer': 'MenuItemInitializers',
|
|
||||||
'x-component-props': {
|
|
||||||
mode: 'mix',
|
|
||||||
theme: 'dark',
|
|
||||||
onSelect: '{{ onSelect }}',
|
|
||||||
sideMenuRefScopeKey: 'sideMenuRef',
|
|
||||||
},
|
|
||||||
properties: _.fromPairs(routesSchemaList.map((schema) => [uid(), schema])),
|
|
||||||
name: 'wecmvuxtid7',
|
|
||||||
'x-uid': 'nocobase-admin-menu',
|
|
||||||
'x-async': false,
|
|
||||||
} as ISchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
const routeTypeToComponent = {
|
|
||||||
[NocoBaseDesktopRouteType.page]: 'Menu.Item',
|
|
||||||
[NocoBaseDesktopRouteType.group]: 'Menu.SubMenu',
|
|
||||||
[NocoBaseDesktopRouteType.link]: 'Menu.URL',
|
|
||||||
};
|
|
||||||
|
|
||||||
function convertRouteToSchema(route: NocoBaseDesktopRoute) {
|
|
||||||
// tabs 需要在页面 Schema 中处理
|
|
||||||
if (route.type === NocoBaseDesktopRouteType.tabs) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const children = route.children?.map((child) => convertRouteToSchema(child)).filter(Boolean);
|
|
||||||
|
|
||||||
return {
|
|
||||||
_isJSONSchemaObject: true,
|
|
||||||
version: '2.0',
|
|
||||||
type: 'void',
|
|
||||||
title: route.title,
|
|
||||||
'x-component': routeTypeToComponent[route.type],
|
|
||||||
'x-decorator': 'ACLMenuItemProvider',
|
|
||||||
'x-component-props': {
|
|
||||||
icon: route.icon,
|
|
||||||
href: route.options?.href,
|
|
||||||
params: route.options?.params,
|
|
||||||
hidden: route.hideInMenu,
|
|
||||||
},
|
|
||||||
properties: children
|
|
||||||
? _.fromPairs(
|
|
||||||
children.map((child) => [
|
|
||||||
uid(), // 生成唯一的 key
|
|
||||||
child,
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
: {},
|
|
||||||
'x-app-version': '1.5.0-beta.12',
|
|
||||||
'x-uid': route.type === NocoBaseDesktopRouteType.page ? route.menuSchemaUid : route.schemaUid,
|
|
||||||
'x-async': false,
|
|
||||||
__route__: route,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,623 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
|
import { TreeSelect } from '@formily/antd-v5';
|
||||||
|
import { Field, onFieldChange } from '@formily/core';
|
||||||
|
import { ISchema } from '@formily/react';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { App, ConfigProvider } from 'antd';
|
||||||
|
import { SiderContext } from 'antd/es/layout/Sider';
|
||||||
|
import React, { FC, useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
findRouteBySchemaUid,
|
||||||
|
isVariable,
|
||||||
|
NocoBaseDesktopRouteType,
|
||||||
|
useAllAccessDesktopRoutes,
|
||||||
|
useCompile,
|
||||||
|
useCurrentPageUid,
|
||||||
|
useCurrentRouteData,
|
||||||
|
useGlobalTheme,
|
||||||
|
useNavigateNoUpdate,
|
||||||
|
useNocoBaseRoutes,
|
||||||
|
useToken,
|
||||||
|
useURLAndHTMLSchema,
|
||||||
|
} from '../../..';
|
||||||
|
import { getPageMenuSchema } from '../../../';
|
||||||
|
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||||
|
import { useInsertPageSchema } from '../../../modules/menu/PageMenuItem';
|
||||||
|
import { SchemaToolbar } from '../../../schema-settings/GeneralSchemaDesigner';
|
||||||
|
import {
|
||||||
|
SchemaSettingsItem,
|
||||||
|
SchemaSettingsModalItem,
|
||||||
|
SchemaSettingsSubMenu,
|
||||||
|
SchemaSettingsSwitchItem,
|
||||||
|
} from '../../../schema-settings/SchemaSettings';
|
||||||
|
import { NocoBaseDesktopRoute } from './convertRoutesToSchema';
|
||||||
|
|
||||||
|
const components = { TreeSelect };
|
||||||
|
|
||||||
|
const toItems = (routes: NocoBaseDesktopRoute[], { t, compile }) => {
|
||||||
|
const items = [];
|
||||||
|
for (const route of routes) {
|
||||||
|
const item = {
|
||||||
|
label: isVariable(route.title) ? compile(route.title) : t(route.title),
|
||||||
|
value: `${route.id}||${route.type}`,
|
||||||
|
};
|
||||||
|
if (route.children?.length > 0) {
|
||||||
|
item['children'] = toItems(route.children, { t, compile });
|
||||||
|
}
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertPositionToMethod = {
|
||||||
|
beforeBegin: 'prepend',
|
||||||
|
afterEnd: 'insertAfter',
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPrevSibling = (routes: NocoBaseDesktopRoute[], currentRoute: NocoBaseDesktopRoute | undefined) => {
|
||||||
|
if (!currentRoute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < routes.length; i++) {
|
||||||
|
const route = routes[i];
|
||||||
|
if (route.id === currentRoute.id) {
|
||||||
|
return routes[i - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.children) {
|
||||||
|
const prevSibling = findPrevSibling(route.children, currentRoute);
|
||||||
|
if (prevSibling) {
|
||||||
|
return prevSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findNextSibling = (routes: NocoBaseDesktopRoute[], currentRoute: NocoBaseDesktopRoute | undefined) => {
|
||||||
|
if (!currentRoute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < routes.length; i++) {
|
||||||
|
const route = routes[i];
|
||||||
|
if (route.id === currentRoute.id) {
|
||||||
|
return routes[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.children) {
|
||||||
|
const nextSibling = findNextSibling(route.children, currentRoute);
|
||||||
|
if (nextSibling) {
|
||||||
|
return nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RemoveRoute: FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { modal } = App.useApp();
|
||||||
|
const { deleteRoute } = useNocoBaseRoutes();
|
||||||
|
const currentRoute = useCurrentRouteData();
|
||||||
|
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
||||||
|
const navigate = useNavigateNoUpdate();
|
||||||
|
const currentPageUid = useCurrentPageUid();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaSettingsItem
|
||||||
|
title="Delete"
|
||||||
|
eventKey="remove"
|
||||||
|
onClick={() => {
|
||||||
|
modal.confirm({
|
||||||
|
title: t('Delete menu item'),
|
||||||
|
content: t('Are you sure you want to delete it?'),
|
||||||
|
onOk: async () => {
|
||||||
|
// 删除对应菜单的路由
|
||||||
|
currentRoute?.id != null && (await deleteRoute(currentRoute.id));
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentPageUid !== currentRoute?.schemaUid &&
|
||||||
|
!findRouteBySchemaUid(currentPageUid, currentRoute?.children)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上一个兄弟节点
|
||||||
|
const prevSibling = findPrevSibling(allAccessRoutes, currentRoute);
|
||||||
|
// 下一个兄弟节点
|
||||||
|
const nextSibling = findNextSibling(allAccessRoutes, currentRoute);
|
||||||
|
|
||||||
|
if (prevSibling || nextSibling) {
|
||||||
|
// 如果删除的是当前打开的页面或分组,需要跳转到上一个页面或分组
|
||||||
|
navigate(`/admin/${prevSibling?.schemaUid || nextSibling?.schemaUid}`);
|
||||||
|
} else {
|
||||||
|
navigate(`/`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Delete')}
|
||||||
|
</SchemaSettingsItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InsertMenuItems = (props) => {
|
||||||
|
const { eventKey, title, insertPosition } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { urlSchema, paramsSchema } = useURLAndHTMLSchema();
|
||||||
|
const currentRoute = useCurrentRouteData();
|
||||||
|
const isSubMenu = currentRoute?.type === NocoBaseDesktopRouteType.group;
|
||||||
|
const { createRoute, moveRoute } = useNocoBaseRoutes();
|
||||||
|
const insertPageSchema = useInsertPageSchema();
|
||||||
|
|
||||||
|
if (!isSubMenu && insertPosition === 'beforeEnd') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaSettingsSubMenu eventKey={eventKey} title={title}>
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
eventKey={`${insertPosition}group`}
|
||||||
|
title={t('Group')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Add group'),
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
title: t('Menu item title'),
|
||||||
|
required: true,
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
title: t('Icon'),
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={async ({ title, icon }) => {
|
||||||
|
const schemaUid = uid();
|
||||||
|
const parentId = insertPosition === 'beforeEnd' ? currentRoute?.id : currentRoute?.parentId;
|
||||||
|
|
||||||
|
// 1. 先创建一个路由
|
||||||
|
const { data } = await createRoute({
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
// 'beforeEnd' 表示的是 Insert inner,此时需要把路由插入到当前路由的内部
|
||||||
|
parentId: parentId || undefined,
|
||||||
|
schemaUid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (insertPositionToMethod[insertPosition]) {
|
||||||
|
// 2. 然后再把路由移动到对应的位置
|
||||||
|
await moveRoute({
|
||||||
|
sourceId: data?.data?.id,
|
||||||
|
targetId: currentRoute?.id as any,
|
||||||
|
sortField: 'sort',
|
||||||
|
method: insertPositionToMethod[insertPosition],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
eventKey={`${insertPosition}page`}
|
||||||
|
title={t('Page')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Add page'),
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
title: t('Menu item title'),
|
||||||
|
required: true,
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
title: t('Icon'),
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={async ({ title, icon }) => {
|
||||||
|
const menuSchemaUid = uid();
|
||||||
|
const pageSchemaUid = uid();
|
||||||
|
const tabSchemaUid = uid();
|
||||||
|
const tabSchemaName = uid();
|
||||||
|
const parentId = insertPosition === 'beforeEnd' ? currentRoute?.id : currentRoute?.parentId;
|
||||||
|
|
||||||
|
// 1. 先创建一个路由
|
||||||
|
const { data } = await createRoute({
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
// 'beforeEnd' 表示的是 Insert inner,此时需要把路由插入到当前路由的内部
|
||||||
|
parentId: parentId || undefined,
|
||||||
|
schemaUid: pageSchemaUid,
|
||||||
|
menuSchemaUid,
|
||||||
|
enableTabs: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: NocoBaseDesktopRouteType.tabs,
|
||||||
|
schemaUid: tabSchemaUid,
|
||||||
|
tabSchemaName,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (insertPositionToMethod[insertPosition]) {
|
||||||
|
// 2. 然后再把路由移动到对应的位置
|
||||||
|
await moveRoute({
|
||||||
|
sourceId: data?.data?.id,
|
||||||
|
targetId: currentRoute?.id,
|
||||||
|
sortField: 'sort',
|
||||||
|
method: insertPositionToMethod[insertPosition],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 插入一个对应的 Schema
|
||||||
|
insertPageSchema(getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
eventKey={`${insertPosition}link`}
|
||||||
|
title={t('Link')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Add link'),
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
title: t('Menu item title'),
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
title: t('Icon'),
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
href: urlSchema,
|
||||||
|
params: paramsSchema,
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={async ({ title, icon, href, params }) => {
|
||||||
|
const schemaUid = uid();
|
||||||
|
const parentId = insertPosition === 'beforeEnd' ? currentRoute?.id : currentRoute?.parentId;
|
||||||
|
|
||||||
|
// 1. 先创建一个路由
|
||||||
|
const { data } = await createRoute({
|
||||||
|
type: NocoBaseDesktopRouteType.link,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
// 'beforeEnd' 表示的是 Insert inner,此时需要把路由插入到当前路由的内部
|
||||||
|
parentId: parentId || undefined,
|
||||||
|
schemaUid,
|
||||||
|
options: {
|
||||||
|
href,
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (insertPositionToMethod[insertPosition]) {
|
||||||
|
// 2. 然后再把路由移动到对应的位置
|
||||||
|
await moveRoute({
|
||||||
|
sourceId: data?.data?.id,
|
||||||
|
targetId: currentRoute?.id,
|
||||||
|
sortField: 'sort',
|
||||||
|
method: insertPositionToMethod[insertPosition],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SchemaSettingsSubMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditMenuItem = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const schema = useMemo(() => {
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
title: t('Edit menu item'),
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
title: t('Menu item title'),
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
title: t('Menu item icon'),
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [t]);
|
||||||
|
const currentRoute = useCurrentRouteData();
|
||||||
|
const { urlSchema, paramsSchema } = useURLAndHTMLSchema();
|
||||||
|
const initialValues = useMemo(() => {
|
||||||
|
return {
|
||||||
|
title: currentRoute.title,
|
||||||
|
icon: currentRoute.icon,
|
||||||
|
};
|
||||||
|
}, [currentRoute.title, currentRoute.icon]);
|
||||||
|
if (currentRoute.type === NocoBaseDesktopRouteType.link) {
|
||||||
|
schema.properties['href'] = urlSchema;
|
||||||
|
schema.properties['params'] = paramsSchema;
|
||||||
|
initialValues['href'] = currentRoute.options.href;
|
||||||
|
initialValues['params'] = currentRoute.options.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { updateRoute } = useNocoBaseRoutes();
|
||||||
|
const onEditSubmit: (values: any) => void = useCallback(({ title, icon, href, params }) => {
|
||||||
|
// 更新菜单对应的路由
|
||||||
|
if (currentRoute.id !== undefined) {
|
||||||
|
updateRoute(currentRoute.id, {
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
options:
|
||||||
|
href || params
|
||||||
|
? {
|
||||||
|
href,
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
title={t('Edit')}
|
||||||
|
eventKey="edit"
|
||||||
|
schema={schema as ISchema}
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={onEditSubmit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const HiddenMenuItem = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const currentRoute = useCurrentRouteData();
|
||||||
|
const { updateRoute } = useNocoBaseRoutes();
|
||||||
|
const { modal } = App.useApp();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaSettingsSwitchItem
|
||||||
|
title={t('Hidden')}
|
||||||
|
checked={currentRoute.hideInMenu}
|
||||||
|
onChange={(value) => {
|
||||||
|
modal.confirm({
|
||||||
|
title: t('Are you sure you want to hide this menu?'),
|
||||||
|
icon: <ExclamationCircleFilled />,
|
||||||
|
content: t(
|
||||||
|
'After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.',
|
||||||
|
),
|
||||||
|
async onOk() {
|
||||||
|
if (currentRoute.id !== undefined) {
|
||||||
|
await updateRoute(currentRoute.id, {
|
||||||
|
hideInMenu: !!value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MoveToMenuItem = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const effects = useCallback(
|
||||||
|
(form) => {
|
||||||
|
onFieldChange('target', (field: Field) => {
|
||||||
|
const [, type] = field?.value?.split?.('||') || [];
|
||||||
|
field.query('position').take((f: Field) => {
|
||||||
|
f.dataSource =
|
||||||
|
type === NocoBaseDesktopRouteType.group
|
||||||
|
? [
|
||||||
|
{ label: t('Before'), value: 'beforeBegin' },
|
||||||
|
{ label: t('After'), value: 'afterEnd' },
|
||||||
|
{ label: t('Inner'), value: 'beforeEnd' },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ label: t('Before'), value: 'beforeBegin' },
|
||||||
|
{ label: t('After'), value: 'afterEnd' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
const compile = useCompile();
|
||||||
|
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
||||||
|
const items = useMemo(() => toItems(allAccessRoutes, { t, compile }), []);
|
||||||
|
const modalSchema = useMemo(() => {
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
title: t('Move to'),
|
||||||
|
properties: {
|
||||||
|
target: {
|
||||||
|
title: t('Target'),
|
||||||
|
enum: items,
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'TreeSelect',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
title: t('Position'),
|
||||||
|
required: true,
|
||||||
|
enum: [
|
||||||
|
{ label: t('Before'), value: 'beforeBegin' },
|
||||||
|
{ label: t('After'), value: 'afterEnd' },
|
||||||
|
],
|
||||||
|
default: 'afterEnd',
|
||||||
|
'x-component': 'Radio.Group',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema;
|
||||||
|
}, [items, t]);
|
||||||
|
|
||||||
|
const { moveRoute } = useNocoBaseRoutes();
|
||||||
|
const currentRoute = useCurrentRouteData();
|
||||||
|
const onMoveToSubmit: (values: any) => void = useCallback(async ({ target, position }) => {
|
||||||
|
const [targetId] = target?.split?.('||') || [];
|
||||||
|
if (!targetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId === undefined || !currentRoute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionToMethod = {
|
||||||
|
beforeBegin: 'prepend',
|
||||||
|
afterEnd: 'insertAfter',
|
||||||
|
};
|
||||||
|
|
||||||
|
await moveRoute({
|
||||||
|
sourceId: currentRoute.id as any,
|
||||||
|
targetId: targetId,
|
||||||
|
sortField: 'sort',
|
||||||
|
method: positionToMethod[position],
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
title={t('Move to')}
|
||||||
|
eventKey="move-to"
|
||||||
|
components={components}
|
||||||
|
effects={effects}
|
||||||
|
schema={modalSchema}
|
||||||
|
onSubmit={onMoveToSubmit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const menuItemSettings = new SchemaSettings({
|
||||||
|
name: 'menuSettings:menuItem',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
Component: EditMenuItem,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hidden',
|
||||||
|
Component: HiddenMenuItem,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'moveTo',
|
||||||
|
Component: MoveToMenuItem,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'divider',
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'insertbeforeBegin',
|
||||||
|
Component: () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<InsertMenuItems eventKey={'insertbeforeBegin'} title={t('Insert before')} insertPosition={'beforeBegin'} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'insertafterEnd',
|
||||||
|
Component: () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return <InsertMenuItems eventKey={'insertafterEnd'} title={t('Insert after')} insertPosition={'afterEnd'} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'insertbeforeEnd',
|
||||||
|
Component: () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return <InsertMenuItems eventKey={'insertbeforeEnd'} title={t('Insert inner')} insertPosition={'beforeEnd'} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'divider',
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
sort: 100,
|
||||||
|
Component: RemoveRoute,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconStyle = css`
|
||||||
|
.anticon {
|
||||||
|
line-height: 16px !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const siderContextValue = { siderCollapsed: false };
|
||||||
|
export const MenuSchemaToolbar: FC<{ container?: HTMLElement }> = (props) => {
|
||||||
|
return (
|
||||||
|
<ResetThemeTokenAndKeepAlgorithm>
|
||||||
|
{/* 避免 Sider 的状态影响到 SchemaToolbar。否则会导致在折叠状态下,SchemaToolbar 的样式异常 */}
|
||||||
|
<SiderContext.Provider value={siderContextValue}>
|
||||||
|
<SchemaToolbar
|
||||||
|
spaceClassName={iconStyle}
|
||||||
|
settings={menuItemSettings}
|
||||||
|
showBorder={false}
|
||||||
|
container={props.container}
|
||||||
|
/>
|
||||||
|
</SiderContext.Provider>
|
||||||
|
</ResetThemeTokenAndKeepAlgorithm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置主题,避免被 ProLayout 的主题影响
|
||||||
|
* @param props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const ResetThemeTokenAndKeepAlgorithm: FC = (props) => {
|
||||||
|
const { theme } = useGlobalTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
...theme,
|
||||||
|
inherit: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -21,7 +21,7 @@ import { ActionContextNoRerender } from './context';
|
|||||||
import { useActionContext } from './hooks';
|
import { useActionContext } from './hooks';
|
||||||
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
|
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
|
||||||
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
||||||
import { useZIndexContext, zIndexContext } from './zIndexContext';
|
import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
|
||||||
|
|
||||||
const MemoizeRecursionField = React.memo(RecursionField);
|
const MemoizeRecursionField = React.memo(RecursionField);
|
||||||
MemoizeRecursionField.displayName = 'MemoizeRecursionField';
|
MemoizeRecursionField.displayName = 'MemoizeRecursionField';
|
||||||
@ -103,7 +103,7 @@ export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
|
|||||||
useSetAriaLabelForDrawer(visible);
|
useSetAriaLabelForDrawer(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zIndex = _zIndex || parentZIndex + (props.level || 0);
|
const zIndex = getZIndex('drawer', _zIndex || parentZIndex, props.level || 0);
|
||||||
|
|
||||||
const onClose = useCallback(
|
const onClose = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
|
@ -22,7 +22,7 @@ import { ActionContextNoRerender } from './context';
|
|||||||
import { useActionContext } from './hooks';
|
import { useActionContext } from './hooks';
|
||||||
import { useSetAriaLabelForModal } from './hooks/useSetAriaLabelForModal';
|
import { useSetAriaLabelForModal } from './hooks/useSetAriaLabelForModal';
|
||||||
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
||||||
import { useZIndexContext, zIndexContext } from './zIndexContext';
|
import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
|
||||||
|
|
||||||
const ModalErrorFallback: React.FC<FallbackProps> = (props) => {
|
const ModalErrorFallback: React.FC<FallbackProps> = (props) => {
|
||||||
const { visible, setVisible } = useActionContext();
|
const { visible, setVisible } = useActionContext();
|
||||||
@ -89,7 +89,7 @@ export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = obse
|
|||||||
useSetAriaLabelForModal(visible);
|
useSetAriaLabelForModal(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zIndex = _zIndex || parentZIndex + (props.level || 0);
|
const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionContextNoRerender>
|
<ActionContextNoRerender>
|
||||||
|
@ -15,7 +15,7 @@ export const useActionPageStyle = genStyleHook('nb-action-page', (token) => {
|
|||||||
return {
|
return {
|
||||||
[componentCls]: {
|
[componentCls]: {
|
||||||
position: 'absolute !important' as any,
|
position: 'absolute !important' as any,
|
||||||
top: 'var(--nb-header-height)',
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
@ -16,7 +16,7 @@ import { BackButtonUsedInSubPage } from '../page/BackButtonUsedInSubPage';
|
|||||||
import { TabsContextProvider, useTabsContext } from '../tabs/context';
|
import { TabsContextProvider, useTabsContext } from '../tabs/context';
|
||||||
import { useActionPageStyle } from './Action.Page.style';
|
import { useActionPageStyle } from './Action.Page.style';
|
||||||
import { usePopupOrSubpagesContainerDOM } from './hooks/usePopupSlotDOM';
|
import { usePopupOrSubpagesContainerDOM } from './hooks/usePopupSlotDOM';
|
||||||
import { useZIndexContext, zIndexContext } from './zIndexContext';
|
import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
|
||||||
|
|
||||||
const ActionPageContent: FC<{ schema: any }> = React.memo(({ schema }) => {
|
const ActionPageContent: FC<{ schema: any }> = React.memo(({ schema }) => {
|
||||||
// Improve the speed of opening the page
|
// Improve the speed of opening the page
|
||||||
@ -45,7 +45,7 @@ export function ActionPage({ level }) {
|
|||||||
|
|
||||||
const style = useMemo(() => {
|
const style = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
zIndex: parentZIndex + (level || 0),
|
zIndex: getZIndex('page', parentZIndex, level || 0),
|
||||||
};
|
};
|
||||||
}, [parentZIndex, level]);
|
}, [parentZIndex, level]);
|
||||||
|
|
||||||
|
@ -14,3 +14,18 @@ export const zIndexContext = React.createContext(100);
|
|||||||
export const useZIndexContext = () => {
|
export const useZIndexContext = () => {
|
||||||
return React.useContext(zIndexContext);
|
return React.useContext(zIndexContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getZIndex = (type: 'page' | 'drawer' | 'modal', basicZIndex: number, level: number) => {
|
||||||
|
let result = basicZIndex;
|
||||||
|
|
||||||
|
// 子页面的 z-index 不能超过 200,不然会遮挡折叠展开菜单的按钮
|
||||||
|
// 注意:嵌入页面时需要跳过,因为嵌入页面中的弹窗不是通过 URL 打开的,会导致子页面被弹窗盖住
|
||||||
|
if (type === 'page' && !window.location.pathname.includes('/embed/')) {
|
||||||
|
result = basicZIndex + level;
|
||||||
|
return result > 200 ? result - 200 : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗的 z-index 需要高一点,不然会被折叠展开按钮遮挡
|
||||||
|
result = basicZIndex + level;
|
||||||
|
return result < 200 ? result + 200 : result;
|
||||||
|
};
|
||||||
|
@ -30,6 +30,7 @@ import {
|
|||||||
useDesignable,
|
useDesignable,
|
||||||
useURLAndHTMLSchema,
|
useURLAndHTMLSchema,
|
||||||
} from '../../../';
|
} from '../../../';
|
||||||
|
import { useInsertPageSchema } from '../../../modules/menu/PageMenuItem';
|
||||||
import { NocoBaseDesktopRouteType } from '../../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
import { NocoBaseDesktopRouteType } from '../../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
||||||
|
|
||||||
const insertPositionToMethod = {
|
const insertPositionToMethod = {
|
||||||
@ -71,11 +72,11 @@ const findMenuSchema = (fieldSchema: Schema) => {
|
|||||||
const InsertMenuItems = (props) => {
|
const InsertMenuItems = (props) => {
|
||||||
const { eventKey, title, insertPosition } = props;
|
const { eventKey, title, insertPosition } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { dn } = useDesignable();
|
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { urlSchema, paramsSchema } = useURLAndHTMLSchema();
|
const { urlSchema, paramsSchema } = useURLAndHTMLSchema();
|
||||||
const isSubMenu = fieldSchema['x-component'] === 'Menu.SubMenu';
|
const isSubMenu = fieldSchema['x-component'] === 'Menu.SubMenu';
|
||||||
const { createRoute, moveRoute } = useNocoBaseRoutes();
|
const { createRoute, moveRoute } = useNocoBaseRoutes();
|
||||||
|
const insertPageSchema = useInsertPageSchema();
|
||||||
|
|
||||||
if (!isSubMenu && insertPosition === 'beforeEnd') {
|
if (!isSubMenu && insertPosition === 'beforeEnd') {
|
||||||
return null;
|
return null;
|
||||||
@ -131,18 +132,6 @@ const InsertMenuItems = (props) => {
|
|||||||
method: insertPositionToMethod[insertPosition],
|
method: insertPositionToMethod[insertPosition],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 插入一个对应的 Schema
|
|
||||||
dn.insertAdjacent(insertPosition, {
|
|
||||||
type: 'void',
|
|
||||||
title,
|
|
||||||
'x-component': 'Menu.SubMenu',
|
|
||||||
'x-decorator': 'ACLMenuItemProvider',
|
|
||||||
'x-component-props': {
|
|
||||||
icon,
|
|
||||||
},
|
|
||||||
'x-uid': schemaUid,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -208,17 +197,7 @@ const InsertMenuItems = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 插入一个对应的 Schema
|
// 3. 插入一个对应的 Schema
|
||||||
dn.insertAdjacent(
|
insertPageSchema(getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }));
|
||||||
insertPosition,
|
|
||||||
getPageMenuSchema({
|
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
pageSchemaUid,
|
|
||||||
menuSchemaUid,
|
|
||||||
tabSchemaUid,
|
|
||||||
tabSchemaName,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SchemaSettingsModalItem
|
<SchemaSettingsModalItem
|
||||||
@ -276,20 +255,6 @@ const InsertMenuItems = (props) => {
|
|||||||
method: insertPositionToMethod[insertPosition],
|
method: insertPositionToMethod[insertPosition],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 插入一个对应的 Schema
|
|
||||||
dn.insertAdjacent(insertPosition, {
|
|
||||||
type: 'void',
|
|
||||||
title,
|
|
||||||
'x-component': 'Menu.URL',
|
|
||||||
'x-decorator': 'ACLMenuItemProvider',
|
|
||||||
'x-component-props': {
|
|
||||||
icon,
|
|
||||||
href,
|
|
||||||
params,
|
|
||||||
},
|
|
||||||
'x-uid': schemaUid,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SchemaSettingsSubMenu>
|
</SchemaSettingsSubMenu>
|
||||||
@ -321,14 +286,14 @@ export const MenuDesigner = () => {
|
|||||||
f.dataSource =
|
f.dataSource =
|
||||||
component === 'Menu.SubMenu'
|
component === 'Menu.SubMenu'
|
||||||
? [
|
? [
|
||||||
{ label: t('Before'), value: 'beforeBegin' },
|
{ label: t('Before'), value: 'beforeBegin' },
|
||||||
{ label: t('After'), value: 'afterEnd' },
|
{ label: t('After'), value: 'afterEnd' },
|
||||||
{ label: t('Inner'), value: 'beforeEnd' },
|
{ label: t('Inner'), value: 'beforeEnd' },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ label: t('Before'), value: 'beforeBegin' },
|
{ label: t('Before'), value: 'beforeBegin' },
|
||||||
{ label: t('After'), value: 'afterEnd' },
|
{ label: t('After'), value: 'afterEnd' },
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -404,9 +369,9 @@ export const MenuDesigner = () => {
|
|||||||
options:
|
options:
|
||||||
href || params
|
href || params
|
||||||
? {
|
? {
|
||||||
href,
|
href,
|
||||||
params,
|
params,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -203,7 +203,7 @@ type ComposedMenu = React.FC<any> & {
|
|||||||
Designer?: React.FC<any>;
|
Designer?: React.FC<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ParentRouteContext = createContext<NocoBaseDesktopRoute>(null);
|
export const ParentRouteContext = createContext<NocoBaseDesktopRoute>(null);
|
||||||
ParentRouteContext.displayName = 'ParentRouteContext';
|
ParentRouteContext.displayName = 'ParentRouteContext';
|
||||||
|
|
||||||
export const useParentRoute = () => {
|
export const useParentRoute = () => {
|
||||||
@ -264,8 +264,8 @@ export const useNocoBaseRoutes = (collectionName = 'desktopRoutes') => {
|
|||||||
method,
|
method,
|
||||||
refreshAfterMove = true,
|
refreshAfterMove = true,
|
||||||
}: {
|
}: {
|
||||||
sourceId: string;
|
sourceId: string | number;
|
||||||
targetId?: string;
|
targetId?: string | number;
|
||||||
targetScope?: any;
|
targetScope?: any;
|
||||||
sortField?: string;
|
sortField?: string;
|
||||||
sticky?: boolean;
|
sticky?: boolean;
|
||||||
|
@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useDesignable, useNocoBaseRoutes } from '../..';
|
import { useDesignable, useNocoBaseRoutes } from '../..';
|
||||||
import { SchemaSettings } from '../../../application/schema-settings';
|
import { SchemaSettings } from '../../../application/schema-settings';
|
||||||
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
||||||
import { useCurrentRoute } from '../../../route-switch';
|
import { useCurrentRouteData } from '../../../route-switch';
|
||||||
|
|
||||||
function useNotDisableHeader() {
|
function useNotDisableHeader() {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
@ -133,7 +133,7 @@ export const pageSettings = new SchemaSettings({
|
|||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const currentRoute = useCurrentRoute();
|
const currentRoute = useCurrentRouteData();
|
||||||
const { updateRoute } = useNocoBaseRoutes();
|
const { updateRoute } = useNocoBaseRoutes();
|
||||||
return {
|
return {
|
||||||
title: t('Enable page tabs'),
|
title: t('Enable page tabs'),
|
||||||
@ -143,6 +143,16 @@ export const pageSettings = new SchemaSettings({
|
|||||||
await updateRoute(currentRoute.id, {
|
await updateRoute(currentRoute.id, {
|
||||||
enableTabs: v,
|
enableTabs: v,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// enableTabs 已经保存在 route 中了,按说这里不需要加了。但 E2E 中需要这个参数。
|
||||||
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
|
fieldSchema['x-component-props']['enablePageTabs'] = v;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
['x-component-props']: fieldSchema['x-component-props'],
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@ import { PlusOutlined } from '@ant-design/icons';
|
|||||||
import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
|
import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { FormLayout } from '@formily/antd-v5';
|
import { FormLayout } from '@formily/antd-v5';
|
||||||
import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react';
|
import { SchemaOptionsContext, useFieldSchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { Button, Tabs } from 'antd';
|
import { Button, Tabs } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@ -25,18 +25,24 @@ import {
|
|||||||
CurrentTabUidContext,
|
CurrentTabUidContext,
|
||||||
useCurrentSearchParams,
|
useCurrentSearchParams,
|
||||||
useCurrentTabUid,
|
useCurrentTabUid,
|
||||||
|
useLocationNoUpdate,
|
||||||
useNavigateNoUpdate,
|
useNavigateNoUpdate,
|
||||||
useRouterBasename,
|
useRouterBasename,
|
||||||
} from '../../../application/CustomRouterContextProvider';
|
} from '../../../application/CustomRouterContextProvider';
|
||||||
import { useDocumentTitle } from '../../../document-title';
|
import { useDocumentTitle } from '../../../document-title';
|
||||||
import { useGlobalTheme } from '../../../global-theme';
|
import { useGlobalTheme } from '../../../global-theme';
|
||||||
import { Icon } from '../../../icon';
|
import { Icon } from '../../../icon';
|
||||||
import { NocoBaseDesktopRouteType, useCurrentRoute } from '../../../route-switch/antd/admin-layout';
|
import { AppNotFound } from '../../../nocobase-buildin-plugin';
|
||||||
|
import {
|
||||||
|
NocoBaseDesktopRouteType,
|
||||||
|
NocoBaseRouteContext,
|
||||||
|
useCurrentRouteData,
|
||||||
|
} from '../../../route-switch/antd/admin-layout';
|
||||||
import { KeepAliveProvider, useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
import { KeepAliveProvider, useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
||||||
import { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer';
|
import { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer';
|
||||||
import { DndContext } from '../../common';
|
import { DndContext } from '../../common';
|
||||||
import { SortableItem } from '../../common/sortable-item';
|
import { SortableItem } from '../../common/sortable-item';
|
||||||
import { SchemaComponent, SchemaComponentOptions } from '../../core';
|
import { RemoteSchemaComponent, SchemaComponent, SchemaComponentOptions } from '../../core';
|
||||||
import { useCompile, useDesignable } from '../../hooks';
|
import { useCompile, useDesignable } from '../../hooks';
|
||||||
import { useToken } from '../__builtins__';
|
import { useToken } from '../__builtins__';
|
||||||
import { ErrorFallback } from '../error-fallback';
|
import { ErrorFallback } from '../error-fallback';
|
||||||
@ -60,12 +66,9 @@ const InternalPage = React.memo((props: PageProps) => {
|
|||||||
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
|
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
|
||||||
const searchParams = useCurrentSearchParams();
|
const searchParams = useCurrentSearchParams();
|
||||||
const loading = false;
|
const loading = false;
|
||||||
const currentRoute = useCurrentRoute();
|
const currentRoute = useCurrentRouteData();
|
||||||
const enablePageTabs = currentRoute.enableTabs;
|
const enablePageTabs = currentRoute.enableTabs;
|
||||||
const defaultActiveKey = useMemo(
|
const defaultActiveKey = currentRoute?.children?.[0]?.schemaUid;
|
||||||
() => getDefaultActiveKey(currentRoute?.children?.[0]?.schemaUid, fieldSchema),
|
|
||||||
[currentRoute?.children, fieldSchema],
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeKey = useMemo(
|
const activeKey = useMemo(
|
||||||
// 处理 searchParams 是为了兼容旧版的 tab 参数
|
// 处理 searchParams 是为了兼容旧版的 tab 参数
|
||||||
@ -74,8 +77,8 @@ const InternalPage = React.memo((props: PageProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const outletContext = useMemo(
|
const outletContext = useMemo(
|
||||||
() => ({ loading, disablePageHeader, enablePageTabs, fieldSchema, tabUid: currentTabUid }),
|
() => ({ loading, disablePageHeader, enablePageTabs, tabUid: currentTabUid }),
|
||||||
[currentTabUid, disablePageHeader, enablePageTabs, fieldSchema, loading],
|
[currentTabUid, disablePageHeader, enablePageTabs, loading],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -92,7 +95,6 @@ const InternalPage = React.memo((props: PageProps) => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
disablePageHeader={disablePageHeader}
|
disablePageHeader={disablePageHeader}
|
||||||
enablePageTabs={enablePageTabs}
|
enablePageTabs={enablePageTabs}
|
||||||
fieldSchema={fieldSchema}
|
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
/>
|
/>
|
||||||
{/* Used to match the route with name "admin.page.popup" */}
|
{/* Used to match the route with name "admin.page.popup" */}
|
||||||
@ -135,14 +137,13 @@ export const Page = React.memo((props: PageProps) => {
|
|||||||
Page.displayName = 'NocoBasePage';
|
Page.displayName = 'NocoBasePage';
|
||||||
|
|
||||||
export const PageTabs = () => {
|
export const PageTabs = () => {
|
||||||
const { loading, disablePageHeader, enablePageTabs, fieldSchema, tabUid } = useOutletContext<any>();
|
const { loading, disablePageHeader, enablePageTabs, tabUid } = useOutletContext<any>();
|
||||||
return (
|
return (
|
||||||
<CurrentTabUidContext.Provider value={tabUid}>
|
<CurrentTabUidContext.Provider value={tabUid}>
|
||||||
<PageContent
|
<PageContent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disablePageHeader={disablePageHeader}
|
disablePageHeader={disablePageHeader}
|
||||||
enablePageTabs={enablePageTabs}
|
enablePageTabs={enablePageTabs}
|
||||||
fieldSchema={fieldSchema}
|
|
||||||
activeKey={tabUid}
|
activeKey={tabUid}
|
||||||
/>
|
/>
|
||||||
{/* used to match the route with name "admin.page.tab.popup" */}
|
{/* used to match the route with name "admin.page.tab.popup" */}
|
||||||
@ -170,7 +171,7 @@ const displayNone = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add a TabPane component to manage caching, implementing an effect similar to Vue's keep-alive
|
// Add a TabPane component to manage caching, implementing an effect similar to Vue's keep-alive
|
||||||
const TabPane = React.memo(({ schema, active: tabActive }: { schema: Schema; active: boolean }) => {
|
const TabPane = React.memo(({ active: tabActive, uid }: { active: boolean; uid: string }) => {
|
||||||
const mountedRef = useRef(false);
|
const mountedRef = useRef(false);
|
||||||
const { active: pageActive } = useKeepAlive();
|
const { active: pageActive } = useKeepAlive();
|
||||||
|
|
||||||
@ -178,16 +179,6 @@ const TabPane = React.memo(({ schema, active: tabActive }: { schema: Schema; act
|
|||||||
mountedRef.current = true;
|
mountedRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSchema = useMemo(
|
|
||||||
() =>
|
|
||||||
new Schema({
|
|
||||||
properties: {
|
|
||||||
[schema.name]: schema,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[schema],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mountedRef.current) {
|
if (!mountedRef.current) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -195,7 +186,7 @@ const TabPane = React.memo(({ schema, active: tabActive }: { schema: Schema; act
|
|||||||
return (
|
return (
|
||||||
<div style={tabActive ? displayBlock : displayNone}>
|
<div style={tabActive ? displayBlock : displayNone}>
|
||||||
<KeepAliveProvider active={pageActive && tabActive}>
|
<KeepAliveProvider active={pageActive && tabActive}>
|
||||||
<SchemaComponent distributed schema={newSchema} />
|
<RemoteSchemaComponent uid={uid} />
|
||||||
</KeepAliveProvider>
|
</KeepAliveProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -205,26 +196,48 @@ interface PageContentProps {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
disablePageHeader: any;
|
disablePageHeader: any;
|
||||||
enablePageTabs: any;
|
enablePageTabs: any;
|
||||||
fieldSchema: Schema<any, any, any, any, any, any, any, any, any>;
|
|
||||||
activeKey: string;
|
activeKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InternalPageContent = (props: PageContentProps) => {
|
const InternalPageContent = (props: PageContentProps) => {
|
||||||
const { loading, disablePageHeader, enablePageTabs, fieldSchema, activeKey } = props;
|
const { loading, disablePageHeader, enablePageTabs, activeKey } = props;
|
||||||
|
const currentRoute = useCurrentRouteData();
|
||||||
|
const navigate = useNavigateNoUpdate();
|
||||||
|
const location = useLocationNoUpdate();
|
||||||
|
|
||||||
|
const children = currentRoute?.children || [];
|
||||||
|
const noTabs = children.every((tabRoute) => tabRoute.schemaUid !== activeKey && tabRoute.tabSchemaName !== activeKey);
|
||||||
|
|
||||||
|
if (activeKey && noTabs) {
|
||||||
|
return <AppNotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容旧版本的 tab 路径
|
||||||
|
const oldTab = currentRoute?.children?.find((tabRoute) => tabRoute.tabSchemaName === activeKey);
|
||||||
|
if (oldTab) {
|
||||||
|
navigate(location.pathname.replace(activeKey, oldTab.schemaUid));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!disablePageHeader && enablePageTabs) {
|
if (!disablePageHeader && enablePageTabs) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{fieldSchema.mapProperties((schema) => (
|
{currentRoute.children?.map((tabRoute) => {
|
||||||
<TabPane key={schema.name} schema={schema} active={schema.name === activeKey} />
|
return (
|
||||||
))}
|
<NocoBaseRouteContext.Provider value={tabRoute} key={tabRoute.schemaUid}>
|
||||||
|
<TabPane active={tabRoute.schemaUid === activeKey} uid={tabRoute.schemaUid} />
|
||||||
|
</NocoBaseRouteContext.Provider>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className1}>
|
<div className={className1}>
|
||||||
<SchemaComponent schema={fieldSchema} distributed />
|
<NocoBaseRouteContext.Provider value={currentRoute?.children?.[0]}>
|
||||||
|
<RemoteSchemaComponent uid={currentRoute?.children?.[0].schemaUid} />
|
||||||
|
</NocoBaseRouteContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -254,7 +267,7 @@ const NocoBasePageHeaderTabs: FC<{ className: string; activeKey: string }> = ({
|
|||||||
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
|
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
|
||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
const { theme } = useGlobalTheme();
|
const { theme } = useGlobalTheme();
|
||||||
const currentRoute = useCurrentRoute();
|
const currentRoute = useCurrentRouteData();
|
||||||
const { createRoute } = useNocoBaseRoutes();
|
const { createRoute } = useNocoBaseRoutes();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
|
||||||
@ -334,29 +347,30 @@ const NocoBasePageHeaderTabs: FC<{ className: string; activeKey: string }> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
return fieldSchema
|
return currentRoute?.children
|
||||||
.mapProperties((schema) => {
|
?.map((tabRoute) => {
|
||||||
const tabRoute = currentRoute?.children?.find((route) => route.schemaUid === schema['x-uid']);
|
|
||||||
if (!tabRoute || tabRoute.hideInMenu) {
|
if (!tabRoute || tabRoute.hideInMenu) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将 tabRoute 挂载到 schema 上,以方便获取
|
// fake schema used to pass routing information to SortableItem
|
||||||
(schema as any).__route__ = tabRoute;
|
const fakeSchema: any = { __route__: tabRoute };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: (
|
label: (
|
||||||
<SortableItem
|
<NocoBaseRouteContext.Provider value={tabRoute}>
|
||||||
id={schema.name as string}
|
<SortableItem
|
||||||
schema={schema}
|
id={String(tabRoute.id)}
|
||||||
className={classNames('nb-action-link', 'designerCss', className)}
|
className={classNames('nb-action-link', 'designerCss', className)}
|
||||||
>
|
schema={fakeSchema}
|
||||||
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
|
>
|
||||||
<span>{(tabRoute.title && routeT(compile(tabRoute.title))) || t('Unnamed')}</span>
|
{tabRoute.icon && <Icon style={{ marginRight: 8 }} type={tabRoute.icon} />}
|
||||||
<PageTabDesigner schema={schema} />
|
<span>{(tabRoute.title && routeT(compile(tabRoute.title))) || t('Unnamed')}</span>
|
||||||
</SortableItem>
|
<PageTabDesigner />
|
||||||
|
</SortableItem>
|
||||||
|
</NocoBaseRouteContext.Provider>
|
||||||
),
|
),
|
||||||
key: schema.name as string,
|
key: tabRoute.schemaUid,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@ -392,10 +406,12 @@ const NocoBasePageHeader = React.memo(({ activeKey, className }: { activeKey: st
|
|||||||
const [pageTitle, setPageTitle] = useState(() => t(fieldSchema.title));
|
const [pageTitle, setPageTitle] = useState(() => t(fieldSchema.title));
|
||||||
|
|
||||||
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
|
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
|
||||||
const currentRoute = useCurrentRoute();
|
const currentRoute = useCurrentRouteData();
|
||||||
const enablePageTabs = currentRoute.enableTabs;
|
const enablePageTabs = currentRoute.enableTabs;
|
||||||
const hidePageTitle = fieldSchema['x-component-props']?.hidePageTitle;
|
const hidePageTitle = fieldSchema['x-component-props']?.hidePageTitle;
|
||||||
|
|
||||||
|
const { token } = useToken();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const title = t(fieldSchema.title) || t(currentRoute?.title);
|
const title = t(fieldSchema.title) || t(currentRoute?.title);
|
||||||
if (title) {
|
if (title) {
|
||||||
@ -410,6 +426,9 @@ const NocoBasePageHeader = React.memo(({ activeKey, className }: { activeKey: st
|
|||||||
{!disablePageHeader && (
|
{!disablePageHeader && (
|
||||||
<AntdPageHeader
|
<AntdPageHeader
|
||||||
className={classNames('pageHeaderCss', pageTitle || enablePageTabs ? '' : 'height0')}
|
className={classNames('pageHeaderCss', pageTitle || enablePageTabs ? '' : 'height0')}
|
||||||
|
style={{
|
||||||
|
paddingBottom: currentRoute.enableTabs || hidePageTitle ? 0 : token.paddingSM,
|
||||||
|
}}
|
||||||
ghost={false}
|
ghost={false}
|
||||||
// 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0
|
// 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0
|
||||||
title={hidePageTitle ? ' ' : (!fieldSchema.title && pageTitle ? routeT(pageTitle) : pageTitle) || ' '}
|
title={hidePageTitle ? ' ' : (!fieldSchema.title && pageTitle ? routeT(pageTitle) : pageTitle) || ' '}
|
||||||
@ -480,19 +499,6 @@ export function getTabSchema({
|
|||||||
'x-initializer': 'page:addBlock',
|
'x-initializer': 'page:addBlock',
|
||||||
properties: {},
|
properties: {},
|
||||||
'x-uid': schemaUid,
|
'x-uid': schemaUid,
|
||||||
|
'x-async': true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultActiveKey(defaultTabSchemaUid: string, fieldSchema: Schema) {
|
|
||||||
if (!fieldSchema.properties) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabSchemaList = Object.values(fieldSchema.properties);
|
|
||||||
|
|
||||||
for (const tabSchema of tabSchemaList) {
|
|
||||||
if (tabSchema['x-uid'] === defaultTabSchemaUid) {
|
|
||||||
return tabSchema.name as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -9,12 +9,13 @@
|
|||||||
|
|
||||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
import { ISchema } from '@formily/json-schema';
|
import { ISchema } from '@formily/json-schema';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { App, Modal } from 'antd';
|
import { App, Modal } from 'antd';
|
||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider';
|
||||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||||
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
import { useCurrentRouteData } from '../../../route-switch';
|
||||||
import { useDesignable } from '../../hooks';
|
import { useDesignable } from '../../hooks';
|
||||||
import { useNocoBaseRoutes } from '../menu/Menu';
|
import { useNocoBaseRoutes } from '../menu/Menu';
|
||||||
|
|
||||||
@ -29,9 +30,8 @@ export const pageTabSettings = new SchemaSettings({
|
|||||||
type: 'modal',
|
type: 'modal',
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { schema } = useSchemaToolbar<{ schema: ISchema }>();
|
|
||||||
const { dn } = useDesignable();
|
|
||||||
const { updateRoute } = useNocoBaseRoutes();
|
const { updateRoute } = useNocoBaseRoutes();
|
||||||
|
const currentRoute = useCurrentRouteData();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
@ -54,20 +54,10 @@ export const pageTabSettings = new SchemaSettings({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
initialValues: { title: schema.title, icon: schema['x-icon'] },
|
initialValues: { title: currentRoute.title, icon: currentRoute.icon },
|
||||||
onSubmit: ({ title, icon }) => {
|
onSubmit: ({ title, icon }) => {
|
||||||
schema.title = title;
|
|
||||||
schema['x-icon'] = icon;
|
|
||||||
dn.emit('patch', {
|
|
||||||
schema: {
|
|
||||||
['x-uid']: schema['x-uid'],
|
|
||||||
title,
|
|
||||||
'x-icon': icon,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新路由
|
// 更新路由
|
||||||
updateRoute(schema['__route__'].id, {
|
updateRoute(currentRoute.id, {
|
||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
});
|
});
|
||||||
@ -80,33 +70,21 @@ export const pageTabSettings = new SchemaSettings({
|
|||||||
type: 'switch',
|
type: 'switch',
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { schema } = useSchemaToolbar<{ schema: ISchema }>();
|
|
||||||
const { updateRoute } = useNocoBaseRoutes();
|
const { updateRoute } = useNocoBaseRoutes();
|
||||||
const { dn } = useDesignable();
|
const currentRoute = useCurrentRouteData();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('Hidden'),
|
title: t('Hidden'),
|
||||||
checked: schema['x-component-props']?.hidden,
|
checked: currentRoute.hideInMenu,
|
||||||
onChange: (v) => {
|
onChange: (v) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确定要隐藏该菜单吗?',
|
title: t('Are you sure you want to hide this tab?'),
|
||||||
icon: <ExclamationCircleFilled />,
|
icon: <ExclamationCircleFilled />,
|
||||||
content: '隐藏后,该菜单将不再显示在菜单栏中。如需再次显示,需要去路由管理页面设置。',
|
content: t('After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.'),
|
||||||
async onOk() {
|
async onOk() {
|
||||||
_.set(schema, 'x-component-props.hidden', !!v);
|
// Update the route corresponding to the menu
|
||||||
|
await updateRoute(currentRoute.id, {
|
||||||
// 更新菜单对应的路由
|
hideInMenu: !!v,
|
||||||
if (schema['__route__']?.id) {
|
|
||||||
await updateRoute(schema['__route__'].id, {
|
|
||||||
hideInMenu: !!v,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dn.emit('patch', {
|
|
||||||
schema: {
|
|
||||||
'x-uid': schema['x-uid'],
|
|
||||||
'x-component-props': schema['x-component-props'],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -125,7 +103,11 @@ export const pageTabSettings = new SchemaSettings({
|
|||||||
const { modal } = App.useApp();
|
const { modal } = App.useApp();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { schema } = useSchemaToolbar();
|
const { deleteRoute } = useNocoBaseRoutes();
|
||||||
|
const currentRoute = useCurrentRouteData();
|
||||||
|
const navigate = useNavigateNoUpdate();
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('Delete'),
|
title: t('Delete'),
|
||||||
eventKey: 'remove',
|
eventKey: 'remove',
|
||||||
@ -134,8 +116,18 @@ export const pageTabSettings = new SchemaSettings({
|
|||||||
title: t('Delete block'),
|
title: t('Delete block'),
|
||||||
content: t('Are you sure you want to delete it?'),
|
content: t('Are you sure you want to delete it?'),
|
||||||
...confirm,
|
...confirm,
|
||||||
onOk() {
|
async onOk() {
|
||||||
dn.remove(schema);
|
await deleteRoute(currentRoute.id);
|
||||||
|
dn.emit('remove', {
|
||||||
|
removed: {
|
||||||
|
'x-uid': currentRoute.schemaUid,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果删除的是当前打开的 tab,需要跳转到其他 tab
|
||||||
|
if (window.location.pathname.includes(currentRoute.schemaUid)) {
|
||||||
|
navigate(`/admin/${schema['x-uid']}`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DragOutlined } from '@ant-design/icons';
|
import { DragOutlined } from '@ant-design/icons';
|
||||||
import { useFieldSchema } from '@formily/react';
|
|
||||||
import { Space } from 'antd';
|
import { Space } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DragHandler, useDesignable } from '../..';
|
import { DragHandler, useDesignable } from '../..';
|
||||||
@ -31,29 +30,25 @@ export const PageDesigner = ({ title }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageTabDesigner = ({ schema }) => {
|
export const PageTabDesigner = () => {
|
||||||
const { designable } = useDesignable();
|
const { designable } = useDesignable();
|
||||||
const { getAriaLabel } = useGetAriaLabelOfDesigner();
|
const { getAriaLabel } = useGetAriaLabelOfDesigner();
|
||||||
const fieldSchema = useFieldSchema();
|
const { render } = useSchemaSettingsRender('PageTabSettings');
|
||||||
const { render } = useSchemaSettingsRender(
|
|
||||||
fieldSchema['x-settings'] || 'PageTabSettings',
|
|
||||||
fieldSchema['x-settings-props'],
|
|
||||||
);
|
|
||||||
if (!designable) {
|
if (!designable) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaToolbarProvider schema={schema}>
|
<div className={'general-schema-designer'}>
|
||||||
<div className={'general-schema-designer'}>
|
<div className={'general-schema-designer-icons'}>
|
||||||
<div className={'general-schema-designer-icons'}>
|
<Space size={3} align={'center'}>
|
||||||
<Space size={3} align={'center'}>
|
<DragHandler>
|
||||||
<DragHandler>
|
<DragOutlined style={{ marginRight: 0 }} role="button" aria-label={getAriaLabel('drag-handler', 'tab')} />
|
||||||
<DragOutlined style={{ marginRight: 0 }} role="button" aria-label={getAriaLabel('drag-handler', 'tab')} />
|
</DragHandler>
|
||||||
</DragHandler>
|
{render()}
|
||||||
{render()}
|
</Space>
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</SchemaToolbarProvider>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,26 +8,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DocumentTitleProvider, Form, FormItem, Grid, IconPicker, Input } from '@nocobase/client';
|
import { DocumentTitleProvider, Form, FormItem, Grid, IconPicker, Input } from '@nocobase/client';
|
||||||
import { render, renderAppOptions, screen, userEvent, waitFor } from '@nocobase/test/client';
|
import { renderAppOptions, screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||||
import React from 'react';
|
|
||||||
import App1 from '../demos/demo1';
|
|
||||||
import { isTabPage, navigateToTab, Page } from '../Page';
|
import { isTabPage, navigateToTab, Page } from '../Page';
|
||||||
|
|
||||||
describe('Page', () => {
|
describe('Page', () => {
|
||||||
it('should render correctly', async () => {
|
|
||||||
render(<App1 />);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(/page title/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(/page content/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(document.title).toBe('Page Title - NocoBase');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Page Component', () => {
|
describe('Page Component', () => {
|
||||||
const title = 'Test Title';
|
const title = 'Test Title';
|
||||||
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
import { ISchema } from '@formily/react';
|
|
||||||
import { DocumentTitleProvider, Page, SchemaComponent, SchemaComponentProvider, Application } from '@nocobase/client';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const schema: ISchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
page1: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Page',
|
|
||||||
title: 'Page Title',
|
|
||||||
properties: {
|
|
||||||
content: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'div',
|
|
||||||
'x-content': 'Page Content',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const Root = () => {
|
|
||||||
return (
|
|
||||||
<SchemaComponentProvider components={{ Page }}>
|
|
||||||
<DocumentTitleProvider addonAfter={'NocoBase'}>
|
|
||||||
<SchemaComponent schema={schema} />
|
|
||||||
</DocumentTitleProvider>
|
|
||||||
</SchemaComponentProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const app = new Application({
|
|
||||||
providers: [Root],
|
|
||||||
});
|
|
||||||
|
|
||||||
export default app.getRootComponent();
|
|
@ -84,4 +84,5 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
|||||||
export const RemoteSchemaComponent: React.FC<RemoteSchemaComponentProps> = memo((props) => {
|
export const RemoteSchemaComponent: React.FC<RemoteSchemaComponentProps> = memo((props) => {
|
||||||
return props.uid ? <RequestSchemaComponent {...props} /> : null;
|
return props.uid ? <RequestSchemaComponent {...props} /> : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
RemoteSchemaComponent.displayName = 'RemoteSchemaComponent';
|
RemoteSchemaComponent.displayName = 'RemoteSchemaComponent';
|
||||||
|
@ -17,13 +17,14 @@ import React, {
|
|||||||
FC,
|
FC,
|
||||||
startTransition,
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
useContext,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SchemaComponentContext } from '../';
|
||||||
import { SchemaInitializer, SchemaSettings, SchemaToolbarProvider, useSchemaInitializerRender } from '../application';
|
import { SchemaInitializer, SchemaSettings, SchemaToolbarProvider, useSchemaInitializerRender } from '../application';
|
||||||
import { useSchemaSettingsRender } from '../application/schema-settings/hooks/useSchemaSettingsRender';
|
import { useSchemaSettingsRender } from '../application/schema-settings/hooks/useSchemaSettingsRender';
|
||||||
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
|
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
|
||||||
@ -33,7 +34,6 @@ import { DragHandler, useCompile, useDesignable, useGridContext, useGridRowConte
|
|||||||
import { gridRowColWrap } from '../schema-initializer/utils';
|
import { gridRowColWrap } from '../schema-initializer/utils';
|
||||||
import { SchemaSettingsDropdown } from './SchemaSettings';
|
import { SchemaSettingsDropdown } from './SchemaSettings';
|
||||||
import { useGetAriaLabelOfDesigner } from './hooks/useGetAriaLabelOfDesigner';
|
import { useGetAriaLabelOfDesigner } from './hooks/useGetAriaLabelOfDesigner';
|
||||||
import { SchemaComponentContext } from '../';
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
const titleCss = css`
|
const titleCss = css`
|
||||||
@ -208,6 +208,11 @@ export interface SchemaToolbarProps {
|
|||||||
spaceWrapperStyle?: React.CSSProperties;
|
spaceWrapperStyle?: React.CSSProperties;
|
||||||
spaceClassName?: string;
|
spaceClassName?: string;
|
||||||
spaceStyle?: React.CSSProperties;
|
spaceStyle?: React.CSSProperties;
|
||||||
|
/**
|
||||||
|
* The HTML element that listens for mouse enter/leave events.
|
||||||
|
* Parent element is used by default.
|
||||||
|
*/
|
||||||
|
container?: HTMLElement;
|
||||||
onVisibleChange?: (nextVisible: boolean) => void;
|
onVisibleChange?: (nextVisible: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +231,7 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = React.memo((props) => {
|
|||||||
spaceStyle,
|
spaceStyle,
|
||||||
toolbarClassName,
|
toolbarClassName,
|
||||||
toolbarStyle = {},
|
toolbarStyle = {},
|
||||||
|
container,
|
||||||
} = {
|
} = {
|
||||||
...props,
|
...props,
|
||||||
...(fieldSchema?.['x-toolbar-props'] || {}),
|
...(fieldSchema?.['x-toolbar-props'] || {}),
|
||||||
@ -312,7 +318,10 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = React.memo((props) => {
|
|||||||
while (parentElement && parentElement.clientHeight === 0) {
|
while (parentElement && parentElement.clientHeight === 0) {
|
||||||
parentElement = parentElement.parentElement;
|
parentElement = parentElement.parentElement;
|
||||||
}
|
}
|
||||||
if (!parentElement) {
|
|
||||||
|
const el = container || parentElement;
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,18 +339,18 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = React.memo((props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = window.getComputedStyle(parentElement);
|
// const style = window.getComputedStyle(parentElement);
|
||||||
if (style.position === 'static') {
|
// if (style.position === 'static') {
|
||||||
parentElement.style.position = 'relative';
|
// parentElement.style.position = 'relative';
|
||||||
}
|
// }
|
||||||
|
|
||||||
parentElement.addEventListener('mouseenter', show);
|
el.addEventListener('mouseenter', show);
|
||||||
parentElement.addEventListener('mouseleave', hide);
|
el.addEventListener('mouseleave', hide);
|
||||||
return () => {
|
return () => {
|
||||||
parentElement.removeEventListener('mouseenter', show);
|
el.removeEventListener('mouseenter', show);
|
||||||
parentElement.removeEventListener('mouseleave', hide);
|
el.removeEventListener('mouseleave', hide);
|
||||||
};
|
};
|
||||||
}, [props.onVisibleChange]);
|
}, [props.onVisibleChange, container]);
|
||||||
|
|
||||||
const containerStyle = useMemo(
|
const containerStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -20,7 +20,7 @@ export const useGetAriaLabelOfDesigner = () => {
|
|||||||
const { name: _collectionName } = useCollection_deprecated();
|
const { name: _collectionName } = useCollection_deprecated();
|
||||||
const getAriaLabel = useCallback(
|
const getAriaLabel = useCallback(
|
||||||
(name: string, postfix?: string) => {
|
(name: string, postfix?: string) => {
|
||||||
if (!fieldSchema) return '';
|
if (!fieldSchema) return `designer-${name}-${postfix}`;
|
||||||
|
|
||||||
const component = fieldSchema['x-component'];
|
const component = fieldSchema['x-component'];
|
||||||
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
|
const componentName = typeof component === 'string' ? component : component?.displayName || component?.name;
|
||||||
|
@ -13,6 +13,24 @@ import { Browser, Page, test as base, expect, request } from '@playwright/test';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { defineConfig } from './defineConfig';
|
import { defineConfig } from './defineConfig';
|
||||||
|
|
||||||
|
function getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }) {
|
||||||
|
return {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Page',
|
||||||
|
properties: {
|
||||||
|
[tabSchemaName]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'page:addBlock',
|
||||||
|
properties: {},
|
||||||
|
'x-uid': tabSchemaUid,
|
||||||
|
'x-async': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': pageSchemaUid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export * from '@playwright/test';
|
export * from '@playwright/test';
|
||||||
|
|
||||||
export { defineConfig };
|
export { defineConfig };
|
||||||
@ -360,7 +378,7 @@ export class NocoPage {
|
|||||||
|
|
||||||
this.uid = schemaUid;
|
this.uid = schemaUid;
|
||||||
this.desktopRouteId = routeId;
|
this.desktopRouteId = routeId;
|
||||||
this.url = `${this.options?.basePath || '/admin/'}${this.uid}`;
|
this.url = `${this.options?.basePath || '/admin/'}${this.uid || this.desktopRouteId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async goto() {
|
async goto() {
|
||||||
@ -393,7 +411,7 @@ export class NocoPage {
|
|||||||
|
|
||||||
async destroy() {
|
async destroy() {
|
||||||
const waitList: any[] = [];
|
const waitList: any[] = [];
|
||||||
if (this.uid) {
|
if (this.uid || this.desktopRouteId !== undefined) {
|
||||||
waitList.push(deletePage(this.uid, this.desktopRouteId));
|
waitList.push(deletePage(this.uid, this.desktopRouteId));
|
||||||
this.uid = undefined;
|
this.uid = undefined;
|
||||||
this.desktopRouteId = undefined;
|
this.desktopRouteId = undefined;
|
||||||
@ -723,30 +741,15 @@ const createPage = async (options?: CreatePageOptions) => {
|
|||||||
const api = await request.newContext({
|
const api = await request.newContext({
|
||||||
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
|
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
|
||||||
});
|
});
|
||||||
const typeToSchema = {
|
|
||||||
group: {
|
|
||||||
'x-component': 'Menu.SubMenu',
|
|
||||||
'x-component-props': {},
|
|
||||||
},
|
|
||||||
page: {
|
|
||||||
'x-component': 'Menu.Item',
|
|
||||||
'x-component-props': {},
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
'x-component': 'Menu.URL',
|
|
||||||
'x-component-props': {
|
|
||||||
href: url,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const state = await api.storageState();
|
const state = await api.storageState();
|
||||||
const headers = getHeaders(state);
|
const headers = getHeaders(state);
|
||||||
const menuSchemaUid = pageUidFromOptions || uid();
|
|
||||||
const pageSchemaUid = uid();
|
|
||||||
const tabSchemaUid = uid();
|
|
||||||
const tabSchemaName = uid();
|
|
||||||
const title = name || menuSchemaUid;
|
|
||||||
const newPageSchema = keepUid ? pageSchema : updateUidOfPageSchema(pageSchema);
|
const newPageSchema = keepUid ? pageSchema : updateUidOfPageSchema(pageSchema);
|
||||||
|
const pageSchemaUid = newPageSchema?.['x-uid'] || uid();
|
||||||
|
const newTabSchemaUid = uid();
|
||||||
|
const newTabSchemaName = uid();
|
||||||
|
|
||||||
|
const title = name || pageSchemaUid;
|
||||||
|
|
||||||
let routeId;
|
let routeId;
|
||||||
let schemaUid;
|
let schemaUid;
|
||||||
|
|
||||||
@ -756,7 +759,6 @@ const createPage = async (options?: CreatePageOptions) => {
|
|||||||
data: {
|
data: {
|
||||||
type: 'group',
|
type: 'group',
|
||||||
title,
|
title,
|
||||||
schemaUid: menuSchemaUid,
|
|
||||||
hideInMenu: false,
|
hideInMenu: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -767,17 +769,15 @@ const createPage = async (options?: CreatePageOptions) => {
|
|||||||
|
|
||||||
const data = await result.json();
|
const data = await result.json();
|
||||||
routeId = data.data?.id;
|
routeId = data.data?.id;
|
||||||
schemaUid = menuSchemaUid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'page') {
|
if (type === 'page') {
|
||||||
const result = await api.post('/api/desktopRoutes:create', {
|
const routeResult = await api.post('/api/desktopRoutes:create', {
|
||||||
headers,
|
headers,
|
||||||
data: {
|
data: {
|
||||||
type: 'page',
|
type: 'page',
|
||||||
title,
|
title,
|
||||||
schemaUid: newPageSchema?.['x-uid'] || pageSchemaUid,
|
schemaUid: pageSchemaUid,
|
||||||
menuSchemaUid,
|
|
||||||
hideInMenu: false,
|
hideInMenu: false,
|
||||||
enableTabs: !!newPageSchema?.['x-component-props']?.enablePageTabs,
|
enableTabs: !!newPageSchema?.['x-component-props']?.enablePageTabs,
|
||||||
children: newPageSchema
|
children: newPageSchema
|
||||||
@ -786,21 +786,36 @@ const createPage = async (options?: CreatePageOptions) => {
|
|||||||
{
|
{
|
||||||
type: 'tabs',
|
type: 'tabs',
|
||||||
title: '{{t("Unnamed")}}',
|
title: '{{t("Unnamed")}}',
|
||||||
schemaUid: tabSchemaUid,
|
schemaUid: newTabSchemaUid,
|
||||||
tabSchemaName,
|
tabSchemaName: newTabSchemaName,
|
||||||
hideInMenu: false,
|
hideInMenu: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.ok()) {
|
if (!routeResult.ok()) {
|
||||||
throw new Error(await result.text());
|
throw new Error(await routeResult.text());
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await result.json();
|
const schemaResult = await api.post(`/api/uiSchemas:insert`, {
|
||||||
|
headers,
|
||||||
|
data:
|
||||||
|
newPageSchema ||
|
||||||
|
getPageMenuSchema({
|
||||||
|
pageSchemaUid,
|
||||||
|
tabSchemaUid: newTabSchemaUid,
|
||||||
|
tabSchemaName: newTabSchemaName,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!schemaResult.ok()) {
|
||||||
|
throw new Error(await routeResult.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await routeResult.json();
|
||||||
routeId = data.data?.id;
|
routeId = data.data?.id;
|
||||||
schemaUid = menuSchemaUid;
|
schemaUid = pageSchemaUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'link') {
|
if (type === 'link') {
|
||||||
@ -809,7 +824,6 @@ const createPage = async (options?: CreatePageOptions) => {
|
|||||||
data: {
|
data: {
|
||||||
type: 'link',
|
type: 'link',
|
||||||
title,
|
title,
|
||||||
schemaUid: menuSchemaUid,
|
|
||||||
hideInMenu: false,
|
hideInMenu: false,
|
||||||
options: {
|
options: {
|
||||||
href: url,
|
href: url,
|
||||||
@ -823,50 +837,6 @@ const createPage = async (options?: CreatePageOptions) => {
|
|||||||
|
|
||||||
const data = await result.json();
|
const data = await result.json();
|
||||||
routeId = data.data?.id;
|
routeId = data.data?.id;
|
||||||
schemaUid = menuSchemaUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await api.post(`/api/uiSchemas:insertAdjacent/nocobase-admin-menu?position=beforeEnd`, {
|
|
||||||
headers,
|
|
||||||
data: {
|
|
||||||
schema: {
|
|
||||||
_isJSONSchemaObject: true,
|
|
||||||
version: '2.0',
|
|
||||||
type: 'void',
|
|
||||||
title,
|
|
||||||
...typeToSchema[type],
|
|
||||||
'x-decorator': 'ACLMenuItemProvider',
|
|
||||||
properties: {
|
|
||||||
page: newPageSchema || {
|
|
||||||
_isJSONSchemaObject: true,
|
|
||||||
version: '2.0',
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Page',
|
|
||||||
'x-async': true,
|
|
||||||
properties: {
|
|
||||||
[tabSchemaName]: {
|
|
||||||
_isJSONSchemaObject: true,
|
|
||||||
version: '2.0',
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Grid',
|
|
||||||
'x-initializer': 'page:addBlock',
|
|
||||||
'x-uid': tabSchemaUid,
|
|
||||||
name: tabSchemaName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'x-uid': pageSchemaUid,
|
|
||||||
name: 'page',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
name: uid(),
|
|
||||||
'x-uid': menuSchemaUid,
|
|
||||||
},
|
|
||||||
wrap: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.ok()) {
|
|
||||||
throw new Error(await result.text());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { schemaUid, routeId };
|
return { schemaUid, routeId };
|
||||||
@ -1063,7 +1033,7 @@ const deleteMobileRoutes = async (mobileRouteId: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据页面 uid 删除一个 NocoBase 的页面
|
* 根据页面 uid 删除一个页面的 schema,根据页面路由的 id 删除一个页面的路由
|
||||||
*/
|
*/
|
||||||
const deletePage = async (pageUid: string, routeId: number) => {
|
const deletePage = async (pageUid: string, routeId: number) => {
|
||||||
const api = await request.newContext({
|
const api = await request.newContext({
|
||||||
@ -1083,12 +1053,14 @@ const deletePage = async (pageUid: string, routeId: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await api.post(`/api/uiSchemas:remove/${pageUid}`, {
|
if (pageUid) {
|
||||||
headers,
|
const result = await api.post(`/api/uiSchemas:remove/${pageUid}`, {
|
||||||
});
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
if (!result.ok()) {
|
if (!result.ok()) {
|
||||||
throw new Error(await result.text());
|
throw new Error(await result.text());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, test } from '@nocobase/test/e2e';
|
import { expect, test } from '@nocobase/test/e2e';
|
||||||
import { oneTableBlock } from './utils';
|
|
||||||
|
|
||||||
test('allows to configure interface', async ({ page, mockPage, mockRole, updateRole }) => {
|
test('allows to configure interface', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||||
await mockPage().goto();
|
await mockPage().goto();
|
||||||
@ -121,13 +120,13 @@ test('new menu items allow to be asscessed by default ', async ({ page, mockPage
|
|||||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||||
}, roleData);
|
}, roleData);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await mockPage({ ...oneTableBlock, name: 'new page' }).goto();
|
await mockPage({ name: 'new page' }).goto();
|
||||||
await expect(page.getByLabel('new page')).not.toBeVisible();
|
await expect(page.getByLabel('new page')).not.toBeVisible();
|
||||||
await updateRole({
|
await updateRole({
|
||||||
name: roleData.name,
|
name: roleData.name,
|
||||||
allowNewMenu: true,
|
allowNewMenu: true,
|
||||||
});
|
});
|
||||||
await mockPage({ ...oneTableBlock, name: 'new page' }).goto();
|
await mockPage({ name: 'new page' }).goto();
|
||||||
await expect(page.getByLabel('new page')).toBeVisible();
|
await expect(page.getByLabel('new page')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { createForm, Form, onFormValuesChange } from '@formily/core';
|
import { createForm, Form, onFormValuesChange } from '@formily/core';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { css, SchemaComponent, useAPIClient, useCompile, useRequest } from '@nocobase/client';
|
import { css, SchemaComponent, useAllAccessDesktopRoutes, useAPIClient, useCompile, useRequest } from '@nocobase/client';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { Checkbox, message, Table } from 'antd';
|
import { Checkbox, message, Table } from 'antd';
|
||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
@ -68,7 +68,7 @@ const style = css`
|
|||||||
|
|
||||||
const translateTitle = (menus: any[], t, compile) => {
|
const translateTitle = (menus: any[], t, compile) => {
|
||||||
return menus.map((menu) => {
|
return menus.map((menu) => {
|
||||||
const title = menu.title?.match(/^\s*\{\{\s*.+?\s*\}\}\s*$/) ? compile(menu.title) : t(menu.title);
|
const title = (menu.title?.match(/^\s*\{\{\s*.+?\s*\}\}\s*$/) ? compile(menu.title) : t(menu.title)) || t('Unnamed');
|
||||||
if (menu.children) {
|
if (menu.children) {
|
||||||
return {
|
return {
|
||||||
...menu,
|
...menu,
|
||||||
@ -123,7 +123,7 @@ const DesktopRoutesProvider: FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DesktopAllRoutesProvider: React.FC<{ active: boolean }> = ({ children, active }) => {
|
export const DesktopAllRoutesProvider: React.FC<{ active: boolean }> = ({ children, active }) => {
|
||||||
const refreshRef = React.useRef(() => {});
|
const refreshRef = React.useRef(() => { });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (active) {
|
if (active) {
|
||||||
@ -166,6 +166,7 @@ export const MenuPermissions: React.FC<{
|
|||||||
);
|
);
|
||||||
const resource = api.resource('roles.desktopRoutes', role.name);
|
const resource = api.resource('roles.desktopRoutes', role.name);
|
||||||
const allChecked = allIDList.length === IDList.length;
|
const allChecked = allIDList.length === IDList.length;
|
||||||
|
const { refresh: refreshDesktopRoutes } = useAllAccessDesktopRoutes();
|
||||||
|
|
||||||
const handleChange = async (checked, menuItem) => {
|
const handleChange = async (checked, menuItem) => {
|
||||||
// 处理取消选中
|
// 处理取消选中
|
||||||
@ -214,6 +215,7 @@ export const MenuPermissions: React.FC<{
|
|||||||
values: shouldAdd,
|
values: shouldAdd,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
refreshDesktopRoutes();
|
||||||
message.success(t('Saved successfully'));
|
message.success(t('Saved successfully'));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -288,6 +290,7 @@ export const MenuPermissions: React.FC<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
refresh();
|
refresh();
|
||||||
|
refreshDesktopRoutes();
|
||||||
message.success(t('Saved successfully'));
|
message.success(t('Saved successfully'));
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
|
@ -382,8 +382,11 @@ export const oneTableWithViewAction: PageConfig = {
|
|||||||
'x-index': 1,
|
'x-index': 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'x-uid': 'j0k2m5r9z3b',
|
||||||
|
'x-async': false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'x-uid': 'l6ioayfnq6c',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
handleDateChangeOnForm,
|
handleDateChangeOnForm,
|
||||||
useACLRoleContext,
|
useACLRoleContext,
|
||||||
useActionContext,
|
useActionContext,
|
||||||
|
useApp,
|
||||||
useCollection,
|
useCollection,
|
||||||
useCollectionParentRecordData,
|
useCollectionParentRecordData,
|
||||||
useDesignable,
|
useDesignable,
|
||||||
@ -27,10 +28,8 @@ import {
|
|||||||
useLazy,
|
useLazy,
|
||||||
usePopupUtils,
|
usePopupUtils,
|
||||||
useProps,
|
useProps,
|
||||||
useToken,
|
|
||||||
withDynamicSchemaProps,
|
withDynamicSchemaProps,
|
||||||
withSkeletonComponent,
|
withSkeletonComponent
|
||||||
useApp,
|
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@ -76,7 +75,7 @@ const getColorString = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteEventContext = React.createContext({
|
export const DeleteEventContext = React.createContext({
|
||||||
close: () => {},
|
close: () => { },
|
||||||
allowDeleteEvent: false,
|
allowDeleteEvent: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
|
||||||
import { Plugin, useToken } from '@nocobase/client';
|
import { Plugin, useToken } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
import { generateNTemplate } from '../locale';
|
import { generateNTemplate } from '../locale';
|
||||||
import { CalendarV2 } from './calendar';
|
import { CalendarV2 } from './calendar';
|
||||||
import { calendarBlockSettings } from './calendar/Calender.Settings';
|
import { calendarBlockSettings } from './calendar/Calender.Settings';
|
||||||
@ -70,9 +70,9 @@ export class PluginCalendarClient extends Plugin {
|
|||||||
colorFieldInterfaces: {
|
colorFieldInterfaces: {
|
||||||
[T: string]: { useGetColor: (field: any) => ColorFunctions };
|
[T: string]: { useGetColor: (field: any) => ColorFunctions };
|
||||||
} = {
|
} = {
|
||||||
select: { useGetColor },
|
select: { useGetColor },
|
||||||
radioGroup: { useGetColor },
|
radioGroup: { useGetColor },
|
||||||
};
|
};
|
||||||
|
|
||||||
dateTimeFieldInterfaces = ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp', 'createdAt', 'updatedAt'];
|
dateTimeFieldInterfaces = ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp', 'createdAt', 'updatedAt'];
|
||||||
|
|
||||||
|
@ -13,8 +13,6 @@ import { useField, useForm } from '@formily/react';
|
|||||||
import {
|
import {
|
||||||
CollectionField,
|
CollectionField,
|
||||||
css,
|
css,
|
||||||
getGroupMenuSchema,
|
|
||||||
getLinkMenuSchema,
|
|
||||||
getPageMenuSchema,
|
getPageMenuSchema,
|
||||||
getTabSchema,
|
getTabSchema,
|
||||||
getVariableComponentWithScope,
|
getVariableComponentWithScope,
|
||||||
@ -26,6 +24,7 @@ import {
|
|||||||
useCollectionRecordData,
|
useCollectionRecordData,
|
||||||
useDataBlockRequestData,
|
useDataBlockRequestData,
|
||||||
useDataBlockRequestGetter,
|
useDataBlockRequestGetter,
|
||||||
|
useInsertPageSchema,
|
||||||
useNocoBaseRoutes,
|
useNocoBaseRoutes,
|
||||||
useRequest,
|
useRequest,
|
||||||
useRouterBasename,
|
useRouterBasename,
|
||||||
@ -237,13 +236,13 @@ export const createRoutesTableSchema = (collectionName: string, basename: string
|
|||||||
'x-component': 'IconPicker',
|
'x-component': 'IconPicker',
|
||||||
'x-reactions': isMobile
|
'x-reactions': isMobile
|
||||||
? {
|
? {
|
||||||
dependencies: ['type'],
|
dependencies: ['type'],
|
||||||
fulfill: {
|
fulfill: {
|
||||||
state: {
|
state: {
|
||||||
required: '{{$deps[0] !== "tabs"}}',
|
required: '{{$deps[0] !== "tabs"}}',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
// 由于历史原因,桌面端使用的是 'href' 作为 key,移动端使用的是 'url' 作为 key
|
// 由于历史原因,桌面端使用的是 'href' 作为 key,移动端使用的是 'url' 作为 key
|
||||||
@ -575,9 +574,8 @@ export const createRoutesTableSchema = (collectionName: string, basename: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (recordData.type === NocoBaseDesktopRouteType.page) {
|
if (recordData.type === NocoBaseDesktopRouteType.page) {
|
||||||
const path = `${basenameOfCurrentRouter.slice(0, -1)}${basename}/${
|
const path = `${basenameOfCurrentRouter.slice(0, -1)}${basename}/${isMobile ? recordData.schemaUid : recordData.menuSchemaUid
|
||||||
isMobile ? recordData.schemaUid : recordData.menuSchemaUid
|
}`;
|
||||||
}`;
|
|
||||||
// 在点击 Access 按钮时,会用到
|
// 在点击 Access 按钮时,会用到
|
||||||
recordData._path = path;
|
recordData._path = path;
|
||||||
|
|
||||||
@ -697,13 +695,13 @@ export const createRoutesTableSchema = (collectionName: string, basename: string
|
|||||||
'x-component': 'IconPicker',
|
'x-component': 'IconPicker',
|
||||||
'x-reactions': isMobile
|
'x-reactions': isMobile
|
||||||
? {
|
? {
|
||||||
dependencies: ['type'],
|
dependencies: ['type'],
|
||||||
fulfill: {
|
fulfill: {
|
||||||
state: {
|
state: {
|
||||||
required: '{{$deps[0] !== "tabs"}}',
|
required: '{{$deps[0] !== "tabs"}}',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
// 由于历史原因,桌面端使用的是 'href' 作为 key,移动端使用的是 'url' 作为 key
|
// 由于历史原因,桌面端使用的是 'href' 作为 key,移动端使用的是 'url' 作为 key
|
||||||
@ -986,13 +984,13 @@ export const createRoutesTableSchema = (collectionName: string, basename: string
|
|||||||
'x-component': 'IconPicker',
|
'x-component': 'IconPicker',
|
||||||
'x-reactions': isMobile
|
'x-reactions': isMobile
|
||||||
? {
|
? {
|
||||||
dependencies: ['type'],
|
dependencies: ['type'],
|
||||||
fulfill: {
|
fulfill: {
|
||||||
state: {
|
state: {
|
||||||
required: '{{$deps[0] !== "tabs"}}',
|
required: '{{$deps[0] !== "tabs"}}',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
// 由于历史原因,桌面端使用的是 'href' 作为 key,移动端使用的是 'url' 作为 key
|
// 由于历史原因,桌面端使用的是 'href' 作为 key,移动端使用的是 'url' 作为 key
|
||||||
@ -1244,22 +1242,15 @@ function useCreateRouteSchema(isMobile: boolean) {
|
|||||||
const collectionName = 'uiSchemas';
|
const collectionName = 'uiSchemas';
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const resource = useMemo(() => api.resource(collectionName), [api, collectionName]);
|
const resource = useMemo(() => api.resource(collectionName), [api, collectionName]);
|
||||||
|
const insertPageSchema = useInsertPageSchema();
|
||||||
|
|
||||||
const createRouteSchema = useCallback(
|
const createRouteSchema = useCallback(
|
||||||
async ({
|
async ({
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
type,
|
type,
|
||||||
href,
|
|
||||||
params,
|
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
|
||||||
icon: string;
|
|
||||||
type: NocoBaseDesktopRouteType;
|
type: NocoBaseDesktopRouteType;
|
||||||
href?: string;
|
|
||||||
params?: Record<string, any>;
|
|
||||||
}) => {
|
}) => {
|
||||||
const menuSchemaUid = uid();
|
const menuSchemaUid = isMobile ? undefined : uid();
|
||||||
const pageSchemaUid = uid();
|
const pageSchemaUid = uid();
|
||||||
const tabSchemaName = uid();
|
const tabSchemaName = uid();
|
||||||
const tabSchemaUid = type === NocoBaseDesktopRouteType.page ? uid() : undefined;
|
const tabSchemaUid = type === NocoBaseDesktopRouteType.page ? uid() : undefined;
|
||||||
@ -1268,17 +1259,16 @@ function useCreateRouteSchema(isMobile: boolean) {
|
|||||||
[NocoBaseDesktopRouteType.page]: isMobile
|
[NocoBaseDesktopRouteType.page]: isMobile
|
||||||
? getMobilePageSchema(pageSchemaUid, tabSchemaUid).schema
|
? getMobilePageSchema(pageSchemaUid, tabSchemaUid).schema
|
||||||
: getPageMenuSchema({
|
: getPageMenuSchema({
|
||||||
title,
|
pageSchemaUid,
|
||||||
icon,
|
tabSchemaUid,
|
||||||
pageSchemaUid,
|
tabSchemaName,
|
||||||
tabSchemaUid,
|
}),
|
||||||
menuSchemaUid,
|
|
||||||
tabSchemaName,
|
|
||||||
}),
|
|
||||||
[NocoBaseDesktopRouteType.group]: getGroupMenuSchema({ title, icon, schemaUid: menuSchemaUid }),
|
|
||||||
[NocoBaseDesktopRouteType.link]: getLinkMenuSchema({ title, icon, schemaUid: menuSchemaUid, href, params }),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!typeToSchema[type]) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
await resource['insertAdjacent']({
|
await resource['insertAdjacent']({
|
||||||
resourceIndex: 'mobile',
|
resourceIndex: 'mobile',
|
||||||
@ -1288,17 +1278,12 @@ function useCreateRouteSchema(isMobile: boolean) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await resource['insertAdjacent/nocobase-admin-menu']({
|
await insertPageSchema(typeToSchema[type]);
|
||||||
position: 'beforeEnd',
|
|
||||||
values: {
|
|
||||||
schema: typeToSchema[type],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { menuSchemaUid, pageSchemaUid, tabSchemaUid, tabSchemaName };
|
return { menuSchemaUid, pageSchemaUid, tabSchemaUid, tabSchemaName };
|
||||||
},
|
},
|
||||||
[isMobile, resource],
|
[isMobile, resource, insertPageSchema],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* 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 { MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
import Migration, { getIds } from '../migrations/202502071837-fix-permissions';
|
||||||
|
import { vi, describe, beforeEach, afterEach, test, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('202502071837-fix-permissions', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['nocobase'],
|
||||||
|
});
|
||||||
|
await app.version.update('1.5.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createTestData() {
|
||||||
|
const desktopRoutes = app.db.getRepository('desktopRoutes');
|
||||||
|
const roles = app.db.getRepository('roles');
|
||||||
|
|
||||||
|
// 创建测试路由
|
||||||
|
const routes = await desktopRoutes.create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
title: 'Page 1',
|
||||||
|
menuSchemaUid: 'page1',
|
||||||
|
children: [{ type: 'tabs', title: 'Tabs 1', parentId: 1, schemaUid: 'tabs1' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
title: 'Page 2',
|
||||||
|
menuSchemaUid: 'page2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建测试角色
|
||||||
|
const role = await roles.create({
|
||||||
|
values: {
|
||||||
|
name: 'test',
|
||||||
|
menuUiSchemas: [
|
||||||
|
{ 'x-uid': 'page1' }, // 已有 page1 的权限
|
||||||
|
{ 'x-uid': 'page3' }, // 不存在的权限
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { routes, role };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should add missing permissions', async () => {
|
||||||
|
const { role } = await createTestData();
|
||||||
|
const migration = new Migration({ db: app.db, app } as any);
|
||||||
|
|
||||||
|
await migration.up();
|
||||||
|
|
||||||
|
// 获取更新后的角色权限
|
||||||
|
const updatedRole = await app.db.getRepository('roles').findOne({
|
||||||
|
filter: { name: 'test' },
|
||||||
|
appends: ['desktopRoutes'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证应该添加的权限
|
||||||
|
const routeIds = updatedRole.desktopRoutes.map((r) => r.id);
|
||||||
|
expect(routeIds).toContain(1); // page1 已存在
|
||||||
|
expect(routeIds).toContain(2); // tabs1 应该被添加
|
||||||
|
expect(routeIds).not.toContain(3); // page2 不应该被添加
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty desktop routes', async () => {
|
||||||
|
const migration = new Migration({ db: app.db, app } as any);
|
||||||
|
await expect(migration.up()).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getIds should return correct needAddIds', () => {
|
||||||
|
const desktopRoutes = [
|
||||||
|
{ id: 1, type: 'page', menuSchemaUid: 'page1' },
|
||||||
|
{ id: 2, type: 'tabs', parentId: 1, schemaUid: 'tabs1' },
|
||||||
|
{ id: 3, type: 'page', menuSchemaUid: 'page2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const menuUiSchemas = [{ 'x-uid': 'page1' }];
|
||||||
|
|
||||||
|
const { needAddIds } = getIds(desktopRoutes, menuUiSchemas);
|
||||||
|
expect(needAddIds).toEqual([1, 2]); // page1 已存在但 tabs1/page2 需要添加
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* 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 Database from '@nocobase/database';
|
||||||
|
import { createMockServer, MockServer } from '@nocobase/test';
|
||||||
|
|
||||||
|
describe('desktopRoutes:listAccessible', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
registerActions: true,
|
||||||
|
acl: true,
|
||||||
|
plugins: ['nocobase'],
|
||||||
|
});
|
||||||
|
db = app.db;
|
||||||
|
|
||||||
|
// 创建测试页面和tab路由
|
||||||
|
await db.getRepository('desktopRoutes').create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
title: 'page1',
|
||||||
|
children: [{ type: 'tab', title: 'tab1' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
title: 'page2',
|
||||||
|
children: [{ type: 'tab', title: 'tab2' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
title: 'page3',
|
||||||
|
children: [{ type: 'tab', title: 'tab3' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all routes for root role', async () => {
|
||||||
|
const rootUser = await db.getRepository('users').create({
|
||||||
|
values: { roles: ['root'] },
|
||||||
|
});
|
||||||
|
const agent = await app.agent().login(rootUser);
|
||||||
|
|
||||||
|
const response = await agent.resource('desktopRoutes').listAccessible();
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.data.length).toBe(3);
|
||||||
|
expect(response.body.data[0].children.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all routes by default for admin/member', async () => {
|
||||||
|
// 测试 admin 角色
|
||||||
|
const adminUser = await db.getRepository('users').create({
|
||||||
|
values: { roles: ['admin'] },
|
||||||
|
});
|
||||||
|
const adminAgent = await app.agent().login(adminUser);
|
||||||
|
|
||||||
|
let response = await adminAgent.resource('desktopRoutes').listAccessible();
|
||||||
|
expect(response.body.data.length).toBe(3);
|
||||||
|
|
||||||
|
// 测试 member 角色
|
||||||
|
const memberUser = await db.getRepository('users').create({
|
||||||
|
values: { roles: ['member'] },
|
||||||
|
});
|
||||||
|
const memberAgent = await app.agent().login(memberUser);
|
||||||
|
|
||||||
|
response = await memberAgent.resource('desktopRoutes').listAccessible();
|
||||||
|
expect(response.body.data.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return filtered routes with children', async () => {
|
||||||
|
// 使用 root 角色配置 member 的可访问路由
|
||||||
|
const rootUser = await db.getRepository('users').create({
|
||||||
|
values: { roles: ['root'] },
|
||||||
|
});
|
||||||
|
const rootAgent = await app.agent().login(rootUser);
|
||||||
|
|
||||||
|
// 更新 member 角色的可访问路由
|
||||||
|
await rootAgent.resource('roles.desktopRoutes', 'member').remove({
|
||||||
|
values: [1, 2, 3, 4, 5, 6], // 移除所有路由的访问权限
|
||||||
|
});
|
||||||
|
await rootAgent.resource('roles.desktopRoutes', 'member').add({
|
||||||
|
values: [1, 2], // 再加上 page1 和 tab1 的访问权限
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用 member 用户测试
|
||||||
|
const memberUser = await db.getRepository('users').create({
|
||||||
|
values: { roles: ['member'] },
|
||||||
|
});
|
||||||
|
const memberAgent = await app.agent().login(memberUser);
|
||||||
|
|
||||||
|
const response = await memberAgent.resource('desktopRoutes').listAccessible();
|
||||||
|
expect(response.body.data.length).toBe(1);
|
||||||
|
expect(response.body.data[0].title).toBe('page1');
|
||||||
|
expect(response.body.data[0].children.length).toBe(1);
|
||||||
|
expect(response.body.data[0].children[0].title).toBe('tab1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty response when there are no accessible routes', async () => {
|
||||||
|
// 使用 root 角色配置 member 的可访问路由
|
||||||
|
const rootUser = await db.getRepository('users').create({
|
||||||
|
values: { roles: ['root'] },
|
||||||
|
});
|
||||||
|
const rootAgent = await app.agent().login(rootUser);
|
||||||
|
|
||||||
|
// 更新 member 角色的可访问路由
|
||||||
|
await rootAgent.resource('roles.desktopRoutes', 'member').remove({
|
||||||
|
values: [1, 2, 3, 4, 5, 6], // 移除所有路由的访问权限
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用 member 用户测试
|
||||||
|
const memberUser = await db.getRepository('users').create({
|
||||||
|
values: { roles: ['member'] },
|
||||||
|
});
|
||||||
|
const memberAgent = await app.agent().login(memberUser);
|
||||||
|
|
||||||
|
const response = await memberAgent.resource('desktopRoutes').listAccessible();
|
||||||
|
expect(response.body.data.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should auto include children when page has no children', async () => {
|
||||||
|
// 创建一个没有子路由的页面
|
||||||
|
const page4 = await db.getRepository('desktopRoutes').create({
|
||||||
|
values: {
|
||||||
|
type: 'page',
|
||||||
|
title: 'page4',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建两个子路由
|
||||||
|
await db.getRepository('desktopRoutes').create({
|
||||||
|
values: [
|
||||||
|
{ type: 'tab', title: 'tab4-1', parentId: page4.id },
|
||||||
|
{ type: 'tab', title: 'tab4-2', parentId: page4.id },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 配置 member 角色只能访问 page4
|
||||||
|
const rootUser = await db.getRepository('users').create({
|
||||||
|
values: { roles: ['root'] },
|
||||||
|
});
|
||||||
|
const rootAgent = await app.agent().login(rootUser);
|
||||||
|
await rootAgent.resource('roles.desktopRoutes', 'member').remove({
|
||||||
|
values: [1, 2, 3, 4, 5, 6, 8, 9], // 只保留 page4 的访问权限
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证返回结果包含子路由
|
||||||
|
const memberUser = await db.getRepository('users').create({
|
||||||
|
values: { roles: ['member'] },
|
||||||
|
});
|
||||||
|
const memberAgent = await app.agent().login(memberUser);
|
||||||
|
|
||||||
|
const response = await memberAgent.resource('desktopRoutes').listAccessible();
|
||||||
|
expect(response.body.data.length).toBe(1);
|
||||||
|
expect(response.body.data[0].title).toBe('page4');
|
||||||
|
expect(response.body.data[0].children.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
@ -8,14 +8,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Model } from '@nocobase/database';
|
import { Model } from '@nocobase/database';
|
||||||
|
import PluginLocalizationServer from '@nocobase/plugin-localization';
|
||||||
import { Plugin } from '@nocobase/server';
|
import { Plugin } from '@nocobase/server';
|
||||||
|
import { tval } from '@nocobase/utils';
|
||||||
import * as process from 'node:process';
|
import * as process from 'node:process';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { getAntdLocale } from './antd';
|
import { getAntdLocale } from './antd';
|
||||||
import { getCronLocale } from './cron';
|
import { getCronLocale } from './cron';
|
||||||
import { getCronstrueLocale } from './cronstrue';
|
import { getCronstrueLocale } from './cronstrue';
|
||||||
import PluginLocalizationServer from '@nocobase/plugin-localization';
|
|
||||||
import { tval } from '@nocobase/utils';
|
|
||||||
|
|
||||||
async function getLang(ctx) {
|
async function getLang(ctx) {
|
||||||
const SystemSetting = ctx.db.getRepository('systemSettings');
|
const SystemSetting = ctx.db.getRepository('systemSettings');
|
||||||
@ -216,20 +216,35 @@ export class PluginClientServer extends Plugin {
|
|||||||
appends: ['desktopRoutes'],
|
appends: ['desktopRoutes'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const desktopRoutesId = role
|
// 1. 如果 page 的 children 为空,那么需要把 page 的 children 全部找出来,然后返回。否则前端会因为缺少 tab 路由的数据而导致页面空白
|
||||||
.get('desktopRoutes')
|
// 2. 如果 page 的 children 不为空,不需要做特殊处理
|
||||||
// hidden 为 true 的节点不会显示在权限配置表格中,所以无法被配置,需要被过滤掉
|
const desktopRoutesId = role.get('desktopRoutes').map(async (item, index, items) => {
|
||||||
.filter((item) => !item.hidden)
|
if (item.type === 'page' && !items.some((tab) => tab.parentId === item.id)) {
|
||||||
.map((item) => item.id);
|
const children = await desktopRoutesRepository.find({
|
||||||
|
filter: {
|
||||||
|
parentId: item.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
ctx.body = await desktopRoutesRepository.find({
|
return [item.id, ...(children || []).map((child) => child.id)];
|
||||||
tree: true,
|
}
|
||||||
...ctx.query,
|
|
||||||
filter: {
|
return item.id;
|
||||||
id: desktopRoutesId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (desktopRoutesId) {
|
||||||
|
const ids = (await Promise.all(desktopRoutesId)).flat();
|
||||||
|
const result = await desktopRoutesRepository.find({
|
||||||
|
tree: true,
|
||||||
|
...ctx.query,
|
||||||
|
filter: {
|
||||||
|
id: ids,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = result;
|
||||||
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -29,25 +29,15 @@ export const useStyles = genStyleHook('nb-mobile-navigation-bar-action', (token)
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
||||||
'.schema-toolbar': {
|
|
||||||
inset: '-15px -8px',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'.nb-navigation-bar-action-title': {
|
'.nb-navigation-bar-action-title': {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
'.schema-toolbar': {
|
|
||||||
inset: '-15px -8px',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'.nb-navigation-bar-action-icon-and-title': {
|
'.nb-navigation-bar-action-icon-and-title': {
|
||||||
height: '32px !important',
|
height: '32px !important',
|
||||||
fontSize: '17px !important',
|
fontSize: '17px !important',
|
||||||
padding: '0 6px !important',
|
padding: '0 6px !important',
|
||||||
'.schema-toolbar': {
|
|
||||||
inset: '-15px',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -7,20 +7,20 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
|
||||||
import { App } from 'antd';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import {
|
import {
|
||||||
SchemaSettings,
|
|
||||||
SchemaToolbar,
|
|
||||||
useSchemaToolbar,
|
|
||||||
SchemaToolbarProvider,
|
|
||||||
createTextSettingsItem,
|
createTextSettingsItem,
|
||||||
|
SchemaSettings,
|
||||||
SchemaSettingsItemType,
|
SchemaSettingsItemType,
|
||||||
|
SchemaToolbar,
|
||||||
|
SchemaToolbarProvider,
|
||||||
|
useSchemaToolbar,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
|
import { App } from 'antd';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { MobileRouteItem, useMobileRoutes } from '../../../../mobile-providers';
|
|
||||||
import { generatePluginTranslationTemplate, usePluginTranslation } from '../../../../locale';
|
import { generatePluginTranslationTemplate, usePluginTranslation } from '../../../../locale';
|
||||||
|
import { MobileRouteItem, useMobileRoutes } from '../../../../mobile-providers';
|
||||||
|
|
||||||
const remove = createTextSettingsItem({
|
const remove = createTextSettingsItem({
|
||||||
name: 'remove',
|
name: 'remove',
|
||||||
@ -118,7 +118,7 @@ export const MobilePageTabsSettings: FC<MobilePageTabsSettingsProps> = ({ tab })
|
|||||||
settings={mobilePageTabsSettings}
|
settings={mobilePageTabsSettings}
|
||||||
showBackground
|
showBackground
|
||||||
showBorder={false}
|
showBorder={false}
|
||||||
toolbarStyle={{ inset: '-15px -12px' }}
|
toolbarStyle={{ inset: '0 -12px' }}
|
||||||
spaceWrapperStyle={{ top: 3 }}
|
spaceWrapperStyle={{ top: 3 }}
|
||||||
/>
|
/>
|
||||||
</SchemaToolbarProvider>
|
</SchemaToolbarProvider>
|
||||||
|
@ -18,22 +18,22 @@ const { ExecutionPage } = lazy(() => import('./ExecutionPage'), 'ExecutionPage')
|
|||||||
const { WorkflowPage } = lazy(() => import('./WorkflowPage'), 'WorkflowPage');
|
const { WorkflowPage } = lazy(() => import('./WorkflowPage'), 'WorkflowPage');
|
||||||
const { WorkflowPane } = lazy(() => import('./WorkflowPane'), 'WorkflowPane');
|
const { WorkflowPane } = lazy(() => import('./WorkflowPane'), 'WorkflowPane');
|
||||||
|
|
||||||
import { Trigger } from './triggers';
|
import { NAMESPACE } from './locale';
|
||||||
import CollectionTrigger from './triggers/collection';
|
|
||||||
import ScheduleTrigger from './triggers/schedule';
|
|
||||||
import { Instruction } from './nodes';
|
import { Instruction } from './nodes';
|
||||||
import CalculationInstruction from './nodes/calculation';
|
import CalculationInstruction from './nodes/calculation';
|
||||||
import ConditionInstruction from './nodes/condition';
|
import ConditionInstruction from './nodes/condition';
|
||||||
|
import CreateInstruction from './nodes/create';
|
||||||
|
import DestroyInstruction from './nodes/destroy';
|
||||||
import EndInstruction from './nodes/end';
|
import EndInstruction from './nodes/end';
|
||||||
import QueryInstruction from './nodes/query';
|
import QueryInstruction from './nodes/query';
|
||||||
import CreateInstruction from './nodes/create';
|
|
||||||
import UpdateInstruction from './nodes/update';
|
import UpdateInstruction from './nodes/update';
|
||||||
import DestroyInstruction from './nodes/destroy';
|
|
||||||
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
|
|
||||||
import { lang, NAMESPACE } from './locale';
|
|
||||||
import { VariableOption } from './variable';
|
|
||||||
import { WorkflowTasks, TasksProvider, TaskTypeOptions } from './WorkflowTasks';
|
|
||||||
import { BindWorkflowConfig } from './settings/BindWorkflowConfig';
|
import { BindWorkflowConfig } from './settings/BindWorkflowConfig';
|
||||||
|
import { Trigger } from './triggers';
|
||||||
|
import CollectionTrigger from './triggers/collection';
|
||||||
|
import ScheduleTrigger from './triggers/schedule';
|
||||||
|
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
|
||||||
|
import { VariableOption } from './variable';
|
||||||
|
import { TasksProvider, TaskTypeOptions, WorkflowTasks } from './WorkflowTasks';
|
||||||
|
|
||||||
const workflowConfigSettings = {
|
const workflowConfigSettings = {
|
||||||
Component: BindWorkflowConfig,
|
Component: BindWorkflowConfig,
|
||||||
@ -76,7 +76,7 @@ export default class PluginWorkflowClient extends Plugin {
|
|||||||
return this.triggers.get(workflow.type)?.sync ?? workflow.sync;
|
return this.triggers.get(workflow.type)?.sync ?? workflow.sync;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerTrigger(type: string, trigger: Trigger | { new (): Trigger }) {
|
registerTrigger(type: string, trigger: Trigger | { new(): Trigger }) {
|
||||||
if (typeof trigger === 'function') {
|
if (typeof trigger === 'function') {
|
||||||
this.triggers.register(type, new trigger());
|
this.triggers.register(type, new trigger());
|
||||||
} else if (trigger) {
|
} else if (trigger) {
|
||||||
@ -86,7 +86,7 @@ export default class PluginWorkflowClient extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerInstruction(type: string, instruction: Instruction | { new (): Instruction }) {
|
registerInstruction(type: string, instruction: Instruction | { new(): Instruction }) {
|
||||||
if (typeof instruction === 'function') {
|
if (typeof instruction === 'function') {
|
||||||
this.instructions.register(type, new instruction());
|
this.instructions.register(type, new instruction());
|
||||||
} else if (instruction instanceof Instruction) {
|
} else if (instruction instanceof Instruction) {
|
||||||
@ -182,15 +182,15 @@ export default class PluginWorkflowClient extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export * from './Branch';
|
export * from './Branch';
|
||||||
export * from './FlowContext';
|
|
||||||
export * from './constants';
|
|
||||||
export * from './nodes';
|
|
||||||
export { Trigger, useTrigger } from './triggers';
|
|
||||||
export * from './variable';
|
|
||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './utils';
|
export * from './constants';
|
||||||
export * from './hooks';
|
|
||||||
export { default as useStyles } from './style';
|
|
||||||
export * from './variable';
|
|
||||||
export * from './ExecutionContextProvider';
|
export * from './ExecutionContextProvider';
|
||||||
|
export * from './FlowContext';
|
||||||
|
export * from './hooks';
|
||||||
|
export * from './nodes';
|
||||||
export * from './settings/BindWorkflowConfig';
|
export * from './settings/BindWorkflowConfig';
|
||||||
|
export { default as useStyles } from './style';
|
||||||
|
export { Trigger, useTrigger } from './triggers';
|
||||||
|
export * from './utils';
|
||||||
|
export * from './variable';
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user