mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +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...',
|
||||
devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false,
|
||||
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` }],
|
||||
headScripts: [
|
||||
{
|
||||
|
@ -9,7 +9,7 @@
|
||||
"@ahooksjs/use-url-state": "3.5.1",
|
||||
"@ant-design/cssinjs": "^1.11.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",
|
||||
"@budibase/handlebars-helpers": "^0.14.0",
|
||||
"@ctrl/tinycolor": "^3.6.0",
|
||||
|
@ -321,7 +321,7 @@ export const ACLActionProvider = (props) => {
|
||||
() => actionPath && 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>;
|
||||
}
|
||||
if (!actionPath) {
|
||||
|
@ -47,6 +47,7 @@ export const CSSVariableProvider = ({ children }) => {
|
||||
document.body.style.setProperty('--colorSettings', token.colorSettings || defaultTheme.token.colorSettings);
|
||||
document.body.style.setProperty('--colorBgSettingsHover', token.colorBgSettingsHover);
|
||||
document.body.style.setProperty('--colorBorderSettingsHover', token.colorBorderSettingsHover);
|
||||
document.body.style.setProperty('--colorBgMenuItemSelected', token.colorBgHeaderMenuActive);
|
||||
|
||||
// 设置登录页面的背景色
|
||||
document.body.style.setProperty('background-color', token.colorBgContainer);
|
||||
|
@ -27,9 +27,21 @@
|
||||
.rc-virtual-list-scrollbar-thumb {
|
||||
background: var(--colorBgScrollBar) !important;
|
||||
}
|
||||
|
||||
.rc-virtual-list-scrollbar-thumb:hover {
|
||||
background: var(--colorBgScrollBarHover) !important;
|
||||
}
|
||||
|
||||
.rc-virtual-list-scrollbar-thumb:active {
|
||||
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/index';
|
||||
export * from './modules/blocks/useParentRecordCommon';
|
||||
export { getGroupMenuSchema } from './modules/menu/GroupItem';
|
||||
export { getLinkMenuSchema } from './modules/menu/LinkMenuItem';
|
||||
export { getPageMenuSchema } from './modules/menu/PageMenuItem';
|
||||
export { getPageMenuSchema, useInsertPageSchema } from './modules/menu/PageMenuItem';
|
||||
export { OpenModeProvider, useOpenModeContext } from './modules/popup/OpenModeProvider';
|
||||
export { PopupContextProvider } from './modules/popup/PopupContextProvider';
|
||||
export { usePopupUtils } from './modules/popup/usePopupUtils';
|
||||
export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||
|
||||
export { useCurrentPopupRecord } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||
export { showFileName } from './modules/fields/component/FileManager/fileManagerComponentFieldSettings';
|
||||
export { useCurrentPopupRecord } from './modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||
|
||||
export { languageCodes } from './locale';
|
||||
|
||||
@ -90,5 +88,6 @@ export {
|
||||
IsInNocoBaseRecursionFieldContext,
|
||||
NocoBaseRecursionField,
|
||||
RefreshComponentProvider,
|
||||
useRefreshFieldSchema,
|
||||
useRefreshFieldSchema
|
||||
} 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?",
|
||||
"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."
|
||||
"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ú?",
|
||||
"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 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 ?",
|
||||
"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 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?": "このメニューを非表示にしますか?",
|
||||
"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 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?": "이 메뉴를 숨기시겠습니까?",
|
||||
"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 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?",
|
||||
"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 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?": "Вы уверены, что хотите скрыть это меню?",
|
||||
"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 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?",
|
||||
"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 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?": "Ви впевнені, що хочете приховати це меню?",
|
||||
"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 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?": "你确定要隐藏这个菜单吗?",
|
||||
"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 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?": "你確定要隱藏這個菜單嗎?",
|
||||
"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 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,
|
||||
},
|
||||
'x-component-props': {
|
||||
url: '/admin/ids0d9esx8k',
|
||||
url: '/admin/ocal3pnltf2',
|
||||
params: [
|
||||
{
|
||||
name: 'roles',
|
||||
|
@ -3824,10 +3824,11 @@ export const T4334 = {
|
||||
},
|
||||
},
|
||||
'x-uid': 'ribk031tkp8',
|
||||
'x-async': false,
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '1j5z1j5z1j5',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -12,7 +12,7 @@ import { SchemaOptionsContext } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaInitializerItem, useSchemaInitializer } from '../../application';
|
||||
import { SchemaInitializerItem } from '../../application';
|
||||
import { useGlobalTheme } from '../../global-theme';
|
||||
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
||||
import {
|
||||
@ -25,7 +25,6 @@ import {
|
||||
import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers';
|
||||
|
||||
export const GroupItem = () => {
|
||||
const { insert } = useSchemaInitializer();
|
||||
const { t } = useTranslation();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const { theme } = useGlobalTheme();
|
||||
@ -69,30 +68,13 @@ export const GroupItem = () => {
|
||||
const schemaUid = uid();
|
||||
|
||||
// 创建一个路由到 desktopRoutes 表中
|
||||
const { data } = await createRoute({
|
||||
await createRoute({
|
||||
type: NocoBaseDesktopRouteType.group,
|
||||
title,
|
||||
icon,
|
||||
parentId: parentRoute?.id,
|
||||
schemaUid,
|
||||
});
|
||||
|
||||
// 同时插入一个对应的 Schema
|
||||
insert(getGroupMenuSchema({ title, icon, schemaUid, route: data?.data }));
|
||||
}, [insert, options.components, options.scope, t, theme]);
|
||||
}, [options.components, options.scope, t, theme]);
|
||||
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 { SchemaOptionsContext } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { SchemaInitializerItem, useSchemaInitializer } from '../../application';
|
||||
import { SchemaInitializerItem } from '../../application';
|
||||
import { useGlobalTheme } from '../../global-theme';
|
||||
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
||||
import {
|
||||
@ -28,7 +27,6 @@ import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers
|
||||
import { useURLAndHTMLSchema } from '../actions/link/useURLAndHTMLSchema';
|
||||
|
||||
export const LinkMenuItem = () => {
|
||||
const { insert } = useSchemaInitializer();
|
||||
const { t } = useTranslation();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const { theme } = useGlobalTheme();
|
||||
@ -75,40 +73,19 @@ export const LinkMenuItem = () => {
|
||||
initialValues: {},
|
||||
});
|
||||
const { title, href, params, icon } = values;
|
||||
const schemaUid = uid();
|
||||
|
||||
// 创建一个路由到 desktopRoutes 表中
|
||||
const { data } = await createRoute({
|
||||
await createRoute({
|
||||
type: NocoBaseDesktopRouteType.link,
|
||||
title: values.title,
|
||||
icon: values.icon,
|
||||
title,
|
||||
icon,
|
||||
parentId: parentRoute?.id,
|
||||
schemaUid,
|
||||
options: {
|
||||
href,
|
||||
params,
|
||||
},
|
||||
});
|
||||
|
||||
// 同时插入一个对应的 Schema
|
||||
insert(getLinkMenuSchema({ title, icon, schemaUid, href, params, route: data?.data }));
|
||||
}, [insert, options.components, options.scope, t, theme]);
|
||||
}, [options.components, options.scope, t, theme]);
|
||||
|
||||
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 React, { useCallback, useContext } from 'react';
|
||||
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 { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
||||
import {
|
||||
@ -24,14 +25,28 @@ import {
|
||||
} from '../../schema-component';
|
||||
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 = () => {
|
||||
const { insert } = useSchemaInitializer();
|
||||
const { t } = useTranslation();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const { theme } = useGlobalTheme();
|
||||
const { componentCls, hashId } = useStyles();
|
||||
const parentRoute = useParentRoute();
|
||||
const { createRoute } = useNocoBaseRoutes();
|
||||
const insertPageSchema = useInsertPageSchema();
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
const values = await FormDialog(
|
||||
@ -65,16 +80,13 @@ export const PageMenuItem = () => {
|
||||
).open({
|
||||
initialValues: {},
|
||||
});
|
||||
const { title, icon } = values;
|
||||
const menuSchemaUid = uid();
|
||||
const pageSchemaUid = uid();
|
||||
const tabSchemaUid = uid();
|
||||
const tabSchemaName = uid();
|
||||
|
||||
// 创建一个路由到 desktopRoutes 表中
|
||||
const {
|
||||
data: { data: route },
|
||||
} = await createRoute({
|
||||
await createRoute({
|
||||
type: NocoBaseDesktopRouteType.page,
|
||||
title: values.title,
|
||||
icon: values.icon,
|
||||
@ -93,33 +105,15 @@ export const PageMenuItem = () => {
|
||||
});
|
||||
|
||||
// 同时插入一个对应的 Schema
|
||||
insert(getPageMenuSchema({ title, icon, pageSchemaUid, tabSchemaUid, menuSchemaUid, tabSchemaName, route }));
|
||||
}, [createRoute, insert, options?.components, options?.scope, parentRoute?.id, t, theme]);
|
||||
insertPageSchema(getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }));
|
||||
}, [createRoute, insertPageSchema, options?.components, options?.scope, parentRoute?.id, t, theme]);
|
||||
return <SchemaInitializerItem title={t('Page')} onClick={handleClick} className={`${componentCls} ${hashId}`} />;
|
||||
};
|
||||
|
||||
export function getPageMenuSchema({
|
||||
title,
|
||||
icon,
|
||||
pageSchemaUid,
|
||||
tabSchemaUid,
|
||||
menuSchemaUid,
|
||||
tabSchemaName,
|
||||
route = undefined,
|
||||
}) {
|
||||
export function getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }) {
|
||||
return {
|
||||
type: 'void',
|
||||
title,
|
||||
'x-component': 'Menu.Item',
|
||||
'x-decorator': 'ACLMenuItemProvider',
|
||||
'x-component-props': {
|
||||
icon,
|
||||
},
|
||||
properties: {
|
||||
page: {
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
'x-async': true,
|
||||
properties: {
|
||||
[tabSchemaName]: {
|
||||
type: 'void',
|
||||
@ -127,12 +121,9 @@ export function getPageMenuSchema({
|
||||
'x-initializer': 'page:addBlock',
|
||||
properties: {},
|
||||
'x-uid': tabSchemaUid,
|
||||
'x-async': true,
|
||||
},
|
||||
},
|
||||
'x-uid': pageSchemaUid,
|
||||
},
|
||||
},
|
||||
'x-uid': menuSchemaUid,
|
||||
__route__: route,
|
||||
};
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ test('single page', async ({ page, mockPage }) => {
|
||||
await mockPage({ name: pageTitle2 }).goto();
|
||||
await page.getByRole('menu').getByText(pageTitle1).click();
|
||||
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.getByLabel('block-item-TreeSelect-Target').locator('.ant-select').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();
|
||||
|
||||
// 2. 点击 tab1 应该跳转到 tab1,并使用新版 URL
|
||||
await page.getByText('tab1').click();
|
||||
await page.getByText('tab1', { exact: true }).click();
|
||||
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
|
||||
await page.getByText('tab2').click();
|
||||
await page.getByText('tab2', { exact: true }).click();
|
||||
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
|
||||
await nocoPage.goto();
|
||||
|
@ -13,12 +13,11 @@ test.describe('deleted popups', () => {
|
||||
test('should display error info when deleted popups', async ({ page, mockPage }) => {
|
||||
const nocoPage = await mockPage().waitForInit();
|
||||
const url = await nocoPage.getUrl();
|
||||
|
||||
await page.goto(
|
||||
const path =
|
||||
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);
|
||||
|
||||
// close the popups
|
||||
|
@ -18,7 +18,7 @@ test.describe('popup router', () => {
|
||||
}).waitForInit();
|
||||
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(
|
||||
url +
|
||||
'/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 { observer } from '@formily/reactive-react';
|
||||
import { getSubAppName } from '@nocobase/sdk';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import { Button, Modal, Result, Spin } from 'antd';
|
||||
import React, { FC } from 'react';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
@ -32,7 +33,6 @@ import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates';
|
||||
import { SystemSettingsPlugin } from '../system-settings';
|
||||
import { CurrentUserProvider, CurrentUserSettingsMenuProvider } from '../user';
|
||||
import { LocalePlugin } from './plugins/LocalePlugin';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
|
||||
const AppSpin = () => {
|
||||
return (
|
||||
@ -251,7 +251,7 @@ const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(
|
||||
{ displayName: 'AppMaintainingDialog' },
|
||||
);
|
||||
|
||||
const AppNotFound = () => {
|
||||
export const AppNotFound = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Result
|
||||
|
@ -9,9 +9,12 @@
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { SchemaOptionsContext } from '@formily/react';
|
||||
import { ConfigProvider, Divider } from 'antd';
|
||||
import { get } from 'lodash';
|
||||
import React, { useContext } from 'react';
|
||||
import { useACLRoleContext } from '../acl/ACLProvider';
|
||||
import { UserCenter } from '../route-switch/antd/admin-layout/UserCenterButton';
|
||||
import { Help } from '../user/Help';
|
||||
import { PinnedPluginListContext } from './context';
|
||||
|
||||
export const PinnedPluginListProvider: React.FC<{ items: any }> = (props) => {
|
||||
@ -25,7 +28,8 @@ export const PinnedPluginListProvider: React.FC<{ items: any }> = (props) => {
|
||||
};
|
||||
|
||||
const pinnedPluginListClassName = css`
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.ant-btn {
|
||||
border: 0;
|
||||
@ -44,6 +48,12 @@ const pinnedPluginListClassName = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const dividerTheme = {
|
||||
token: {
|
||||
colorSplit: 'rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
};
|
||||
|
||||
export const PinnedPluginList = React.memo(() => {
|
||||
const { allowAll, snippets } = useACLRoleContext();
|
||||
const getSnippetsAllow = (aclKey) => {
|
||||
@ -61,6 +71,11 @@ export const PinnedPluginList = React.memo(() => {
|
||||
const Action = get(components, ctx.items[key].component);
|
||||
return Action ? <Action key={key} /> : null;
|
||||
})}
|
||||
<ConfigProvider theme={dividerTheme}>
|
||||
<Divider type="vertical" />
|
||||
</ConfigProvider>
|
||||
<Help key="help" />
|
||||
<UserCenter />
|
||||
</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.
|
||||
*/
|
||||
|
||||
import { ISchema } from '@formily/json-schema';
|
||||
import { uid } from '@formily/shared';
|
||||
import _ from 'lodash';
|
||||
|
||||
export enum NocoBaseDesktopRouteType {
|
||||
group = 'group',
|
||||
page = 'page',
|
||||
@ -56,71 +52,3 @@ export interface NocoBaseDesktopRoute {
|
||||
createdBy?: 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 { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
|
||||
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
||||
import { useZIndexContext, zIndexContext } from './zIndexContext';
|
||||
import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
|
||||
|
||||
const MemoizeRecursionField = React.memo(RecursionField);
|
||||
MemoizeRecursionField.displayName = 'MemoizeRecursionField';
|
||||
@ -103,7 +103,7 @@ export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
|
||||
useSetAriaLabelForDrawer(visible);
|
||||
}
|
||||
|
||||
const zIndex = _zIndex || parentZIndex + (props.level || 0);
|
||||
const zIndex = getZIndex('drawer', _zIndex || parentZIndex, props.level || 0);
|
||||
|
||||
const onClose = useCallback(
|
||||
(e) => {
|
||||
|
@ -22,7 +22,7 @@ import { ActionContextNoRerender } from './context';
|
||||
import { useActionContext } from './hooks';
|
||||
import { useSetAriaLabelForModal } from './hooks/useSetAriaLabelForModal';
|
||||
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
||||
import { useZIndexContext, zIndexContext } from './zIndexContext';
|
||||
import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
|
||||
|
||||
const ModalErrorFallback: React.FC<FallbackProps> = (props) => {
|
||||
const { visible, setVisible } = useActionContext();
|
||||
@ -89,7 +89,7 @@ export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = obse
|
||||
useSetAriaLabelForModal(visible);
|
||||
}
|
||||
|
||||
const zIndex = _zIndex || parentZIndex + (props.level || 0);
|
||||
const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0);
|
||||
|
||||
return (
|
||||
<ActionContextNoRerender>
|
||||
|
@ -15,7 +15,7 @@ export const useActionPageStyle = genStyleHook('nb-action-page', (token) => {
|
||||
return {
|
||||
[componentCls]: {
|
||||
position: 'absolute !important' as any,
|
||||
top: 'var(--nb-header-height)',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
|
@ -16,7 +16,7 @@ import { BackButtonUsedInSubPage } from '../page/BackButtonUsedInSubPage';
|
||||
import { TabsContextProvider, useTabsContext } from '../tabs/context';
|
||||
import { useActionPageStyle } from './Action.Page.style';
|
||||
import { usePopupOrSubpagesContainerDOM } from './hooks/usePopupSlotDOM';
|
||||
import { useZIndexContext, zIndexContext } from './zIndexContext';
|
||||
import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
|
||||
|
||||
const ActionPageContent: FC<{ schema: any }> = React.memo(({ schema }) => {
|
||||
// Improve the speed of opening the page
|
||||
@ -45,7 +45,7 @@ export function ActionPage({ level }) {
|
||||
|
||||
const style = useMemo(() => {
|
||||
return {
|
||||
zIndex: parentZIndex + (level || 0),
|
||||
zIndex: getZIndex('page', parentZIndex, level || 0),
|
||||
};
|
||||
}, [parentZIndex, level]);
|
||||
|
||||
|
@ -14,3 +14,18 @@ export const zIndexContext = React.createContext(100);
|
||||
export const useZIndexContext = () => {
|
||||
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,
|
||||
useURLAndHTMLSchema,
|
||||
} from '../../../';
|
||||
import { useInsertPageSchema } from '../../../modules/menu/PageMenuItem';
|
||||
import { NocoBaseDesktopRouteType } from '../../../route-switch/antd/admin-layout/convertRoutesToSchema';
|
||||
|
||||
const insertPositionToMethod = {
|
||||
@ -71,11 +72,11 @@ const findMenuSchema = (fieldSchema: Schema) => {
|
||||
const InsertMenuItems = (props) => {
|
||||
const { eventKey, title, insertPosition } = props;
|
||||
const { t } = useTranslation();
|
||||
const { dn } = useDesignable();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { urlSchema, paramsSchema } = useURLAndHTMLSchema();
|
||||
const isSubMenu = fieldSchema['x-component'] === 'Menu.SubMenu';
|
||||
const { createRoute, moveRoute } = useNocoBaseRoutes();
|
||||
const insertPageSchema = useInsertPageSchema();
|
||||
|
||||
if (!isSubMenu && insertPosition === 'beforeEnd') {
|
||||
return null;
|
||||
@ -131,18 +132,6 @@ const InsertMenuItems = (props) => {
|
||||
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
|
||||
dn.insertAdjacent(
|
||||
insertPosition,
|
||||
getPageMenuSchema({
|
||||
title,
|
||||
icon,
|
||||
pageSchemaUid,
|
||||
menuSchemaUid,
|
||||
tabSchemaUid,
|
||||
tabSchemaName,
|
||||
}),
|
||||
);
|
||||
insertPageSchema(getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }));
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsModalItem
|
||||
@ -276,20 +255,6 @@ const InsertMenuItems = (props) => {
|
||||
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>
|
||||
|
@ -203,7 +203,7 @@ type ComposedMenu = React.FC<any> & {
|
||||
Designer?: React.FC<any>;
|
||||
};
|
||||
|
||||
const ParentRouteContext = createContext<NocoBaseDesktopRoute>(null);
|
||||
export const ParentRouteContext = createContext<NocoBaseDesktopRoute>(null);
|
||||
ParentRouteContext.displayName = 'ParentRouteContext';
|
||||
|
||||
export const useParentRoute = () => {
|
||||
@ -264,8 +264,8 @@ export const useNocoBaseRoutes = (collectionName = 'desktopRoutes') => {
|
||||
method,
|
||||
refreshAfterMove = true,
|
||||
}: {
|
||||
sourceId: string;
|
||||
targetId?: string;
|
||||
sourceId: string | number;
|
||||
targetId?: string | number;
|
||||
targetScope?: any;
|
||||
sortField?: string;
|
||||
sticky?: boolean;
|
||||
|
@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useDesignable, useNocoBaseRoutes } from '../..';
|
||||
import { SchemaSettings } from '../../../application/schema-settings';
|
||||
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
||||
import { useCurrentRoute } from '../../../route-switch';
|
||||
import { useCurrentRouteData } from '../../../route-switch';
|
||||
|
||||
function useNotDisableHeader() {
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -133,7 +133,7 @@ export const pageSettings = new SchemaSettings({
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const currentRoute = useCurrentRoute();
|
||||
const currentRoute = useCurrentRouteData();
|
||||
const { updateRoute } = useNocoBaseRoutes();
|
||||
return {
|
||||
title: t('Enable page tabs'),
|
||||
@ -143,6 +143,16 @@ export const pageSettings = new SchemaSettings({
|
||||
await updateRoute(currentRoute.id, {
|
||||
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 { css } from '@emotion/css';
|
||||
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 { Button, Tabs } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
@ -25,18 +25,24 @@ import {
|
||||
CurrentTabUidContext,
|
||||
useCurrentSearchParams,
|
||||
useCurrentTabUid,
|
||||
useLocationNoUpdate,
|
||||
useNavigateNoUpdate,
|
||||
useRouterBasename,
|
||||
} from '../../../application/CustomRouterContextProvider';
|
||||
import { useDocumentTitle } from '../../../document-title';
|
||||
import { useGlobalTheme } from '../../../global-theme';
|
||||
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 { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer';
|
||||
import { DndContext } from '../../common';
|
||||
import { SortableItem } from '../../common/sortable-item';
|
||||
import { SchemaComponent, SchemaComponentOptions } from '../../core';
|
||||
import { RemoteSchemaComponent, SchemaComponent, SchemaComponentOptions } from '../../core';
|
||||
import { useCompile, useDesignable } from '../../hooks';
|
||||
import { useToken } from '../__builtins__';
|
||||
import { ErrorFallback } from '../error-fallback';
|
||||
@ -60,12 +66,9 @@ const InternalPage = React.memo((props: PageProps) => {
|
||||
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
|
||||
const searchParams = useCurrentSearchParams();
|
||||
const loading = false;
|
||||
const currentRoute = useCurrentRoute();
|
||||
const currentRoute = useCurrentRouteData();
|
||||
const enablePageTabs = currentRoute.enableTabs;
|
||||
const defaultActiveKey = useMemo(
|
||||
() => getDefaultActiveKey(currentRoute?.children?.[0]?.schemaUid, fieldSchema),
|
||||
[currentRoute?.children, fieldSchema],
|
||||
);
|
||||
const defaultActiveKey = currentRoute?.children?.[0]?.schemaUid;
|
||||
|
||||
const activeKey = useMemo(
|
||||
// 处理 searchParams 是为了兼容旧版的 tab 参数
|
||||
@ -74,8 +77,8 @@ const InternalPage = React.memo((props: PageProps) => {
|
||||
);
|
||||
|
||||
const outletContext = useMemo(
|
||||
() => ({ loading, disablePageHeader, enablePageTabs, fieldSchema, tabUid: currentTabUid }),
|
||||
[currentTabUid, disablePageHeader, enablePageTabs, fieldSchema, loading],
|
||||
() => ({ loading, disablePageHeader, enablePageTabs, tabUid: currentTabUid }),
|
||||
[currentTabUid, disablePageHeader, enablePageTabs, loading],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -92,7 +95,6 @@ const InternalPage = React.memo((props: PageProps) => {
|
||||
loading={loading}
|
||||
disablePageHeader={disablePageHeader}
|
||||
enablePageTabs={enablePageTabs}
|
||||
fieldSchema={fieldSchema}
|
||||
activeKey={activeKey}
|
||||
/>
|
||||
{/* Used to match the route with name "admin.page.popup" */}
|
||||
@ -135,14 +137,13 @@ export const Page = React.memo((props: PageProps) => {
|
||||
Page.displayName = 'NocoBasePage';
|
||||
|
||||
export const PageTabs = () => {
|
||||
const { loading, disablePageHeader, enablePageTabs, fieldSchema, tabUid } = useOutletContext<any>();
|
||||
const { loading, disablePageHeader, enablePageTabs, tabUid } = useOutletContext<any>();
|
||||
return (
|
||||
<CurrentTabUidContext.Provider value={tabUid}>
|
||||
<PageContent
|
||||
loading={loading}
|
||||
disablePageHeader={disablePageHeader}
|
||||
enablePageTabs={enablePageTabs}
|
||||
fieldSchema={fieldSchema}
|
||||
activeKey={tabUid}
|
||||
/>
|
||||
{/* 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
|
||||
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 { active: pageActive } = useKeepAlive();
|
||||
|
||||
@ -178,16 +179,6 @@ const TabPane = React.memo(({ schema, active: tabActive }: { schema: Schema; act
|
||||
mountedRef.current = true;
|
||||
}
|
||||
|
||||
const newSchema = useMemo(
|
||||
() =>
|
||||
new Schema({
|
||||
properties: {
|
||||
[schema.name]: schema,
|
||||
},
|
||||
}),
|
||||
[schema],
|
||||
);
|
||||
|
||||
if (!mountedRef.current) {
|
||||
return null;
|
||||
}
|
||||
@ -195,7 +186,7 @@ const TabPane = React.memo(({ schema, active: tabActive }: { schema: Schema; act
|
||||
return (
|
||||
<div style={tabActive ? displayBlock : displayNone}>
|
||||
<KeepAliveProvider active={pageActive && tabActive}>
|
||||
<SchemaComponent distributed schema={newSchema} />
|
||||
<RemoteSchemaComponent uid={uid} />
|
||||
</KeepAliveProvider>
|
||||
</div>
|
||||
);
|
||||
@ -205,26 +196,48 @@ interface PageContentProps {
|
||||
loading: boolean;
|
||||
disablePageHeader: any;
|
||||
enablePageTabs: any;
|
||||
fieldSchema: Schema<any, any, any, any, any, any, any, any, any>;
|
||||
activeKey: string;
|
||||
}
|
||||
|
||||
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) {
|
||||
return (
|
||||
<>
|
||||
{fieldSchema.mapProperties((schema) => (
|
||||
<TabPane key={schema.name} schema={schema} active={schema.name === activeKey} />
|
||||
))}
|
||||
{currentRoute.children?.map((tabRoute) => {
|
||||
return (
|
||||
<NocoBaseRouteContext.Provider value={tabRoute} key={tabRoute.schemaUid}>
|
||||
<TabPane active={tabRoute.schemaUid === activeKey} uid={tabRoute.schemaUid} />
|
||||
</NocoBaseRouteContext.Provider>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className1}>
|
||||
<SchemaComponent schema={fieldSchema} distributed />
|
||||
<NocoBaseRouteContext.Provider value={currentRoute?.children?.[0]}>
|
||||
<RemoteSchemaComponent uid={currentRoute?.children?.[0].schemaUid} />
|
||||
</NocoBaseRouteContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -254,7 +267,7 @@ const NocoBasePageHeaderTabs: FC<{ className: string; activeKey: string }> = ({
|
||||
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const { theme } = useGlobalTheme();
|
||||
const currentRoute = useCurrentRoute();
|
||||
const currentRoute = useCurrentRouteData();
|
||||
const { createRoute } = useNocoBaseRoutes();
|
||||
const compile = useCompile();
|
||||
|
||||
@ -334,29 +347,30 @@ const NocoBasePageHeaderTabs: FC<{ className: string; activeKey: string }> = ({
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
return fieldSchema
|
||||
.mapProperties((schema) => {
|
||||
const tabRoute = currentRoute?.children?.find((route) => route.schemaUid === schema['x-uid']);
|
||||
return currentRoute?.children
|
||||
?.map((tabRoute) => {
|
||||
if (!tabRoute || tabRoute.hideInMenu) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 将 tabRoute 挂载到 schema 上,以方便获取
|
||||
(schema as any).__route__ = tabRoute;
|
||||
// fake schema used to pass routing information to SortableItem
|
||||
const fakeSchema: any = { __route__: tabRoute };
|
||||
|
||||
return {
|
||||
label: (
|
||||
<NocoBaseRouteContext.Provider value={tabRoute}>
|
||||
<SortableItem
|
||||
id={schema.name as string}
|
||||
schema={schema}
|
||||
id={String(tabRoute.id)}
|
||||
className={classNames('nb-action-link', 'designerCss', className)}
|
||||
schema={fakeSchema}
|
||||
>
|
||||
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
|
||||
{tabRoute.icon && <Icon style={{ marginRight: 8 }} type={tabRoute.icon} />}
|
||||
<span>{(tabRoute.title && routeT(compile(tabRoute.title))) || t('Unnamed')}</span>
|
||||
<PageTabDesigner schema={schema} />
|
||||
<PageTabDesigner />
|
||||
</SortableItem>
|
||||
</NocoBaseRouteContext.Provider>
|
||||
),
|
||||
key: schema.name as string,
|
||||
key: tabRoute.schemaUid,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
@ -392,10 +406,12 @@ const NocoBasePageHeader = React.memo(({ activeKey, className }: { activeKey: st
|
||||
const [pageTitle, setPageTitle] = useState(() => t(fieldSchema.title));
|
||||
|
||||
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
|
||||
const currentRoute = useCurrentRoute();
|
||||
const currentRoute = useCurrentRouteData();
|
||||
const enablePageTabs = currentRoute.enableTabs;
|
||||
const hidePageTitle = fieldSchema['x-component-props']?.hidePageTitle;
|
||||
|
||||
const { token } = useToken();
|
||||
|
||||
useEffect(() => {
|
||||
const title = t(fieldSchema.title) || t(currentRoute?.title);
|
||||
if (title) {
|
||||
@ -410,6 +426,9 @@ const NocoBasePageHeader = React.memo(({ activeKey, className }: { activeKey: st
|
||||
{!disablePageHeader && (
|
||||
<AntdPageHeader
|
||||
className={classNames('pageHeaderCss', pageTitle || enablePageTabs ? '' : 'height0')}
|
||||
style={{
|
||||
paddingBottom: currentRoute.enableTabs || hidePageTitle ? 0 : token.paddingSM,
|
||||
}}
|
||||
ghost={false}
|
||||
// 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0
|
||||
title={hidePageTitle ? ' ' : (!fieldSchema.title && pageTitle ? routeT(pageTitle) : pageTitle) || ' '}
|
||||
@ -480,19 +499,6 @@ export function getTabSchema({
|
||||
'x-initializer': 'page:addBlock',
|
||||
properties: {},
|
||||
'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 { ISchema } from '@formily/json-schema';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { App, Modal } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider';
|
||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
||||
import { useCurrentRouteData } from '../../../route-switch';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { useNocoBaseRoutes } from '../menu/Menu';
|
||||
|
||||
@ -29,9 +30,8 @@ export const pageTabSettings = new SchemaSettings({
|
||||
type: 'modal',
|
||||
useComponentProps() {
|
||||
const { t } = useTranslation();
|
||||
const { schema } = useSchemaToolbar<{ schema: ISchema }>();
|
||||
const { dn } = useDesignable();
|
||||
const { updateRoute } = useNocoBaseRoutes();
|
||||
const currentRoute = useCurrentRouteData();
|
||||
|
||||
return {
|
||||
title: t('Edit'),
|
||||
@ -54,20 +54,10 @@ export const pageTabSettings = new SchemaSettings({
|
||||
},
|
||||
},
|
||||
} as ISchema,
|
||||
initialValues: { title: schema.title, icon: schema['x-icon'] },
|
||||
initialValues: { title: currentRoute.title, icon: currentRoute.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,
|
||||
icon,
|
||||
});
|
||||
@ -80,34 +70,22 @@ export const pageTabSettings = new SchemaSettings({
|
||||
type: 'switch',
|
||||
useComponentProps() {
|
||||
const { t } = useTranslation();
|
||||
const { schema } = useSchemaToolbar<{ schema: ISchema }>();
|
||||
const { updateRoute } = useNocoBaseRoutes();
|
||||
const { dn } = useDesignable();
|
||||
const currentRoute = useCurrentRouteData();
|
||||
|
||||
return {
|
||||
title: t('Hidden'),
|
||||
checked: schema['x-component-props']?.hidden,
|
||||
checked: currentRoute.hideInMenu,
|
||||
onChange: (v) => {
|
||||
Modal.confirm({
|
||||
title: '确定要隐藏该菜单吗?',
|
||||
title: t('Are you sure you want to hide this tab?'),
|
||||
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() {
|
||||
_.set(schema, 'x-component-props.hidden', !!v);
|
||||
|
||||
// 更新菜单对应的路由
|
||||
if (schema['__route__']?.id) {
|
||||
await updateRoute(schema['__route__'].id, {
|
||||
// Update the route corresponding to the menu
|
||||
await updateRoute(currentRoute.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 { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const { schema } = useSchemaToolbar();
|
||||
const { deleteRoute } = useNocoBaseRoutes();
|
||||
const currentRoute = useCurrentRouteData();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const schema = useFieldSchema();
|
||||
|
||||
return {
|
||||
title: t('Delete'),
|
||||
eventKey: 'remove',
|
||||
@ -134,8 +116,18 @@ export const pageTabSettings = new SchemaSettings({
|
||||
title: t('Delete block'),
|
||||
content: t('Are you sure you want to delete it?'),
|
||||
...confirm,
|
||||
onOk() {
|
||||
dn.remove(schema);
|
||||
async onOk() {
|
||||
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 { useFieldSchema } from '@formily/react';
|
||||
import { Space } from 'antd';
|
||||
import React from 'react';
|
||||
import { DragHandler, useDesignable } from '../..';
|
||||
@ -31,19 +30,16 @@ export const PageDesigner = ({ title }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const PageTabDesigner = ({ schema }) => {
|
||||
export const PageTabDesigner = () => {
|
||||
const { designable } = useDesignable();
|
||||
const { getAriaLabel } = useGetAriaLabelOfDesigner();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaSettingsRender(
|
||||
fieldSchema['x-settings'] || 'PageTabSettings',
|
||||
fieldSchema['x-settings-props'],
|
||||
);
|
||||
const { render } = useSchemaSettingsRender('PageTabSettings');
|
||||
|
||||
if (!designable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SchemaToolbarProvider schema={schema}>
|
||||
<div className={'general-schema-designer'}>
|
||||
<div className={'general-schema-designer-icons'}>
|
||||
<Space size={3} align={'center'}>
|
||||
@ -54,6 +50,5 @@ export const PageTabDesigner = ({ schema }) => {
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</SchemaToolbarProvider>
|
||||
);
|
||||
};
|
||||
|
@ -8,26 +8,10 @@
|
||||
*/
|
||||
|
||||
import { DocumentTitleProvider, Form, FormItem, Grid, IconPicker, Input } from '@nocobase/client';
|
||||
import { render, renderAppOptions, screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||
import React from 'react';
|
||||
import App1 from '../demos/demo1';
|
||||
import { renderAppOptions, screen, userEvent, waitFor } from '@nocobase/test/client';
|
||||
import { isTabPage, navigateToTab, Page } from '../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', () => {
|
||||
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) => {
|
||||
return props.uid ? <RequestSchemaComponent {...props} /> : null;
|
||||
});
|
||||
|
||||
RemoteSchemaComponent.displayName = 'RemoteSchemaComponent';
|
||||
|
@ -17,13 +17,14 @@ import React, {
|
||||
FC,
|
||||
startTransition,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useContext,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaComponentContext } from '../';
|
||||
import { SchemaInitializer, SchemaSettings, SchemaToolbarProvider, useSchemaInitializerRender } from '../application';
|
||||
import { useSchemaSettingsRender } from '../application/schema-settings/hooks/useSchemaSettingsRender';
|
||||
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 { SchemaSettingsDropdown } from './SchemaSettings';
|
||||
import { useGetAriaLabelOfDesigner } from './hooks/useGetAriaLabelOfDesigner';
|
||||
import { SchemaComponentContext } from '../';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
const titleCss = css`
|
||||
@ -208,6 +208,11 @@ export interface SchemaToolbarProps {
|
||||
spaceWrapperStyle?: React.CSSProperties;
|
||||
spaceClassName?: string;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -226,6 +231,7 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = React.memo((props) => {
|
||||
spaceStyle,
|
||||
toolbarClassName,
|
||||
toolbarStyle = {},
|
||||
container,
|
||||
} = {
|
||||
...props,
|
||||
...(fieldSchema?.['x-toolbar-props'] || {}),
|
||||
@ -312,7 +318,10 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = React.memo((props) => {
|
||||
while (parentElement && parentElement.clientHeight === 0) {
|
||||
parentElement = parentElement.parentElement;
|
||||
}
|
||||
if (!parentElement) {
|
||||
|
||||
const el = container || parentElement;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -330,18 +339,18 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = React.memo((props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(parentElement);
|
||||
if (style.position === 'static') {
|
||||
parentElement.style.position = 'relative';
|
||||
}
|
||||
// const style = window.getComputedStyle(parentElement);
|
||||
// if (style.position === 'static') {
|
||||
// parentElement.style.position = 'relative';
|
||||
// }
|
||||
|
||||
parentElement.addEventListener('mouseenter', show);
|
||||
parentElement.addEventListener('mouseleave', hide);
|
||||
el.addEventListener('mouseenter', show);
|
||||
el.addEventListener('mouseleave', hide);
|
||||
return () => {
|
||||
parentElement.removeEventListener('mouseenter', show);
|
||||
parentElement.removeEventListener('mouseleave', hide);
|
||||
el.removeEventListener('mouseenter', show);
|
||||
el.removeEventListener('mouseleave', hide);
|
||||
};
|
||||
}, [props.onVisibleChange]);
|
||||
}, [props.onVisibleChange, container]);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => ({
|
||||
|
@ -20,7 +20,7 @@ export const useGetAriaLabelOfDesigner = () => {
|
||||
const { name: _collectionName } = useCollection_deprecated();
|
||||
const getAriaLabel = useCallback(
|
||||
(name: string, postfix?: string) => {
|
||||
if (!fieldSchema) return '';
|
||||
if (!fieldSchema) return `designer-${name}-${postfix}`;
|
||||
|
||||
const component = fieldSchema['x-component'];
|
||||
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 { 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 { defineConfig };
|
||||
@ -360,7 +378,7 @@ export class NocoPage {
|
||||
|
||||
this.uid = schemaUid;
|
||||
this.desktopRouteId = routeId;
|
||||
this.url = `${this.options?.basePath || '/admin/'}${this.uid}`;
|
||||
this.url = `${this.options?.basePath || '/admin/'}${this.uid || this.desktopRouteId}`;
|
||||
}
|
||||
|
||||
async goto() {
|
||||
@ -393,7 +411,7 @@ export class NocoPage {
|
||||
|
||||
async destroy() {
|
||||
const waitList: any[] = [];
|
||||
if (this.uid) {
|
||||
if (this.uid || this.desktopRouteId !== undefined) {
|
||||
waitList.push(deletePage(this.uid, this.desktopRouteId));
|
||||
this.uid = undefined;
|
||||
this.desktopRouteId = undefined;
|
||||
@ -723,30 +741,15 @@ const createPage = async (options?: CreatePageOptions) => {
|
||||
const api = await request.newContext({
|
||||
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 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 pageSchemaUid = newPageSchema?.['x-uid'] || uid();
|
||||
const newTabSchemaUid = uid();
|
||||
const newTabSchemaName = uid();
|
||||
|
||||
const title = name || pageSchemaUid;
|
||||
|
||||
let routeId;
|
||||
let schemaUid;
|
||||
|
||||
@ -756,7 +759,6 @@ const createPage = async (options?: CreatePageOptions) => {
|
||||
data: {
|
||||
type: 'group',
|
||||
title,
|
||||
schemaUid: menuSchemaUid,
|
||||
hideInMenu: false,
|
||||
},
|
||||
});
|
||||
@ -767,17 +769,15 @@ const createPage = async (options?: CreatePageOptions) => {
|
||||
|
||||
const data = await result.json();
|
||||
routeId = data.data?.id;
|
||||
schemaUid = menuSchemaUid;
|
||||
}
|
||||
|
||||
if (type === 'page') {
|
||||
const result = await api.post('/api/desktopRoutes:create', {
|
||||
const routeResult = await api.post('/api/desktopRoutes:create', {
|
||||
headers,
|
||||
data: {
|
||||
type: 'page',
|
||||
title,
|
||||
schemaUid: newPageSchema?.['x-uid'] || pageSchemaUid,
|
||||
menuSchemaUid,
|
||||
schemaUid: pageSchemaUid,
|
||||
hideInMenu: false,
|
||||
enableTabs: !!newPageSchema?.['x-component-props']?.enablePageTabs,
|
||||
children: newPageSchema
|
||||
@ -786,21 +786,36 @@ const createPage = async (options?: CreatePageOptions) => {
|
||||
{
|
||||
type: 'tabs',
|
||||
title: '{{t("Unnamed")}}',
|
||||
schemaUid: tabSchemaUid,
|
||||
tabSchemaName,
|
||||
schemaUid: newTabSchemaUid,
|
||||
tabSchemaName: newTabSchemaName,
|
||||
hideInMenu: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.ok()) {
|
||||
throw new Error(await result.text());
|
||||
if (!routeResult.ok()) {
|
||||
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;
|
||||
schemaUid = menuSchemaUid;
|
||||
schemaUid = pageSchemaUid;
|
||||
}
|
||||
|
||||
if (type === 'link') {
|
||||
@ -809,7 +824,6 @@ const createPage = async (options?: CreatePageOptions) => {
|
||||
data: {
|
||||
type: 'link',
|
||||
title,
|
||||
schemaUid: menuSchemaUid,
|
||||
hideInMenu: false,
|
||||
options: {
|
||||
href: url,
|
||||
@ -823,50 +837,6 @@ const createPage = async (options?: CreatePageOptions) => {
|
||||
|
||||
const data = await result.json();
|
||||
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 };
|
||||
@ -1063,7 +1033,7 @@ const deleteMobileRoutes = async (mobileRouteId: number) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据页面 uid 删除一个 NocoBase 的页面
|
||||
* 根据页面 uid 删除一个页面的 schema,根据页面路由的 id 删除一个页面的路由
|
||||
*/
|
||||
const deletePage = async (pageUid: string, routeId: number) => {
|
||||
const api = await request.newContext({
|
||||
@ -1083,6 +1053,7 @@ const deletePage = async (pageUid: string, routeId: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (pageUid) {
|
||||
const result = await api.post(`/api/uiSchemas:remove/${pageUid}`, {
|
||||
headers,
|
||||
});
|
||||
@ -1090,6 +1061,7 @@ const deletePage = async (pageUid: string, routeId: number) => {
|
||||
if (!result.ok()) {
|
||||
throw new Error(await result.text());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCollections = async (collectionNames: string[]) => {
|
||||
|
@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
import { expect, test } from '@nocobase/test/e2e';
|
||||
import { oneTableBlock } from './utils';
|
||||
|
||||
test('allows to configure interface', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
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);
|
||||
}, roleData);
|
||||
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 updateRole({
|
||||
name: roleData.name,
|
||||
allowNewMenu: true,
|
||||
});
|
||||
await mockPage({ ...oneTableBlock, name: 'new page' }).goto();
|
||||
await mockPage({ name: 'new page' }).goto();
|
||||
await expect(page.getByLabel('new page')).toBeVisible();
|
||||
});
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { createForm, Form, onFormValuesChange } from '@formily/core';
|
||||
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 { Checkbox, message, Table } from 'antd';
|
||||
import { uniq } from 'lodash';
|
||||
@ -68,7 +68,7 @@ const style = css`
|
||||
|
||||
const translateTitle = (menus: any[], t, compile) => {
|
||||
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) {
|
||||
return {
|
||||
...menu,
|
||||
@ -123,7 +123,7 @@ const DesktopRoutesProvider: FC<{
|
||||
};
|
||||
|
||||
export const DesktopAllRoutesProvider: React.FC<{ active: boolean }> = ({ children, active }) => {
|
||||
const refreshRef = React.useRef(() => {});
|
||||
const refreshRef = React.useRef(() => { });
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
@ -166,6 +166,7 @@ export const MenuPermissions: React.FC<{
|
||||
);
|
||||
const resource = api.resource('roles.desktopRoutes', role.name);
|
||||
const allChecked = allIDList.length === IDList.length;
|
||||
const { refresh: refreshDesktopRoutes } = useAllAccessDesktopRoutes();
|
||||
|
||||
const handleChange = async (checked, menuItem) => {
|
||||
// 处理取消选中
|
||||
@ -214,6 +215,7 @@ export const MenuPermissions: React.FC<{
|
||||
values: shouldAdd,
|
||||
});
|
||||
}
|
||||
refreshDesktopRoutes();
|
||||
message.success(t('Saved successfully'));
|
||||
};
|
||||
|
||||
@ -288,6 +290,7 @@ export const MenuPermissions: React.FC<{
|
||||
});
|
||||
}
|
||||
refresh();
|
||||
refreshDesktopRoutes();
|
||||
message.success(t('Saved successfully'));
|
||||
}}
|
||||
/>{' '}
|
||||
|
@ -382,8 +382,11 @@ export const oneTableWithViewAction: PageConfig = {
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'j0k2m5r9z3b',
|
||||
'x-async': false,
|
||||
},
|
||||
},
|
||||
'x-uid': 'l6ioayfnq6c',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
handleDateChangeOnForm,
|
||||
useACLRoleContext,
|
||||
useActionContext,
|
||||
useApp,
|
||||
useCollection,
|
||||
useCollectionParentRecordData,
|
||||
useDesignable,
|
||||
@ -27,10 +28,8 @@ import {
|
||||
useLazy,
|
||||
usePopupUtils,
|
||||
useProps,
|
||||
useToken,
|
||||
withDynamicSchemaProps,
|
||||
withSkeletonComponent,
|
||||
useApp,
|
||||
withSkeletonComponent
|
||||
} from '@nocobase/client';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
@ -76,7 +75,7 @@ const getColorString = (
|
||||
};
|
||||
|
||||
export const DeleteEventContext = React.createContext({
|
||||
close: () => {},
|
||||
close: () => { },
|
||||
allowDeleteEvent: false,
|
||||
});
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
* 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 React from 'react';
|
||||
import { Plugin, useToken } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { generateNTemplate } from '../locale';
|
||||
import { CalendarV2 } from './calendar';
|
||||
import { calendarBlockSettings } from './calendar/Calender.Settings';
|
||||
|
@ -13,8 +13,6 @@ import { useField, useForm } from '@formily/react';
|
||||
import {
|
||||
CollectionField,
|
||||
css,
|
||||
getGroupMenuSchema,
|
||||
getLinkMenuSchema,
|
||||
getPageMenuSchema,
|
||||
getTabSchema,
|
||||
getVariableComponentWithScope,
|
||||
@ -26,6 +24,7 @@ import {
|
||||
useCollectionRecordData,
|
||||
useDataBlockRequestData,
|
||||
useDataBlockRequestGetter,
|
||||
useInsertPageSchema,
|
||||
useNocoBaseRoutes,
|
||||
useRequest,
|
||||
useRouterBasename,
|
||||
@ -575,8 +574,7 @@ export const createRoutesTableSchema = (collectionName: string, basename: string
|
||||
}
|
||||
|
||||
if (recordData.type === NocoBaseDesktopRouteType.page) {
|
||||
const path = `${basenameOfCurrentRouter.slice(0, -1)}${basename}/${
|
||||
isMobile ? recordData.schemaUid : recordData.menuSchemaUid
|
||||
const path = `${basenameOfCurrentRouter.slice(0, -1)}${basename}/${isMobile ? recordData.schemaUid : recordData.menuSchemaUid
|
||||
}`;
|
||||
// 在点击 Access 按钮时,会用到
|
||||
recordData._path = path;
|
||||
@ -1244,22 +1242,15 @@ function useCreateRouteSchema(isMobile: boolean) {
|
||||
const collectionName = 'uiSchemas';
|
||||
const api = useAPIClient();
|
||||
const resource = useMemo(() => api.resource(collectionName), [api, collectionName]);
|
||||
const insertPageSchema = useInsertPageSchema();
|
||||
|
||||
const createRouteSchema = useCallback(
|
||||
async ({
|
||||
title,
|
||||
icon,
|
||||
type,
|
||||
href,
|
||||
params,
|
||||
}: {
|
||||
title: string;
|
||||
icon: string;
|
||||
type: NocoBaseDesktopRouteType;
|
||||
href?: string;
|
||||
params?: Record<string, any>;
|
||||
}) => {
|
||||
const menuSchemaUid = uid();
|
||||
const menuSchemaUid = isMobile ? undefined : uid();
|
||||
const pageSchemaUid = uid();
|
||||
const tabSchemaName = uid();
|
||||
const tabSchemaUid = type === NocoBaseDesktopRouteType.page ? uid() : undefined;
|
||||
@ -1268,17 +1259,16 @@ function useCreateRouteSchema(isMobile: boolean) {
|
||||
[NocoBaseDesktopRouteType.page]: isMobile
|
||||
? getMobilePageSchema(pageSchemaUid, tabSchemaUid).schema
|
||||
: getPageMenuSchema({
|
||||
title,
|
||||
icon,
|
||||
pageSchemaUid,
|
||||
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) {
|
||||
await resource['insertAdjacent']({
|
||||
resourceIndex: 'mobile',
|
||||
@ -1288,17 +1278,12 @@ function useCreateRouteSchema(isMobile: boolean) {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await resource['insertAdjacent/nocobase-admin-menu']({
|
||||
position: 'beforeEnd',
|
||||
values: {
|
||||
schema: typeToSchema[type],
|
||||
},
|
||||
});
|
||||
await insertPageSchema(typeToSchema[type]);
|
||||
}
|
||||
|
||||
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 PluginLocalizationServer from '@nocobase/plugin-localization';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { tval } from '@nocobase/utils';
|
||||
import * as process from 'node:process';
|
||||
import { resolve } from 'path';
|
||||
import { getAntdLocale } from './antd';
|
||||
import { getCronLocale } from './cron';
|
||||
import { getCronstrueLocale } from './cronstrue';
|
||||
import PluginLocalizationServer from '@nocobase/plugin-localization';
|
||||
import { tval } from '@nocobase/utils';
|
||||
|
||||
async function getLang(ctx) {
|
||||
const SystemSetting = ctx.db.getRepository('systemSettings');
|
||||
@ -216,20 +216,35 @@ export class PluginClientServer extends Plugin {
|
||||
appends: ['desktopRoutes'],
|
||||
});
|
||||
|
||||
const desktopRoutesId = role
|
||||
.get('desktopRoutes')
|
||||
// hidden 为 true 的节点不会显示在权限配置表格中,所以无法被配置,需要被过滤掉
|
||||
.filter((item) => !item.hidden)
|
||||
.map((item) => item.id);
|
||||
// 1. 如果 page 的 children 为空,那么需要把 page 的 children 全部找出来,然后返回。否则前端会因为缺少 tab 路由的数据而导致页面空白
|
||||
// 2. 如果 page 的 children 不为空,不需要做特殊处理
|
||||
const desktopRoutesId = role.get('desktopRoutes').map(async (item, index, items) => {
|
||||
if (item.type === 'page' && !items.some((tab) => tab.parentId === item.id)) {
|
||||
const children = await desktopRoutesRepository.find({
|
||||
filter: {
|
||||
parentId: item.id,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = await desktopRoutesRepository.find({
|
||||
return [item.id, ...(children || []).map((child) => child.id)];
|
||||
}
|
||||
|
||||
return item.id;
|
||||
});
|
||||
|
||||
if (desktopRoutesId) {
|
||||
const ids = (await Promise.all(desktopRoutesId)).flat();
|
||||
const result = await desktopRoutesRepository.find({
|
||||
tree: true,
|
||||
...ctx.query,
|
||||
filter: {
|
||||
id: desktopRoutesId,
|
||||
id: ids,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = result;
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
}
|
||||
|
@ -29,25 +29,15 @@ export const useStyles = genStyleHook('nb-mobile-navigation-bar-action', (token)
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
'.schema-toolbar': {
|
||||
inset: '-15px -8px',
|
||||
},
|
||||
},
|
||||
'.nb-navigation-bar-action-title': {
|
||||
fontSize: 17,
|
||||
padding: 0,
|
||||
'.schema-toolbar': {
|
||||
inset: '-15px -8px',
|
||||
},
|
||||
},
|
||||
'.nb-navigation-bar-action-icon-and-title': {
|
||||
height: '32px !important',
|
||||
fontSize: '17px !important',
|
||||
padding: '0 6px !important',
|
||||
'.schema-toolbar': {
|
||||
inset: '-15px',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -7,20 +7,20 @@
|
||||
* 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 {
|
||||
SchemaSettings,
|
||||
SchemaToolbar,
|
||||
useSchemaToolbar,
|
||||
SchemaToolbarProvider,
|
||||
createTextSettingsItem,
|
||||
SchemaSettings,
|
||||
SchemaSettingsItemType,
|
||||
SchemaToolbar,
|
||||
SchemaToolbarProvider,
|
||||
useSchemaToolbar,
|
||||
} 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 { MobileRouteItem, useMobileRoutes } from '../../../../mobile-providers';
|
||||
|
||||
const remove = createTextSettingsItem({
|
||||
name: 'remove',
|
||||
@ -118,7 +118,7 @@ export const MobilePageTabsSettings: FC<MobilePageTabsSettingsProps> = ({ tab })
|
||||
settings={mobilePageTabsSettings}
|
||||
showBackground
|
||||
showBorder={false}
|
||||
toolbarStyle={{ inset: '-15px -12px' }}
|
||||
toolbarStyle={{ inset: '0 -12px' }}
|
||||
spaceWrapperStyle={{ top: 3 }}
|
||||
/>
|
||||
</SchemaToolbarProvider>
|
||||
|
@ -18,22 +18,22 @@ const { ExecutionPage } = lazy(() => import('./ExecutionPage'), 'ExecutionPage')
|
||||
const { WorkflowPage } = lazy(() => import('./WorkflowPage'), 'WorkflowPage');
|
||||
const { WorkflowPane } = lazy(() => import('./WorkflowPane'), 'WorkflowPane');
|
||||
|
||||
import { Trigger } from './triggers';
|
||||
import CollectionTrigger from './triggers/collection';
|
||||
import ScheduleTrigger from './triggers/schedule';
|
||||
import { NAMESPACE } from './locale';
|
||||
import { Instruction } from './nodes';
|
||||
import CalculationInstruction from './nodes/calculation';
|
||||
import ConditionInstruction from './nodes/condition';
|
||||
import CreateInstruction from './nodes/create';
|
||||
import DestroyInstruction from './nodes/destroy';
|
||||
import EndInstruction from './nodes/end';
|
||||
import QueryInstruction from './nodes/query';
|
||||
import CreateInstruction from './nodes/create';
|
||||
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 { 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 = {
|
||||
Component: BindWorkflowConfig,
|
||||
@ -76,7 +76,7 @@ export default class PluginWorkflowClient extends Plugin {
|
||||
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') {
|
||||
this.triggers.register(type, new 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') {
|
||||
this.instructions.register(type, new instruction());
|
||||
} else if (instruction instanceof Instruction) {
|
||||
@ -182,15 +182,15 @@ export default class PluginWorkflowClient extends Plugin {
|
||||
}
|
||||
|
||||
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 './utils';
|
||||
export * from './hooks';
|
||||
export { default as useStyles } from './style';
|
||||
export * from './variable';
|
||||
export * from './constants';
|
||||
export * from './ExecutionContextProvider';
|
||||
export * from './FlowContext';
|
||||
export * from './hooks';
|
||||
export * from './nodes';
|
||||
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