diff --git a/web/src/api/workspaces.ts b/web/src/api/workspaces.ts index 5c62489d..ee394abc 100644 --- a/web/src/api/workspaces.ts +++ b/web/src/api/workspaces.ts @@ -9,8 +9,9 @@ import type { SpaceModalData } from '@/views/SpaceManagement/types' import type { SpaceConfigData } from '@/views/SpaceConfig/types' // Workspace list +export const getWorkspacesUrl = '/workspaces' export const getWorkspaces = (data?: { include_current?: boolean }) => { - return request.get('/workspaces', data) + return request.get(getWorkspacesUrl, data) } // Create workspace export const createWorkspace = (values: SpaceModalData) => { diff --git a/web/src/assets/images/menuNew/return.svg b/web/src/assets/images/menuNew/return.svg new file mode 100644 index 00000000..7fb038dd --- /dev/null +++ b/web/src/assets/images/menuNew/return.svg @@ -0,0 +1,19 @@ + + + 退出 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/menuNew/switch.svg b/web/src/assets/images/menuNew/switch.svg new file mode 100644 index 00000000..8adfd3ee --- /dev/null +++ b/web/src/assets/images/menuNew/switch.svg @@ -0,0 +1,18 @@ + + + 切换 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/SiderMenu/SwitchSpaceModal.tsx b/web/src/components/SiderMenu/SwitchSpaceModal.tsx new file mode 100644 index 00000000..e5483f63 --- /dev/null +++ b/web/src/components/SiderMenu/SwitchSpaceModal.tsx @@ -0,0 +1,114 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-04-22 18:50:14 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-22 18:50:14 + */ +/** + * SwitchSpaceModal Component + * + * A modal for switching the current workspace. + * Displays a dropdown to select a workspace and reloads the page upon confirmation. + */ + +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Form, App, Space } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import RbModal from '@/components/RbModal' +import { switchWorkspace, getWorkspacesUrl } from '@/api/workspaces' +import CustomSelect from '@/components/CustomSelect'; +import Tag from '@/components/Tag' +import { useUser } from '@/store/user'; + +const FormItem = Form.Item; + +export interface SwitchSpaceModalRef { + handleOpen: () => void; +} + +const SwitchSpaceModal = forwardRef((_props, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm<{ space_id: string }>(); + const [loading, setLoading] = useState(false) + const { user } = useUser() + + /** Close modal and reset form */ + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false) + }; + + /** Open modal */ + const handleOpen = () => { + form.resetFields(); + setVisible(true); + form.setFieldsValue({ space_id: user?.current_workspace_id }) + }; + /** Handle save/next button click - proceed to next step or submit email change */ + const handleSave = () => { + form + .validateFields() + .then((values) => { + if (user?.current_workspace_id === values.space_id) { + handleClose() + return + } + setLoading(true) + switchWorkspace(values.space_id) + .then(res => { + if (res) { + message.success(t('common.operateSuccess')); + localStorage.removeItem('user') + window.location.reload() + } + }) + .finally(() => setLoading(false)) + }) + .catch((err) => { + console.log('err', err) + }); + } + + /** Expose methods to parent component */ + useImperativeHandle(ref, () => ({ + handleOpen, + })); + + return ( + +
+ + list.map(item => ({ + value: item.id, + label: {item.name}{t(`space.${item.storage_type || 'neo4j'}`)} + }))} + /> + +
+
+ ); +}); + +export default SwitchSpaceModal; \ No newline at end of file diff --git a/web/src/components/SiderMenu/index.module.css b/web/src/components/SiderMenu/index.module.css index 9cc61665..e07f4c9c 100644 --- a/web/src/components/SiderMenu/index.module.css +++ b/web/src/components/SiderMenu/index.module.css @@ -2,6 +2,10 @@ /* border-right: 1px solid #EAECEE; */ max-height: 100vh; } +.sider :global(.ant-layout-sider-children) { + display: flex; + flex-direction: column; +} .title { height: 64px; padding: 24px 10px 12px 12px; diff --git a/web/src/components/SiderMenu/index.tsx b/web/src/components/SiderMenu/index.tsx index 0363ce45..4acc8b46 100644 --- a/web/src/components/SiderMenu/index.tsx +++ b/web/src/components/SiderMenu/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:25:31 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-16 17:35:38 + * @Last Modified time: 2026-04-21 17:56:09 */ /** * SiderMenu Component @@ -19,7 +19,7 @@ */ import { useState, useEffect, useRef, type FC } from 'react'; -import { Menu as AntMenu, Layout, Flex } from 'antd'; +import { Menu as AntMenu, Layout, Flex, Divider } from 'antd'; import { UserOutlined } from '@ant-design/icons'; import type { MenuProps } from 'antd'; import { useNavigate, useLocation } from 'react-router-dom'; @@ -32,7 +32,8 @@ import logo from '@/assets/images/logo.png' import { useUser } from '@/store/user'; import { getTenantSubscription } from '@/api/user'; import { useI18n } from '@/store/locale' -import SubscriptionDetailModal, { type SubscriptionDetailModalRef } from './SubscriptionDetailModal' +import SubscriptionDetailModal, { type SubscriptionDetailModalRef } from './SubscriptionDetailModal'; +import SwitchSpaceModal, { type SwitchSpaceModalRef } from './SwitchSpaceModal'; // Import SVG files // space @@ -174,6 +175,7 @@ const Menu: FC<{ const [menus, setMenus] = useState([]) const { user, storageType } = useUser() const subscriptionDetailRef = useRef(null) + const switchSpaceModalRef = useRef(null) /** Filter menus based on user role and source */ useEffect(() => { @@ -346,6 +348,9 @@ const Menu: FC<{ const handleViewDetail = () => { subscriptionDetailRef.current?.handleOpen(subscription) } + const handleSwitchSpace = () => { + switchSpaceModalRef.current?.handleOpen() + } return ( {/* Return to space button for superusers */} {user?.is_superuser && source === 'space' && - -
- {collapsed ? null : t('common.returnToSpace')} + + + +
+ {collapsed ? null : t('common.switchSpace')} +
+ +
+ {collapsed ? null : t('common.returnToSpace')} +
} {source === 'manage' && subscription && !collapsed && -
+
{subscription.package_plan?.[getKeyWithLanguage('name')]}
@@ -434,6 +448,9 @@ const Menu: FC<{ + ); }; diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 032b63db..04412d5f 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -474,6 +474,7 @@ export const en = { view: 'View', updated_at: 'Updated At', callbackUrlInvalid: 'Please enter a valid URL', + switchSpace: 'Switch Space', }, model: { searchPlaceholder: 'search model…', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 505d9a74..aa0f08b7 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1153,6 +1153,7 @@ export const zh = { view: '查看', updated_at: '更新时间', callbackUrlInvalid: '请输入有效的 URL', + switchSpace: '切换空间', }, model: { searchPlaceholder: '搜索模型…',