diff --git a/packages/core/app/client/.umirc.ts b/packages/core/app/client/.umirc.ts index f7fa98c609..7aa8a36332 100644 --- a/packages/core/app/client/.umirc.ts +++ b/packages/core/app/client/.umirc.ts @@ -17,7 +17,7 @@ export default defineConfig({ title: 'Loading...', devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false, favicons: [`${appPublicPath}favicon/favicon.ico`], - 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: [ { diff --git a/packages/core/client/package.json b/packages/core/client/package.json index 50d964fc02..dbae933bb4 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -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", diff --git a/packages/core/client/src/css-variable/CSSVariableProvider.tsx b/packages/core/client/src/css-variable/CSSVariableProvider.tsx index de6912bf77..d3c8d28daf 100644 --- a/packages/core/client/src/css-variable/CSSVariableProvider.tsx +++ b/packages/core/client/src/css-variable/CSSVariableProvider.tsx @@ -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); diff --git a/packages/core/client/src/global.less b/packages/core/client/src/global.less index 0a7b20ed1e..04061774d2 100644 --- a/packages/core/client/src/global.less +++ b/packages/core/client/src/global.less @@ -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; + } diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts index 8b33d7e223..964e16f96a 100644 --- a/packages/core/client/src/index.ts +++ b/packages/core/client/src/index.ts @@ -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'; + diff --git a/packages/core/client/src/locale/en-US.json b/packages/core/client/src/locale/en-US.json index 01ddf4f08e..f80a244aa7 100644 --- a/packages/core/client/src/locale/en-US.json +++ b/packages/core/client/src/locale/en-US.json @@ -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." } diff --git a/packages/core/client/src/locale/es-ES.json b/packages/core/client/src/locale/es-ES.json index 328137f0e3..3a3a45d646 100644 --- a/packages/core/client/src/locale/es-ES.json +++ b/packages/core/client/src/locale/es-ES.json @@ -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." } diff --git a/packages/core/client/src/locale/fr-FR.json b/packages/core/client/src/locale/fr-FR.json index 4342e0cca1..c5d0ee72d0 100644 --- a/packages/core/client/src/locale/fr-FR.json +++ b/packages/core/client/src/locale/fr-FR.json @@ -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." } diff --git a/packages/core/client/src/locale/ja-JP.json b/packages/core/client/src/locale/ja-JP.json index 326f25aca1..7013d30af2 100644 --- a/packages/core/client/src/locale/ja-JP.json +++ b/packages/core/client/src/locale/ja-JP.json @@ -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.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。" } diff --git a/packages/core/client/src/locale/ko-KR.json b/packages/core/client/src/locale/ko-KR.json index caf538a964..9e17f29876 100644 --- a/packages/core/client/src/locale/ko-KR.json +++ b/packages/core/client/src/locale/ko-KR.json @@ -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.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다." } diff --git a/packages/core/client/src/locale/pt-BR.json b/packages/core/client/src/locale/pt-BR.json index 155f98f68f..1bacded113 100644 --- a/packages/core/client/src/locale/pt-BR.json +++ b/packages/core/client/src/locale/pt-BR.json @@ -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." } diff --git a/packages/core/client/src/locale/ru-RU.json b/packages/core/client/src/locale/ru-RU.json index bf4f6775ce..c85a74cdd1 100644 --- a/packages/core/client/src/locale/ru-RU.json +++ b/packages/core/client/src/locale/ru-RU.json @@ -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.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее." } diff --git a/packages/core/client/src/locale/tr-TR.json b/packages/core/client/src/locale/tr-TR.json index 126267b0c8..3845bc75a0 100644 --- a/packages/core/client/src/locale/tr-TR.json +++ b/packages/core/client/src/locale/tr-TR.json @@ -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." } diff --git a/packages/core/client/src/locale/uk-UA.json b/packages/core/client/src/locale/uk-UA.json index bac2c81d1a..c381200d65 100644 --- a/packages/core/client/src/locale/uk-UA.json +++ b/packages/core/client/src/locale/uk-UA.json @@ -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.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її." } diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index acbb7b43f8..ee7937d066 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -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.": "隐藏后,该标签将不再显示在标签栏中。要想再次显示它,你需要到路由管理页面进行设置。" } diff --git a/packages/core/client/src/locale/zh-TW.json b/packages/core/client/src/locale/zh-TW.json index 1d4361c0af..fd825c88cf 100644 --- a/packages/core/client/src/locale/zh-TW.json +++ b/packages/core/client/src/locale/zh-TW.json @@ -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.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。" } diff --git a/packages/core/client/src/modules/actions/__e2e__/link/templates.ts b/packages/core/client/src/modules/actions/__e2e__/link/templates.ts index e9578e32ad..1e5af6a0fc 100644 --- a/packages/core/client/src/modules/actions/__e2e__/link/templates.ts +++ b/packages/core/client/src/modules/actions/__e2e__/link/templates.ts @@ -884,7 +884,7 @@ export const URLSearchParamsUseAssociationFieldValue = { linkageAction: true, }, 'x-component-props': { - url: '/admin/ids0d9esx8k', + url: '/admin/ocal3pnltf2', params: [ { name: 'roles', diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts index ffaf80aa15..3ca864e3c2 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts @@ -3824,10 +3824,11 @@ export const T4334 = { }, }, 'x-uid': 'ribk031tkp8', - 'x-async': false, + 'x-async': true, 'x-index': 1, }, }, + 'x-uid': '1j5z1j5z1j5', }, }; diff --git a/packages/core/client/src/modules/menu/GroupItem.tsx b/packages/core/client/src/modules/menu/GroupItem.tsx index 2571af23f8..e2acf4b353 100644 --- a/packages/core/client/src/modules/menu/GroupItem.tsx +++ b/packages/core/client/src/modules/menu/GroupItem.tsx @@ -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 ; }; - -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, - }; -} diff --git a/packages/core/client/src/modules/menu/LinkMenuItem.tsx b/packages/core/client/src/modules/menu/LinkMenuItem.tsx index 93b2c6ade8..8217f7de14 100644 --- a/packages/core/client/src/modules/menu/LinkMenuItem.tsx +++ b/packages/core/client/src/modules/menu/LinkMenuItem.tsx @@ -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 ; }; - -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, - }; -} diff --git a/packages/core/client/src/modules/menu/PageMenuItem.tsx b/packages/core/client/src/modules/menu/PageMenuItem.tsx index 675de5746a..819e11592c 100644 --- a/packages/core/client/src/modules/menu/PageMenuItem.tsx +++ b/packages/core/client/src/modules/menu/PageMenuItem.tsx @@ -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,46 +105,25 @@ 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 ; }; -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, - }, + 'x-component': 'Page', properties: { - page: { + [tabSchemaName]: { type: 'void', - 'x-component': 'Page', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: {}, + 'x-uid': tabSchemaUid, 'x-async': true, - properties: { - [tabSchemaName]: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'page:addBlock', - properties: {}, - 'x-uid': tabSchemaUid, - }, - }, - 'x-uid': pageSchemaUid, }, }, - 'x-uid': menuSchemaUid, - __route__: route, + 'x-uid': pageSchemaUid, }; } diff --git a/packages/core/client/src/modules/menu/__e2e__/dragAndDrop.test.ts b/packages/core/client/src/modules/menu/__e2e__/dragAndDrop.test.ts index 770cefd04d..5ca3b7f0cc 100644 --- a/packages/core/client/src/modules/menu/__e2e__/dragAndDrop.test.ts +++ b/packages/core/client/src/modules/menu/__e2e__/dragAndDrop.test.ts @@ -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(); diff --git a/packages/core/client/src/modules/page/__e2e__/router.test.ts b/packages/core/client/src/modules/page/__e2e__/router.test.ts index d9aa85995c..4692e7a112 100644 --- a/packages/core/client/src/modules/page/__e2e__/router.test.ts +++ b/packages/core/client/src/modules/page/__e2e__/router.test.ts @@ -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(); diff --git a/packages/core/client/src/modules/popup/__e2e__/deletedPopups.test.ts b/packages/core/client/src/modules/popup/__e2e__/deletedPopups.test.ts index af82147663..60f9964cac 100644 --- a/packages/core/client/src/modules/popup/__e2e__/deletedPopups.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/deletedPopups.test.ts @@ -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 diff --git a/packages/core/client/src/modules/popup/__e2e__/router.test.ts b/packages/core/client/src/modules/popup/__e2e__/router.test.ts index 690b235b89..1d534bc912 100644 --- a/packages/core/client/src/modules/popup/__e2e__/router.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/router.test.ts @@ -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', diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx index 3caea7ddc7..b9d61e9123 100644 --- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx +++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx @@ -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 ( = (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 ? : null; })} + + + + + ); }); diff --git a/packages/core/client/src/route-switch/antd/admin-layout/__tests__/convertRoutesToSchema.test.ts b/packages/core/client/src/route-switch/antd/admin-layout/__tests__/convertRoutesToSchema.test.ts deleted file mode 100644 index 3115361ccb..0000000000 --- a/packages/core/client/src/route-switch/antd/admin-layout/__tests__/convertRoutesToSchema.test.ts +++ /dev/null @@ -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', - }); - }); -}); diff --git a/packages/core/client/src/route-switch/antd/admin-layout/convertRoutesToSchema.ts b/packages/core/client/src/route-switch/antd/admin-layout/convertRoutesToSchema.ts index 6f4e636d88..16fbf9ea85 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/convertRoutesToSchema.ts +++ b/packages/core/client/src/route-switch/antd/admin-layout/convertRoutesToSchema.ts @@ -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, - }; -} diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index 5142f86838..e41cdc9a8d 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -7,36 +7,32 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { EllipsisOutlined } from '@ant-design/icons'; +import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layout'; +import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header'; import { css } from '@emotion/css'; -import { ConfigProvider, Divider, Layout } from 'antd'; -import { createGlobalStyle } from 'antd-style'; -import React, { - createContext, - FC, - memo, - startTransition, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { Outlet } from 'react-router-dom'; +import { Popover, Tooltip } from 'antd'; +import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import ReactDOM from 'react-dom'; +import { Link, Navigate, Outlet, useLocation } from 'react-router-dom'; import { ACLRolesCheckProvider, + AppNotFound, CurrentAppInfoProvider, - findByUid, - findMenuItem, + DndContext, + Icon, + ParentRouteContext, PinnedPluginList, RemoteCollectionManagerProvider, RemoteSchemaComponent, RemoteSchemaTemplateManagerPlugin, RemoteSchemaTemplateManagerProvider, - SchemaComponent, - useAdminSchemaUid, - useDocumentTitle, + SortableItem, + useDesignable, + useMenuDragEnd, + useParseURLAndParams, useRequest, + useSchemaInitializerRender, useSystemSettings, useToken, } from '../../../'; @@ -45,44 +41,30 @@ import { CurrentTabUidProvider, IsSubPageClosedByPageMenuProvider, useCurrentPageUid, - useIsInSettingsPage, - useMatchAdmin, - useMatchAdminName, - useNavigateNoUpdate, + useLocationNoUpdate, } from '../../../application/CustomRouterContextProvider'; import { Plugin } from '../../../application/Plugin'; -import { useMenuTranslation } from '../../../schema-component/antd/menu/locale'; -import { Help } from '../../../user/Help'; +import { menuItemInitializer } from '../../../modules/menu/menuItemInitializer'; import { KeepAlive } from './KeepAlive'; -import { convertRoutesToSchema, NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema'; +import { NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema'; +import { MenuSchemaToolbar, ResetThemeTokenAndKeepAlgorithm } from './menuItemSettings'; import { userCenterSettings } from './userCenterSettings'; -import { UserCenter } from './UserCenterButton'; + export { KeepAlive, NocoBaseDesktopRouteType }; -const RouteContext = createContext(null); -RouteContext.displayName = 'RouteContext'; +export const NocoBaseRouteContext = createContext(null); +NocoBaseRouteContext.displayName = 'NocoBaseRouteContext'; const CurrentRouteProvider: FC<{ uid: string }> = ({ children, uid }) => { const { allAccessRoutes } = useAllAccessDesktopRoutes(); - const routeNode = useMemo(() => getRouteNodeBySchemaUid(uid, allAccessRoutes), [uid, allAccessRoutes]); - return {children}; + const routeNode = useMemo(() => findRouteBySchemaUid(uid, allAccessRoutes), [uid, allAccessRoutes]); + return {children}; }; -export const useCurrentRoute = () => { - return useContext(RouteContext) || {}; +export const useCurrentRouteData = () => { + return useContext(NocoBaseRouteContext) || {}; }; -const useMenuProps = () => { - const currentPageUid = useCurrentPageUid(); - return { - selectedUid: currentPageUid, - defaultSelectedUid: currentPageUid, - }; -}; - -const MenuSchemaRequestContext = createContext(null); -MenuSchemaRequestContext.displayName = 'MenuSchemaRequestContext'; - const emptyArray = []; const AllAccessDesktopRoutesContext = createContext<{ allAccessRoutes: NocoBaseDesktopRoute[]; @@ -97,71 +79,14 @@ export const useAllAccessDesktopRoutes = () => { return useContext(AllAccessDesktopRoutesContext); }; -const MenuSchemaRequestProvider: FC = ({ children }) => { - const { t } = useMenuTranslation(); - const { setTitle: _setTitle } = useDocumentTitle(); - const setTitle = useCallback((title) => _setTitle(t(title)), [_setTitle, t]); - const navigate = useNavigateNoUpdate(); - const isMatchAdmin = useMatchAdmin(); - const isMatchAdminName = useMatchAdminName(); - const currentPageUid = useCurrentPageUid(); - const isDynamicPage = !!currentPageUid; - const adminSchemaUid = useAdminSchemaUid(); - - const { data, refresh } = useRequest<{ +const RoutesRequestProvider: FC = ({ children }) => { + const mountedRef = useRef(false); + const { data, refresh, loading } = useRequest<{ data: any; - }>( - { - url: `/desktopRoutes:listAccessible`, - params: { tree: true, sort: 'sort' }, - }, - { - refreshDeps: [adminSchemaUid], - onSuccess(data) { - const schema = convertRoutesToSchema(data?.data); - // url 为 `/admin` 的情况 - if (isMatchAdmin) { - const s = findMenuItem(schema); - if (s) { - navigate(`/admin/${s['x-uid']}`); - startTransition(() => { - setTitle(s.title); - }); - } else { - navigate(`/admin/`); - } - return; - } - - // url 不为 `/admin/xxx` 的情况,不做处理 - if (!isMatchAdminName || !isDynamicPage) return; - - // url 为 `admin/xxx` 的情况 - const s = findByUid(schema, currentPageUid); - if (s) { - startTransition(() => { - setTitle(s.title); - }); - } else { - const s = findMenuItem(schema); - if (s) { - navigate(`/admin/${s['x-uid']}`); - startTransition(() => { - setTitle(s.title); - }); - } else { - navigate(`/admin/`); - } - } - }, - }, - ); - - const menuSchema = useMemo(() => { - if (data?.data) { - return convertRoutesToSchema(data?.data); - } - }, [data?.data]); + }>({ + url: `/desktopRoutes:listAccessible`, + params: { tree: true, sort: 'sort' }, + }); const allAccessRoutesValue = useMemo(() => { return { @@ -170,183 +95,55 @@ const MenuSchemaRequestProvider: FC = ({ children }) => { }; }, [data?.data, refresh]); + // Only valid on first load + if (loading && !mountedRef.current) { + return null; + } else { + mountedRef.current = true; + } + return ( - {children} + {children} ); }; -const MenuEditor = (props) => { - const { t } = useMenuTranslation(); - const { setTitle: _setTitle } = useDocumentTitle(); - const setTitle = useCallback((title) => _setTitle(t(title)), [_setTitle, t]); - const navigate = useNavigateNoUpdate(); - const isInSettingsPage = useIsInSettingsPage(); - const isMatchAdmin = useMatchAdmin(); - const isMatchAdminName = useMatchAdminName(); - const currentPageUid = useCurrentPageUid(); - const { sideMenuRef } = props; - const [current, setCurrent] = useState(null); - const menuSchema = useContext(MenuSchemaRequestContext); - - const onSelect = useCallback( - ({ item }: { item; key; keyPath; domEvent }) => { - const schema = item.props.schema; - startTransition(() => { - setTitle(schema.title); - setCurrent(schema); - }); - navigate(`/admin/${schema['x-uid']}`); - }, - [navigate, setTitle], - ); - - useEffect(() => { - const properties = Object.values(current?.root?.properties || {}).shift()?.['properties'] || menuSchema?.properties; - if (sideMenuRef.current) { - const pageType = - properties && - Object.values(properties).find( - (item) => item['x-uid'] === currentPageUid && item['x-component'] === 'Menu.Item', - ); - if (pageType || isInSettingsPage) { - sideMenuRef.current.style.display = 'none'; - } else { - sideMenuRef.current.style.display = 'block'; - } - } - }, [current?.root?.properties, currentPageUid, menuSchema?.properties, isInSettingsPage, sideMenuRef]); - - const schema = useMemo(() => { - const s = menuSchema; - if (s?.['x-component-props']) { - s['x-component-props']['useProps'] = useMenuProps; - } - return s; - }, [menuSchema]); - - useEffect(() => { - if (isMatchAdminName) { - const s = findByUid(schema, currentPageUid); - if (s) { - startTransition(() => { - setTitle(s.title); - }); - } - } - }, [currentPageUid, isMatchAdmin, isMatchAdminName, schema, setTitle]); - - const scope = useMemo(() => { - return { useMenuProps, onSelect, sideMenuRef }; - }, [onSelect, sideMenuRef]); - - return ; -}; - -/** - * 鼠标悬浮在顶部“更多”按钮时显示的子菜单的样式 - */ -const GlobalStyleForAdminLayout = createGlobalStyle` - .nb-container-of-header-submenu { - .ant-menu.ant-menu-submenu.ant-menu-submenu-popup { - .ant-menu.ant-menu-sub.ant-menu-vertical { - background-color: ${(p) => { - // @ts-ignore - return p.theme.colorBgHeader + ' !important'; - }}; - color: ${(p) => { - // @ts-ignore - return p.theme.colorTextHeaderMenu + ' !important'; - }}; - .ant-menu-item:hover { - color: ${(p) => { - // @ts-ignore - return p.theme.colorTextHeaderMenuHover + ' !important'; - }}; - background-color: ${(p) => { - // @ts-ignore - return p.theme.colorBgHeaderMenuHover + ' !important'; - }}; - } - .ant-menu-item.ant-menu-item-selected { - color: ${(p) => { - // @ts-ignore - return p.theme.colorTextHeaderMenuActive + ' !important'; - }}; - background-color: ${(p) => { - // @ts-ignore - return p.theme.colorBgHeaderMenuActive + ' !important'; - }}; - } - } - } +const noAccessPermission = (currentPageUid: string, allAccessRoutes: NocoBaseDesktopRoute[]) => { + if (!currentPageUid) { + return false; } -`; -/** - * 确保顶部菜单的子菜单的主题样式正确 - * @param param0 - * @returns - */ -const SetThemeOfHeaderSubmenu = ({ children }) => { - const containerRef = useRef(null); - - useEffect(() => { - containerRef.current = document.createElement('div'); - containerRef.current.classList.add('nb-container-of-header-submenu'); - document.body.appendChild(containerRef.current); - - return () => { - document.body.removeChild(containerRef.current); - }; - }, []); - - const getPopupContainer = useCallback(() => containerRef.current, []); - - return {children}; -}; - -const sideClass = css` - height: 100%; - /* position: fixed; */ - position: relative; - left: 0; - top: 0; - background: rgba(0, 0, 0, 0); - z-index: 100; - .ant-layout-sider-children { - top: var(--nb-header-height); - position: fixed; - width: 200px; - height: calc(100vh - var(--nb-header-height)); + const routeNode = findRouteBySchemaUid(currentPageUid, allAccessRoutes); + if (!routeNode) { + return true; } -`; -const InternalAdminSideBar: FC<{ pageUid: string; sideMenuRef: any }> = memo((props) => { - if (!props.pageUid) return null; - return ; -}); -InternalAdminSideBar.displayName = 'InternalAdminSideBar'; - -const AdminSideBar = ({ sideMenuRef }) => { - const currentPageUid = useCurrentPageUid(); - return ; + return false; }; export const AdminDynamicPage = () => { const currentPageUid = useCurrentPageUid(); + const { allAccessRoutes } = useAllAccessDesktopRoutes(); - return ( - {(uid) => } - ); + // Group page should not request schema data + if (isGroup(currentPageUid, allAccessRoutes)) { + return null; + } + + // 404 page + if (noAccessPermission(currentPageUid, allAccessRoutes)) { + return ; + } + + return {(uid) => }; }; const layoutContentClass = css` display: flex; flex-direction: column; position: relative; - height: 100vh; + height: calc(100vh - var(--nb-header-height)); > div { position: relative; } @@ -360,14 +157,6 @@ const layoutContentClass = css` } `; -const layoutContentHeaderClass = css` - flex-shrink: 0; - height: var(--nb-header-height); - line-height: var(--nb-header-height); - background: transparent; - pointer-events: none; -`; - const style1: any = { position: 'relative', width: '100%', @@ -384,7 +173,8 @@ const style2: any = { }; const className1 = css` - width: 200px; + width: 168px; + margin-right: 4px; display: inline-flex; flex-shrink: 0; color: #fff; @@ -392,13 +182,11 @@ const className1 = css` align-items: center; `; const className2 = css` - padding: 0 16px; object-fit: contain; width: 100%; height: 100%; `; const className3 = css` - padding: 0 16px; width: 100%; height: 100%; font-weight: 500; @@ -417,11 +205,6 @@ const className5 = css` height: 100%; z-index: 10; `; -const theme = { - token: { - colorSplit: 'rgba(255, 255, 255, 0.1)', - }, -}; const pageContentStyle: React.CSSProperties = { flex: 1, @@ -434,13 +217,11 @@ export const LayoutContent = () => { /* Use the "nb-subpages-slot-without-header-and-side" class name to locate the position of the subpages */ return ( - -
+
- {/* {service.contentLoading ? render() : } */} - +
); }; @@ -461,95 +242,441 @@ const NocoBaseLogo = () => { return
{result?.loading ? null : logo}
; }; -export const InternalAdminLayout = () => { - const { token } = useToken(); - const sideMenuRef = useRef(); +/** + * Fix the issue where SchemaToolbar cannot be displayed normally in Group + * @returns + */ +const MenuSchemaToolbarWithContainer = () => { + const divRef = useRef(null); + const [container, setContainer] = React.useState(null); - const layoutHeaderCss = useMemo(() => { - return css` - .ant-menu.ant-menu-dark .ant-menu-item-selected, - .ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected, - .ant-menu-submenu-horizontal.ant-menu-submenu-selected { - background-color: ${token.colorBgHeaderMenuActive} !important; - color: ${token.colorTextHeaderMenuActive} !important; - } - .ant-menu-submenu-horizontal.ant-menu-submenu-selected > .ant-menu-submenu-title { - color: ${token.colorTextHeaderMenuActive} !important; - } - .ant-menu-dark.ant-menu-horizontal > .ant-menu-item:hover { - background-color: ${token.colorBgHeaderMenuHover} !important; - color: ${token.colorTextHeaderMenuHover} !important; - } - - position: fixed; - left: 0; - right: 0; - height: var(--nb-header-height); - line-height: var(--nb-header-height); - padding: 0; - z-index: 100; - background-color: ${token.colorBgHeader} !important; - - .ant-menu { - background-color: transparent; - } - - .ant-menu-item, - .ant-menu-submenu-horizontal { - color: ${token.colorTextHeaderMenu} !important; - } - `; - }, [ - token.colorBgHeaderMenuActive, - token.colorTextHeaderMenuActive, - token.colorBgHeaderMenuHover, - token.colorTextHeaderMenuHover, - token.colorBgHeader, - token.colorTextHeaderMenu, - ]); + useEffect(() => { + setContainer(divRef.current.parentElement.parentElement.parentElement); + }, []); return ( - - - -
-
- -
- - - -
-
-
- - - - - - -
-
-
- - -
+ <> + +
+ ); }; +const GroupItem: FC<{ item: any }> = (props) => { + const { item } = props; + const { designable } = useDesignable(); + + // fake schema used to pass routing information to SortableItem + const fakeSchema: any = { __route__: item._route }; + + return ( + + + + {props.children} + {designable && } + + + + ); +}; + +const WithTooltip: FC<{ title: string; hidden: boolean }> = (props) => { + const { inHeader } = useContext(headerContext); + + return ( + + {(context) => + context.collapsed && !props.hidden && !inHeader ? ( + + {props.children} + + ) : ( + props.children + ) + } + + ); +}; + +const MenuItem: FC<{ item: any; options: { isMobile: boolean; collapsed: boolean } }> = (props) => { + const { item } = props; + const { parseURLAndParams } = useParseURLAndParams(); + const divRef = useRef(null); + const location = useLocation(); + + useEffect(() => { + if (divRef.current) { + // 顶部 Add menu item 按钮放置在右侧 + divRef.current.parentElement.parentElement.style.order = 999; + + divRef.current.parentElement.parentElement.style.paddingLeft = 0; + divRef.current.parentElement.parentElement.style.padding = 0; + } + }, []); + + const handleClickLink = useCallback( + async (event: React.MouseEvent) => { + const href = item._route.options?.href; + const params = item._route.options?.params; + + event.preventDefault(); + event.stopPropagation(); + + try { + const url = await parseURLAndParams(href, params || []); + window.open(url, '_blank'); + } catch (err) { + console.error(err); + window.open(href, '_blank'); + } + }, + [parseURLAndParams, item], + ); + + if (item._hidden) { + return null; + } + + const fakeSchema: any = { __route__: item._route }; + + // "Add menu item" does not need SchemaToolbar + if (item.key === 'x-designer-button') { + return ( +
+ + + {props.children} + + +
+ ); + } + + if (item._route?.type === NocoBaseDesktopRouteType.link) { + // fake schema used to pass routing information to SortableItem + return ( + + + +
+ {/* 这里是为了扩大点击区域 */} + + {props.children} + +
+ +
+
+
+ ); + } + + // 如果点击的是一个 group,直接跳转到第一个子页面 + const path = item.redirect || item.path; + + return ( + + + + + + + + + ); +}; + +const resetStyle = css` + .ant-layout-sider-children { + margin-inline-end: 0 !important; + } + + .ant-layout-header.ant-pro-layout-header { + border-block-end: none !important; + } + + // 固定菜单中图标和标题的距离,防止当切换到紧凑模式后,图标和标题之间的距离过近 + .ant-menu-title-content .ant-pro-base-menu-inline-item-title, + .ant-menu-title-content .ant-pro-base-menu-horizontal-item-title { + gap: 8px; + } + + // 修复紧凑模式下且菜单收起时,菜单的高度不够的问题 + .ant-pro-base-menu-vertical-collapsed .ant-pro-base-menu-vertical-menu-item { + height: auto; + } +`; + +const contentStyle = { + paddingBlock: 0, + paddingInline: 0, +}; + +const headerContext = React.createContext<{ inHeader: boolean }>({ inHeader: false }); + +const popoverStyle = css` + .ant-popover-inner { + padding: 0; + overflow: hidden; + } +`; + +const MobileActions: FC = (props) => { + const { token } = useToken(); + + return ( + } color={token.colorBgHeader}> +
+ +
+
+ ); +}; + +const actionsRender = (props) => { + if (props.isMobile) { + return ; + } + + return ; +}; + +const menuItemRender = (item, dom, options) => { + return ( + + {dom} + + ); +}; + +const subMenuItemRender = (item, dom) => { + return {dom}; +}; + +const CollapsedButton: FC<{ collapsed: boolean }> = (props) => { + return ( + + {(context) => + context.isMobile ? ( + <>{props.children} + ) : ( + ReactDOM.createPortal( +
+ {props.children} +
, + document.body, + ) + ) + } +
+ ); +}; + +const collapsedButtonRender = (collapsed, dom) => { + return {dom}; +}; + +const headerContextValue = { inHeader: true }; +const headerRender = (props: HeaderViewProps, defaultDom: React.ReactNode) => { + return {defaultDom}; +}; + +export const InternalAdminLayout = () => { + const { allAccessRoutes } = useAllAccessDesktopRoutes(); + const { render: renderInitializer } = useSchemaInitializerRender(menuItemInitializer); + const { designable } = useDesignable(); + const location = useLocation(); + const { onDragEnd } = useMenuDragEnd(); + const { token } = useToken(); + const [isMobile, setIsMobile] = useState(false); + const [collapsed, setCollapsed] = useState(false); + const doNotChangeCollapsedRef = useRef(false); + const route = useMemo(() => { + return { + path: '/', + children: convertRoutesToLayout(allAccessRoutes, { renderInitializer, designable, isMobile }), + }; + }, [allAccessRoutes, renderInitializer, designable, isMobile]); + const layoutToken = useMemo(() => { + return { + header: { + colorBgHeader: token.colorBgHeader, + colorTextMenu: token.colorTextHeaderMenu, + colorTextMenuSelected: token.colorTextHeaderMenuActive, + colorTextMenuActive: token.colorTextHeaderMenuActive, + colorBgMenuItemHover: token.colorBgHeaderMenuHover, + colorBgMenuItemSelected: token.colorBgHeaderMenuActive, + heightLayoutHeader: 46, + colorHeaderTitle: token.colorTextHeaderMenu, + }, + sider: { + colorMenuBackground: token.colorBgContainer, + colorTextMenu: token.colorText, + colorTextMenuSelected: token.colorPrimary, + colorBgMenuItemSelected: token.colorPrimaryBg, + colorBgMenuItemActive: token.colorPrimaryBg, + colorBgMenuItemHover: token.colorBgTextHover, + }, + bgLayout: token.colorBgLayout, + }; + }, [token]); + + const onCollapse = useCallback((collapsed: boolean) => { + if (doNotChangeCollapsedRef.current) { + return; + } + setCollapsed(collapsed); + }, []); + + const onPageChange = useCallback(() => { + doNotChangeCollapsedRef.current = true; + setTimeout(() => { + doNotChangeCollapsedRef.current = false; + }); + }, []); + + return ( + + } + title={''} + layout="mix" + splitMenus + token={layoutToken} + headerRender={headerRender} + menuItemRender={menuItemRender} + subMenuItemRender={subMenuItemRender} + collapsedButtonRender={collapsedButtonRender} + onCollapse={onCollapse} + collapsed={collapsed} + onPageChange={onPageChange} + > + + {(value: RouteContextType) => { + const { isMobile: _isMobile } = value; + + if (_isMobile !== isMobile) { + setIsMobile(_isMobile); + } + + return ; + }} + + + + ); +}; + +function getDefaultPageUid(routes: NocoBaseDesktopRoute[]) { + // Find the first route of type "page" + for (const route of routes) { + if (route.type === NocoBaseDesktopRouteType.page) { + return route.schemaUid; + } + + if (route.children?.length) { + const result = getDefaultPageUid(route.children); + if (result) { + return result; + } + } + } +} + +const NavigateToDefaultPage: FC = (props) => { + const { allAccessRoutes } = useAllAccessDesktopRoutes(); + const location = useLocationNoUpdate(); + + const defaultPageUid = getDefaultPageUid(allAccessRoutes); + + return ( + <> + {props.children} + {defaultPageUid && location.pathname === '/admin' && } + + ); +}; + +const findRouteByMenuSchemaUid = (schemaUid: string, routes: NocoBaseDesktopRoute[]) => { + if (!routes) return; + + for (const route of routes) { + if (route.menuSchemaUid === schemaUid) { + return route; + } + + if (route.children?.length) { + const result = findRouteByMenuSchemaUid(schemaUid, route.children); + if (result) { + return result; + } + } + } +}; + +/** + * Compatibility with legacy page routes + * @param props + * @returns + */ +const LegacyRouteCompat: FC = (props) => { + const currentPageUid = useCurrentPageUid(); + const { allAccessRoutes } = useAllAccessDesktopRoutes(); + const route = findRouteByMenuSchemaUid(currentPageUid, allAccessRoutes); + + if (route) { + return ; + } + + return <>{props.children}; +}; + export const AdminProvider = (props) => { return ( - - - - {props.children} - - - + + + + + + {props.children} + + + + + @@ -575,14 +702,16 @@ export class AdminLayoutPlugin extends Plugin { } } -function getRouteNodeBySchemaUid(schemaUid: string, treeArray: any[]) { +export function findRouteBySchemaUid(schemaUid: string, treeArray: any[]) { + if (!treeArray) return; + for (const node of treeArray) { if (schemaUid === node.schemaUid || schemaUid === node.menuSchemaUid) { return node; } if (node.children?.length) { - const result = getRouteNodeBySchemaUid(schemaUid, node.children); + const result = findRouteBySchemaUid(schemaUid, node.children); if (result) { return result; } @@ -590,3 +719,146 @@ function getRouteNodeBySchemaUid(schemaUid: string, treeArray: any[]) { } return null; } + +const MenuItemIcon: FC<{ icon: string; title: string }> = (props) => { + const { inHeader } = useContext(headerContext); + + return ( + + {(value: RouteContextType) => { + const { collapsed } = value; + + if (collapsed && !inHeader) { + return props.icon ? ( + + ) : ( + + {props.title.charAt(0)} + + ); + } + + return props.icon ? : null; + }} + + ); +}; + +function convertRoutesToLayout( + routes: NocoBaseDesktopRoute[], + { renderInitializer, designable, parentRoute, isMobile, depth = 0 }: any, +) { + if (!routes) return; + + const getInitializerButton = (testId: string) => { + return { + key: 'x-designer-button', + name: renderInitializer({ + style: { background: 'none' }, + 'data-testid': testId, + }), + path: '/', + disabled: true, + icon: , + }; + }; + + const result: any[] = routes.map((item) => { + if (item.type === NocoBaseDesktopRouteType.link) { + return { + name: item.title, + icon: item.icon ? : null, + path: '/', + hideInMenu: item.hideInMenu, + _route: item, + _parentRoute: parentRoute, + }; + } + + if (item.type === NocoBaseDesktopRouteType.page) { + return { + name: item.title, + icon: item.icon ? : null, + path: `/admin/${item.schemaUid}`, + redirect: `/admin/${item.schemaUid}`, + hideInMenu: item.hideInMenu, + _route: item, + _parentRoute: parentRoute, + }; + } + + if (item.type === NocoBaseDesktopRouteType.group) { + const children = + convertRoutesToLayout(item.children, { renderInitializer, designable, parentRoute: item, depth: depth + 1 }) || + []; + + // add a designer button + if (designable && depth === 0) { + children.push({ ...getInitializerButton('schema-initializer-Menu-side'), _parentRoute: item }); + } + + return { + name: item.title, + icon: item.icon ? : null, + path: `/admin/${item.id}`, + redirect: + children[0]?.key === 'x-designer-button' + ? undefined + : `/admin/${findFirstPageRoute(item.children)?.schemaUid || ''}`, + routes: children.length === 0 ? [{ path: '/', name: ' ', disabled: true, _hidden: true }] : children, + hideInMenu: item.hideInMenu, + _route: item, + _depth: depth, + }; + } + }); + + if (designable && depth === 0) { + isMobile + ? result.push({ ...getInitializerButton('schema-initializer-Menu-header') }) + : result.unshift({ ...getInitializerButton('schema-initializer-Menu-header') }); + } + + return result; +} + +function isGroup(groupId: string, allAccessRoutes: NocoBaseDesktopRoute[]) { + const route = findRouteById(groupId, allAccessRoutes); + return route?.type === NocoBaseDesktopRouteType.group; +} + +function findRouteById(id: string, treeArray: any[]) { + for (const node of treeArray) { + if (Number(id) === Number(node.id)) { + return node; + } + + if (node.children?.length) { + const result = findRouteById(id, node.children); + if (result) { + return result; + } + } + } + return null; +} + +function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) { + if (!routes) return; + + for (const route of routes) { + if (route.type === NocoBaseDesktopRouteType.page) { + return route; + } + + if (route.children?.length) { + return findFirstPageRoute(route.children); + } + } +} diff --git a/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx b/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx new file mode 100644 index 0000000000..92855cdb0d --- /dev/null +++ b/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx @@ -0,0 +1,622 @@ +/** + * 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, + 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 ( + { + 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')} + + ); +}; + +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 ( + + { + 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], + }); + } + }} + /> + + { + 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 })); + }} + /> + { + 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], + }); + } + }} + /> + + ); +}; + +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 ( + + ); +}; + +const HiddenMenuItem = () => { + const { t } = useTranslation(); + const currentRoute = useCurrentRouteData(); + const { updateRoute } = useNocoBaseRoutes(); + const { modal } = App.useApp(); + + return ( + { + modal.confirm({ + title: t('Are you sure you want to hide this menu?'), + icon: , + 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 ( + + ); +}; + +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 ( + + ); + }, + }, + { + name: 'insertafterEnd', + Component: () => { + const { t } = useTranslation(); + return ; + }, + }, + { + name: 'insertbeforeEnd', + Component: () => { + const { t } = useTranslation(); + return ; + }, + }, + { + 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 ( + + {/* 避免 Sider 的状态影响到 SchemaToolbar。否则会导致在折叠状态下,SchemaToolbar 的样式异常 */} + + + + + ); +}; + +/** + * 重置主题,避免被 ProLayout 的主题影响 + * @param props + * @returns + */ +export const ResetThemeTokenAndKeepAlgorithm: FC = (props) => { + const { theme } = useToken() as any; + + return ( + + {props.children} + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx index 0566eef305..49d5eaaf60 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx @@ -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 = observer( useSetAriaLabelForDrawer(visible); } - const zIndex = _zIndex || parentZIndex + (props.level || 0); + const zIndex = getZIndex('drawer', _zIndex || parentZIndex, props.level || 0); const onClose = useCallback( (e) => { diff --git a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx index 1be3c4a941..09562cb54d 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx @@ -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 = (props) => { const { visible, setVisible } = useActionContext(); @@ -89,7 +89,7 @@ export const InternalActionModal: React.FC> = obse useSetAriaLabelForModal(visible); } - const zIndex = _zIndex || parentZIndex + (props.level || 0); + const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0); return ( diff --git a/packages/core/client/src/schema-component/antd/action/Action.Page.style.ts b/packages/core/client/src/schema-component/antd/action/Action.Page.style.ts index 61c3ba9636..198c7c3df7 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Page.style.ts +++ b/packages/core/client/src/schema-component/antd/action/Action.Page.style.ts @@ -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, diff --git a/packages/core/client/src/schema-component/antd/action/Action.Page.tsx b/packages/core/client/src/schema-component/antd/action/Action.Page.tsx index 3853779f40..5e04ec5d3b 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Page.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Page.tsx @@ -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]); diff --git a/packages/core/client/src/schema-component/antd/action/zIndexContext.tsx b/packages/core/client/src/schema-component/antd/action/zIndexContext.tsx index 76721e13a3..b76f67a63c 100644 --- a/packages/core/client/src/schema-component/antd/action/zIndexContext.tsx +++ b/packages/core/client/src/schema-component/antd/action/zIndexContext.tsx @@ -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; +}; diff --git a/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx b/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx index 48aac3d858..d6ba0d0371 100644 --- a/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx @@ -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 })); }} /> { 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, - }); }} /> @@ -321,14 +286,14 @@ export const MenuDesigner = () => { f.dataSource = component === 'Menu.SubMenu' ? [ - { 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' }, + { label: t('Inner'), value: 'beforeEnd' }, + ] : [ - { label: t('Before'), value: 'beforeBegin' }, - { label: t('After'), value: 'afterEnd' }, - ]; + { label: t('Before'), value: 'beforeBegin' }, + { label: t('After'), value: 'afterEnd' }, + ]; }); }); }, @@ -404,9 +369,9 @@ export const MenuDesigner = () => { options: href || params ? { - href, - params, - } + href, + params, + } : undefined, }); } diff --git a/packages/core/client/src/schema-component/antd/menu/Menu.tsx b/packages/core/client/src/schema-component/antd/menu/Menu.tsx index 203772d8fe..71a231b472 100644 --- a/packages/core/client/src/schema-component/antd/menu/Menu.tsx +++ b/packages/core/client/src/schema-component/antd/menu/Menu.tsx @@ -203,7 +203,7 @@ type ComposedMenu = React.FC & { Designer?: React.FC; }; -const ParentRouteContext = createContext(null); +export const ParentRouteContext = createContext(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; diff --git a/packages/core/client/src/schema-component/antd/page/Page.Settings.tsx b/packages/core/client/src/schema-component/antd/page/Page.Settings.tsx index e1ba94737d..4d23b3b370 100644 --- a/packages/core/client/src/schema-component/antd/page/Page.Settings.tsx +++ b/packages/core/client/src/schema-component/antd/page/Page.Settings.tsx @@ -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'], + }, + }); }, }; }, diff --git a/packages/core/client/src/schema-component/antd/page/Page.tsx b/packages/core/client/src/schema-component/antd/page/Page.tsx index b5f164ba53..d402ab725e 100644 --- a/packages/core/client/src/schema-component/antd/page/Page.tsx +++ b/packages/core/client/src/schema-component/antd/page/Page.tsx @@ -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(); + const { loading, disablePageHeader, enablePageTabs, tabUid } = useOutletContext(); return ( {/* 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 (
- +
); @@ -205,26 +196,48 @@ interface PageContentProps { loading: boolean; disablePageHeader: any; enablePageTabs: any; - fieldSchema: Schema; activeKey: string; } const InternalPageContent = (props: PageContentProps) => { - const { loading, disablePageHeader, enablePageTabs, fieldSchema, activeKey } = props; + const { loading, disablePageHeader, enablePageTabs, activeKey } = props; + const currentRoute = useCurrentRouteData(); + const location = useLocationNoUpdate(); + const navigate = useNavigateNoUpdate(); + + const children = currentRoute?.children || []; + const noTabs = children.every((tabRoute) => tabRoute.schemaUid !== activeKey && tabRoute.tabSchemaName !== activeKey); + + if (activeKey && noTabs) { + return ; + } + + // 兼容旧版本的 tab 路径 + const oldTab = currentRoute?.children?.find((tabRoute) => tabRoute.tabSchemaName === activeKey); + if (oldTab) { + navigate(`/admin/${currentRoute.schemaUid}/tabs/${oldTab.schemaUid}`); + return null; + } if (!disablePageHeader && enablePageTabs) { return ( <> - {fieldSchema.mapProperties((schema) => ( - - ))} + {currentRoute.children?.map((tabRoute) => { + return ( + + + + ); + })} ); } return (
- + + +
); }; @@ -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: ( - - {schema['x-icon'] && } - {(tabRoute.title && routeT(compile(tabRoute.title))) || t('Unnamed')} - - + + + {tabRoute.icon && } + {(tabRoute.title && routeT(compile(tabRoute.title))) || t('Unnamed')} + + + ), - 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 && ( (); - 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,33 +70,21 @@ 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: , - 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, { - hideInMenu: !!v, - }); - } - - dn.emit('patch', { - schema: { - 'x-uid': schema['x-uid'], - 'x-component-props': schema['x-component-props'], - }, + // Update the route corresponding to the menu + await updateRoute(currentRoute.id, { + hideInMenu: !!v, }); }, }); @@ -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']}`); + } }, }); }, diff --git a/packages/core/client/src/schema-component/antd/page/PageTabDesigner.tsx b/packages/core/client/src/schema-component/antd/page/PageTabDesigner.tsx index ba41fbc048..58d19d5fdd 100644 --- a/packages/core/client/src/schema-component/antd/page/PageTabDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/page/PageTabDesigner.tsx @@ -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,29 +30,25 @@ 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 ( - -
-
- - - - - {render()} - -
+
+
+ + + + + {render()} +
- +
); }; diff --git a/packages/core/client/src/schema-component/antd/page/__tests__/page.test.tsx b/packages/core/client/src/schema-component/antd/page/__tests__/page.test.tsx index 01654a8784..05fecbda1a 100644 --- a/packages/core/client/src/schema-component/antd/page/__tests__/page.test.tsx +++ b/packages/core/client/src/schema-component/antd/page/__tests__/page.test.tsx @@ -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(); - - 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'; diff --git a/packages/core/client/src/schema-component/antd/page/demos/demo1.tsx b/packages/core/client/src/schema-component/antd/page/demos/demo1.tsx deleted file mode 100644 index e947ddbc50..0000000000 --- a/packages/core/client/src/schema-component/antd/page/demos/demo1.tsx +++ /dev/null @@ -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 ( - - - - - - ); -}; - -const app = new Application({ - providers: [Root], -}); - -export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-component/core/RemoteSchemaComponent.tsx b/packages/core/client/src/schema-component/core/RemoteSchemaComponent.tsx index b42eac095c..356705c31b 100644 --- a/packages/core/client/src/schema-component/core/RemoteSchemaComponent.tsx +++ b/packages/core/client/src/schema-component/core/RemoteSchemaComponent.tsx @@ -84,4 +84,5 @@ const RequestSchemaComponent: React.FC = (props) => export const RemoteSchemaComponent: React.FC = memo((props) => { return props.uid ? : null; }); + RemoteSchemaComponent.displayName = 'RemoteSchemaComponent'; diff --git a/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx b/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx index 3f41bf55ec..7ba10563d5 100644 --- a/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx +++ b/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx @@ -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 = React.memo((props) => { spaceStyle, toolbarClassName, toolbarStyle = {}, + container, } = { ...props, ...(fieldSchema?.['x-toolbar-props'] || {}), @@ -312,7 +318,10 @@ const InternalSchemaToolbar: FC = 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 = 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( () => ({ diff --git a/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts b/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts index bcb54d9e51..a0cdc9d51d 100644 --- a/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts +++ b/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts @@ -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; diff --git a/packages/core/test/src/e2e/e2eUtils.ts b/packages/core/test/src/e2e/e2eUtils.ts index f3a46dd8ce..0f2a11c16d 100644 --- a/packages/core/test/src/e2e/e2eUtils.ts +++ b/packages/core/test/src/e2e/e2eUtils.ts @@ -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,12 +1053,14 @@ const deletePage = async (pageUid: string, routeId: number) => { } } - const result = await api.post(`/api/uiSchemas:remove/${pageUid}`, { - headers, - }); + if (pageUid) { + const result = await api.post(`/api/uiSchemas:remove/${pageUid}`, { + headers, + }); - if (!result.ok()) { - throw new Error(await result.text()); + if (!result.ok()) { + throw new Error(await result.text()); + } } }; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/configure.test.ts b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/configure.test.ts index b4fe1bf717..e204788c54 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/configure.test.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/configure.test.ts @@ -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(); }); diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/MenuPermissions.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/MenuPermissions.tsx index 2a1fdecd0d..2aebfaf4b6 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/MenuPermissions.tsx +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/MenuPermissions.tsx @@ -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')); }} />{' '} diff --git a/packages/plugins/@nocobase/plugin-action-print/src/client/__e2e__/utils.ts b/packages/plugins/@nocobase/plugin-action-print/src/client/__e2e__/utils.ts index 27faa3662d..0eced99eeb 100644 --- a/packages/plugins/@nocobase/plugin-action-print/src/client/__e2e__/utils.ts +++ b/packages/plugins/@nocobase/plugin-action-print/src/client/__e2e__/utils.ts @@ -382,8 +382,11 @@ export const oneTableWithViewAction: PageConfig = { 'x-index': 1, }, }, + 'x-uid': 'j0k2m5r9z3b', + 'x-async': false, }, }, + 'x-uid': 'l6ioayfnq6c', }, }; diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx index f012dbc4d5..653ffc6219 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx @@ -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, }); diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx index f6ed96abb9..7ad775475f 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx @@ -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'; @@ -70,9 +70,9 @@ export class PluginCalendarClient extends Plugin { colorFieldInterfaces: { [T: string]: { useGetColor: (field: any) => ColorFunctions }; } = { - select: { useGetColor }, - radioGroup: { useGetColor }, - }; + select: { useGetColor }, + radioGroup: { useGetColor }, + }; dateTimeFieldInterfaces = ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp', 'createdAt', 'updatedAt']; diff --git a/packages/plugins/@nocobase/plugin-client/src/client/routesTableSchema.tsx b/packages/plugins/@nocobase/plugin-client/src/client/routesTableSchema.tsx index 6c2ed43808..c261329184 100644 --- a/packages/plugins/@nocobase/plugin-client/src/client/routesTableSchema.tsx +++ b/packages/plugins/@nocobase/plugin-client/src/client/routesTableSchema.tsx @@ -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, @@ -237,13 +236,13 @@ export const createRoutesTableSchema = (collectionName: string, basename: string 'x-component': 'IconPicker', 'x-reactions': isMobile ? { - dependencies: ['type'], - fulfill: { - state: { - required: '{{$deps[0] !== "tabs"}}', - }, + dependencies: ['type'], + fulfill: { + state: { + required: '{{$deps[0] !== "tabs"}}', }, - } + }, + } : undefined, }, // 由于历史原因,桌面端使用的是 'href' 作为 key,移动端使用的是 'url' 作为 key @@ -575,9 +574,8 @@ 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; @@ -697,13 +695,13 @@ export const createRoutesTableSchema = (collectionName: string, basename: string 'x-component': 'IconPicker', 'x-reactions': isMobile ? { - dependencies: ['type'], - fulfill: { - state: { - required: '{{$deps[0] !== "tabs"}}', - }, + dependencies: ['type'], + fulfill: { + state: { + required: '{{$deps[0] !== "tabs"}}', }, - } + }, + } : undefined, }, // 由于历史原因,桌面端使用的是 'href' 作为 key,移动端使用的是 'url' 作为 key @@ -986,13 +984,13 @@ export const createRoutesTableSchema = (collectionName: string, basename: string 'x-component': 'IconPicker', 'x-reactions': isMobile ? { - dependencies: ['type'], - fulfill: { - state: { - required: '{{$deps[0] !== "tabs"}}', - }, + dependencies: ['type'], + fulfill: { + state: { + required: '{{$deps[0] !== "tabs"}}', }, - } + }, + } : undefined, }, // 由于历史原因,桌面端使用的是 'href' 作为 key,移动端使用的是 'url' 作为 key @@ -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; }) => { - 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 }), + pageSchemaUid, + tabSchemaUid, + tabSchemaName, + }), }; + 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], ); /** diff --git a/packages/plugins/@nocobase/plugin-client/src/server/server.ts b/packages/plugins/@nocobase/plugin-client/src/server/server.ts index c114001b5a..eb6b8ac748 100644 --- a/packages/plugins/@nocobase/plugin-client/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-client/src/server/server.ts @@ -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'); @@ -33,7 +33,7 @@ async function getLang(ctx) { } export class PluginClientServer extends Plugin { - async beforeLoad() {} + async beforeLoad() { } async install() { const uiSchemas = this.db.getRepository('uiSchemas'); @@ -218,8 +218,6 @@ export class PluginClientServer extends Plugin { const desktopRoutesId = role .get('desktopRoutes') - // hidden 为 true 的节点不会显示在权限配置表格中,所以无法被配置,需要被过滤掉 - .filter((item) => !item.hidden) .map((item) => item.id); ctx.body = await desktopRoutesRepository.find({ diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/header/navigation-bar/actions/mobile-navigation-bar-action/styles.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/header/navigation-bar/actions/mobile-navigation-bar-action/styles.ts index a506df1fad..06751439b7 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/header/navigation-bar/actions/mobile-navigation-bar-action/styles.ts +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/header/navigation-bar/actions/mobile-navigation-bar-action/styles.ts @@ -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', - }, }, }, }; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/header/tabs/settings.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/header/tabs/settings.tsx index 9f6fa411f6..299fcf3aeb 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/header/tabs/settings.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/pages/dynamic-page/header/tabs/settings.tsx @@ -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 = ({ tab }) settings={mobilePageTabsSettings} showBackground showBorder={false} - toolbarStyle={{ inset: '-15px -12px' }} + toolbarStyle={{ inset: '0 -12px' }} spaceWrapperStyle={{ top: 3 }} /> diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx index b51e3afab7..2fb72f3640 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx @@ -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'; +