From d12e35674e859dc80deb75873f1564fa2f26429c Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Wed, 6 Nov 2024 11:42:19 +0800 Subject: [PATCH 01/34] feat: multiple tabs --- .../src/ui/components/document-tab.tsx | 35 +++ .../insomnia/src/ui/components/tabs/tab.tsx | 132 ++++++++ .../src/ui/components/tabs/tabList.tsx | 68 ++++ .../src/ui/components/tags/method-tag.tsx | 23 +- .../ui/context/app/insomnia-tab-context.tsx | 136 ++++++++ packages/insomnia/src/ui/hooks/tab.ts | 292 ++++++++++++++++++ packages/insomnia/src/ui/routes/debug.tsx | 38 ++- packages/insomnia/src/ui/routes/design.tsx | 48 ++- .../insomnia/src/ui/routes/environments.tsx | 19 +- .../insomnia/src/ui/routes/mock-server.tsx | 26 +- .../insomnia/src/ui/routes/organization.tsx | 32 +- packages/insomnia/src/ui/routes/project.tsx | 9 +- packages/insomnia/src/ui/routes/unit-test.tsx | 28 ++ 13 files changed, 835 insertions(+), 51 deletions(-) create mode 100644 packages/insomnia/src/ui/components/document-tab.tsx create mode 100644 packages/insomnia/src/ui/components/tabs/tab.tsx create mode 100644 packages/insomnia/src/ui/components/tabs/tabList.tsx create mode 100644 packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx create mode 100644 packages/insomnia/src/ui/hooks/tab.ts diff --git a/packages/insomnia/src/ui/components/document-tab.tsx b/packages/insomnia/src/ui/components/document-tab.tsx new file mode 100644 index 00000000000..e9e5711e2b7 --- /dev/null +++ b/packages/insomnia/src/ui/components/document-tab.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; + +interface Props { + organizationId: string; + projectId: string; + workspaceId: string; + className?: string; +} + +export const DocumentTab = ({ organizationId, projectId, workspaceId, className }: Props) => { + return ( + + ); +}; diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx new file mode 100644 index 00000000000..86144e5a64a --- /dev/null +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { Button, GridListItem } from 'react-aria-components'; + +import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; +import { Icon } from '../icon'; +import { Tooltip } from '../tooltip'; + +export const enum TabEnum { + Request = 'request', + Folder = 'folder', + Env = 'environment', + Mock = 'mock-server', + MockRoute = 'mock-route', + Document = 'document', + Collection = 'collection', + Runner = 'runner', + TEST = 'test', + TESTSUITE = 'test-suite', +}; + +export interface BaseTab { + type: TabEnum; + name: string; + url: string; + organizationId: string; + projectId: string; + workspaceId: string; + organizationName: string; + projectName: string; + workspaceName: string; + id: string; + [key: string]: string; +}; + +const REQUEST_TAG_MAP: Record = { + 'GET': 'text-[--color-font-surprise] bg-[rgba(var(--color-surprise-rgb),0.5)]', + 'POST': 'text-[--color-font-success] bg-[rgba(var(--color-success-rgb),0.5)]', + 'GQL': 'text-[--color-font-success] bg-[rgba(var(--color-success-rgb),0.5)]', + 'HEAD': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]', + 'OPTIONS': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]', + 'DELETE': 'text-[--color-font-danger] bg-[rgba(var(--color-danger-rgb),0.5)]', + 'PUT': 'text-[--color-font-warning] bg-[rgba(var(--color-warning-rgb),0.5)]', + 'PATCH': 'text-[--color-font-notice] bg-[rgba(var(--color-notice-rgb),0.5)]', + 'WS': 'text-[--color-font-notice] bg-[rgba(var(--color-notice-rgb),0.5)]', + 'gRPC': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]', +}; + +const WORKSPACE_TAB_UI_MAP: Record = { + [TabEnum.Collection]: { + icon: 'bars', + bgColor: 'bg-[--color-surprise]', + textColor: 'text-[--color-font-surprise]', + }, + [TabEnum.Env]: { + icon: 'code', + bgColor: 'bg-[--color-font]', + textColor: 'text-[--color-bg]', + }, + [TabEnum.Mock]: { + icon: 'server', + bgColor: 'bg-[--color-warning]', + textColor: 'text-[--color-font-warning]', + }, + [TabEnum.Document]: { + icon: 'file', + bgColor: 'bg-[--color-info]', + textColor: 'text-[--color-font-info]', + }, +}; + +export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { + + const { deleteTabById } = useInsomniaTabContext(); + + const renderTabIcon = (type: TabEnum) => { + if (WORKSPACE_TAB_UI_MAP[type]) { + return ( +
+ +
+ ); + } + + if (type === TabEnum.Request || type === TabEnum.MockRoute) { + return ( + {tab.tag} + ); + } + + if (type === TabEnum.Folder) { + return ; + } + if (type === TabEnum.Runner) { + return ; + }; + + if (type === TabEnum.TESTSUITE) { + return ; + } + + return null; + }; + + const handleClose = (id: string) => { + deleteTabById(id); + }; + + return ( + + {({ isSelected, isHovered }) => ( + +
+ {renderTabIcon(tab.type)} + {tab.name} +
+ {isHovered && ( + + )} +
+ {isSelected && } +
+
+ )} +
+ ); +}; diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx new file mode 100644 index 00000000000..29e22f323f9 --- /dev/null +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { GridList, type Key, type Selection } from 'react-aria-components'; +import { useNavigate } from 'react-router-dom'; + +import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; +import { Icon } from '../icon'; +import { type BaseTab, InsomniaTab, TabEnum } from './tab'; + +export interface OrganizationTabs { + tabList: BaseTab[]; + activeTabId?: Key | null; +} + +export const TAB_ROUTER_PATH: Record = { + [TabEnum.Collection]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug', + [TabEnum.Folder]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request-group/:requestGroupId', + [TabEnum.Request]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId', + [TabEnum.Env]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/environment', + [TabEnum.Mock]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server', + [TabEnum.Runner]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/runner', + [TabEnum.Document]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/spec', + [TabEnum.MockRoute]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server/mock-route/:mockRouteId', + [TabEnum.TEST]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test', + [TabEnum.TESTSUITE]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/*', +}; + +export const OrganizationTabList = ({ showActiveStatus = true }) => { + const { currentOrgTabs } = useInsomniaTabContext(); + const { tabList, activeTabId } = currentOrgTabs; + console.log('activeTabId', activeTabId); + const navigate = useNavigate(); + + const { changeActiveTab } = useInsomniaTabContext(); + + const handleSelectionChange = (keys: Selection) => { + console.log('changeActiveTab'); + if (keys !== 'all') { + console.log('tab change', keys); + const key = [...keys.values()]?.[0] as string; + const tab = tabList.find(tab => tab.id === key); + tab?.url && navigate(tab?.url); + changeActiveTab(key); + } + }; + + if (!tabList.length) { + return null; + }; + + return ( +
+ + {item => } + + +
+ ); +}; diff --git a/packages/insomnia/src/ui/components/tags/method-tag.tsx b/packages/insomnia/src/ui/components/tags/method-tag.tsx index 34e921fa5dd..51983a8f20a 100644 --- a/packages/insomnia/src/ui/components/tags/method-tag.tsx +++ b/packages/insomnia/src/ui/components/tags/method-tag.tsx @@ -1,7 +1,9 @@ import React, { type FC, memo } from 'react'; import { CONTENT_TYPE_GRAPHQL, METHOD_DELETE, METHOD_OPTIONS } from '../../../common/constants'; -import { isEventStreamRequest, type Request } from '../../../models/request'; +import { type GrpcRequest, isGrpcRequest } from '../../../models/grpc-request'; +import { isEventStreamRequest, isRequest, type Request } from '../../../models/request'; +import { isWebSocketRequest, type WebSocketRequest } from '../../../models/websocket-request'; interface Props { method: string; @@ -34,6 +36,25 @@ export function formatMethodName(method: string) { return methodName; } +export const getRequestMethodShortHand = (doc?: Request | WebSocketRequest | GrpcRequest) => { + if (!doc) { + return ''; + } + if (isRequest(doc)) { + return getMethodShortHand(doc); + } + + if (isWebSocketRequest(doc)) { + return 'WS'; + } + + if (isGrpcRequest(doc)) { + return 'gRPC'; + } + + return ''; +}; + export const MethodTag: FC = memo(({ method, override, fullNames }) => { let methodName = method; let overrideName = override; diff --git a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx new file mode 100644 index 00000000000..7db50910bab --- /dev/null +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -0,0 +1,136 @@ +import React, { createContext, type FC, type PropsWithChildren, useCallback, useContext, useEffect, useRef } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useLocalStorage } from 'react-use'; + +import type { BaseTab } from '../../components/tabs/tab'; +import type { OrganizationTabs } from '../../components/tabs/tabList'; + +interface UpdateInsomniaTabParams { + organizationId: string; + tabList: OrganizationTabs['tabList']; + activeTabId?: string; +} + +interface ContextProps { + currentOrgTabs: OrganizationTabs; + appTabsRef?: React.MutableRefObject; + deleteTabById: (id: string) => void; + addTab: (tab: BaseTab) => void; + changeActiveTab: (id: string) => void; +} + +const InsomniaTabContext = createContext({ + currentOrgTabs: { + tabList: [], + activeTabId: '', + }, + deleteTabById: () => { }, + addTab: () => { }, + changeActiveTab: () => { }, +}); + +interface InsomniaTabs { + [orgId: string]: OrganizationTabs; +}; + +export const InsomniaTabProvider: FC = ({ children }) => { + const { + organizationId, + projectId, + } = useParams() as { + organizationId: string; + projectId: string; + workspaceId: string; + }; + + const [appTabs, setAppTabs] = useLocalStorage('insomnia-tabs', {}); + + // keep a ref of the appTabs to avoid the function recreated, which will cause the useEffect to run again and cannot delete a tab + // file: packages/insomnia/src/ui/hooks/tab.ts + const appTabsRef = useRef(appTabs); + + const navigate = useNavigate(); + + const updateInsomniaTabs = useCallback(({ organizationId, tabList, activeTabId }: UpdateInsomniaTabParams) => { + const newState = { + ...appTabsRef.current, + [organizationId]: { + tabList, + activeTabId, + }, + }; + appTabsRef.current = newState; + setAppTabs(newState); + }, [setAppTabs]); + + const addTab = useCallback((tab: BaseTab) => { + const currentTabs = appTabsRef?.current?.[organizationId] || { tabList: [], activeTabId: '' }; + + updateInsomniaTabs({ + organizationId, + tabList: [...currentTabs.tabList, tab], + activeTabId: tab.id, + }); + }, [organizationId, updateInsomniaTabs]); + + useEffect(() => { + console.log('addTab change'); + }, [addTab]); + + const deleteTabById = useCallback((id: string) => { + const currentTabs = appTabs?.[organizationId]; + if (!currentTabs) { + return; + } + + // If the tab being deleted is the only tab and is active, navigate to the project dashboard + if (currentTabs.activeTabId === id && currentTabs.tabList.length === 1) { + navigate(`/organization/${organizationId}/project/${projectId}`); + updateInsomniaTabs({ + organizationId, + tabList: [], + activeTabId: '', + }); + return; + } + + const index = currentTabs.tabList.findIndex(tab => tab.id === id); + const newTabList = currentTabs.tabList.filter(tab => tab.id !== id); + if (currentTabs.activeTabId === id) { + navigate(newTabList[index - 1 < 0 ? 0 : index - 1]?.url || ''); + } + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: currentTabs.activeTabId === id ? newTabList[index - 1 < 0 ? 0 : index - 1]?.id : currentTabs.activeTabId as string, + }); + }, [appTabs, navigate, organizationId, projectId, updateInsomniaTabs]); + + const changeActiveTab = useCallback((id: string) => { + const currentTabs = appTabsRef?.current?.[organizationId] || { tabList: [], activeTabId: '' }; + if (!currentTabs) { + return; + } + updateInsomniaTabs({ + organizationId, + tabList: currentTabs.tabList, + activeTabId: id, + }); + }, [organizationId, updateInsomniaTabs]); + + return ( + + {children} + + ); +}; + +export const useInsomniaTabContext = () => useContext(InsomniaTabContext); diff --git a/packages/insomnia/src/ui/hooks/tab.ts b/packages/insomnia/src/ui/hooks/tab.ts new file mode 100644 index 00000000000..1874ebbe1c0 --- /dev/null +++ b/packages/insomnia/src/ui/hooks/tab.ts @@ -0,0 +1,292 @@ +import { useCallback, useEffect } from 'react'; +import { matchPath, useLocation } from 'react-router-dom'; + +import type { GrpcRequest } from '../../models/grpc-request'; +import type { MockRoute } from '../../models/mock-route'; +import type { Organization } from '../../models/organization'; +import type { Project } from '../../models/project'; +import type { Request } from '../../models/request'; +import type { RequestGroup } from '../../models/request-group'; +import type { UnitTestSuite } from '../../models/unit-test-suite'; +import type { WebSocketRequest } from '../../models/websocket-request'; +import type { Workspace } from '../../models/workspace'; +import { type BaseTab, TabEnum } from '../components/tabs/tab'; +import { TAB_ROUTER_PATH } from '../components/tabs/tabList'; +import { formatMethodName, getRequestMethodShortHand } from '../components/tags/method-tag'; +import { useInsomniaTabContext } from '../context/app/insomnia-tab-context'; + +interface InsomniaTabProps { + organizationId: string; + projectId: string; + workspaceId: string; + activeProject: Project; + activeWorkspace: Workspace; + activeRequest?: Request | GrpcRequest | WebSocketRequest; + activeRequestGroup?: RequestGroup; + activeOrganization?: Organization; + activeMockRoute?: MockRoute; + unitTestSuite?: UnitTestSuite; +} + +export const useInsomniaTab = ({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeRequest, + activeRequestGroup, + activeOrganization, + activeMockRoute, + unitTestSuite, +}: InsomniaTabProps) => { + + console.log(activeMockRoute, 'activeMockRoute'); + const { appTabsRef, addTab, changeActiveTab } = useInsomniaTabContext(); + + const generateTabUrl = useCallback((type: TabEnum) => { + if (type === TabEnum.Request) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${activeRequest?._id}`; + } + + if (type === TabEnum.Folder) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/${activeRequestGroup?._id}`; + } + + if (type === TabEnum.Collection) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`; + } + + if (type === TabEnum.Env) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment`; + } + + if (type === TabEnum.Runner) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner`; + } + + if (type === TabEnum.Mock) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server`; + } + + if (type === TabEnum.MockRoute) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${activeMockRoute?._id}`; + } + + if (type === TabEnum.Document) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/spec`; + } + + if (type === TabEnum.TEST) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test`; + } + + if (type === TabEnum.TESTSUITE) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite?._id}`; + } + return ''; + }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, organizationId, projectId, unitTestSuite?._id, workspaceId]); + + const location = useLocation(); + + const getTabType = (pathname: string) => { + console.log(pathname); + for (const type in TAB_ROUTER_PATH) { + const ifMatch = matchPath({ + path: TAB_ROUTER_PATH[type as TabEnum], + end: true, + }, pathname); + if (ifMatch) { + return type as TabEnum; + } + } + + return null; + }; + + const getCurrentTab = useCallback((type: TabEnum | null) => { + if (!type) { + return undefined; + } + const currentOrgTabs = appTabsRef?.current?.[organizationId]; + if (type === TabEnum.Request) { + return currentOrgTabs?.tabList.find(tab => tab.id === activeRequest?._id); + } + + if (type === TabEnum.Folder) { + return currentOrgTabs?.tabList.find(tab => tab.id === activeRequestGroup?._id); + } + + if (type === TabEnum.Runner) { + // collection runner tab id is prefixed with 'runner_' + return currentOrgTabs?.tabList.find(tab => tab.id === `runner_${workspaceId}`); + } + + if (type === TabEnum.MockRoute) { + return currentOrgTabs?.tabList.find(tab => tab.id === activeMockRoute?._id); + } + + if (type === TabEnum.TESTSUITE) { + return currentOrgTabs?.tabList.find(tab => tab.id === unitTestSuite?._id); + } + + if ([TabEnum.Collection, TabEnum.Document, TabEnum.Env, TabEnum.Mock, TabEnum.TEST].includes(type)) { + return currentOrgTabs?.tabList.find(tab => tab.id === workspaceId); + } + return undefined; + }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, appTabsRef, organizationId, unitTestSuite?._id, workspaceId]); + + const getTabId = useCallback((type: TabEnum | null): string => { + if (!type) { + return ''; + } + if (type === TabEnum.Request) { + return activeRequest?._id || ''; + } + + if (type === TabEnum.Folder) { + return activeRequestGroup?._id || ''; + } + + if (type === TabEnum.Runner) { + return `runner_${workspaceId}`; + } + + if (type === TabEnum.MockRoute) { + return activeMockRoute?._id || ''; + } + + if (type === TabEnum.TESTSUITE) { + return unitTestSuite?._id || ''; + } + + if ([TabEnum.Collection, TabEnum.Document, TabEnum.Env, TabEnum.Mock, TabEnum.TEST].includes(type)) { + return workspaceId; + } + + return ''; + }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, unitTestSuite?._id, workspaceId]); + + const packTabInfo = useCallback((type: TabEnum): BaseTab | undefined => { + if (!type) { + return undefined; + } + if (type === TabEnum.Request) { + return { + type, + name: activeRequest?.name || 'Untitled request', + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + tag: getRequestMethodShortHand(activeRequest), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + }; + } + + if (type === TabEnum.Folder) { + return { + type, + name: 'My folder', + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + tag: getRequestMethodShortHand(activeRequest), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + }; + } + + if ([TabEnum.Collection, TabEnum.Document, TabEnum.Env, TabEnum.Mock, TabEnum.TEST].includes(type)) { + return { + type, + name: activeWorkspace.name, + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + }; + } + + if (type === TabEnum.Runner) { + return { + type, + name: 'Runner', + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + }; + } + + if (type === TabEnum.MockRoute) { + return { + type, + name: activeMockRoute?.name || 'Untitled mock route', + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + tag: formatMethodName(activeMockRoute?.method || ''), + }; + } + + if (type === TabEnum.TESTSUITE) { + return { + type, + name: unitTestSuite?.name || 'Untitled test suite', + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + }; + } + + return; + }, [activeMockRoute?.method, activeMockRoute?.name, activeOrganization?.name, activeProject.name, activeRequest, activeWorkspace.name, generateTabUrl, getTabId, organizationId, projectId, unitTestSuite?.name, workspaceId]); + + useEffect(() => { + const type = getTabType(location.pathname); + console.log('tabType:', type); + const currentTab = getCurrentTab(type); + console.log('currentTabExist:', currentTab); + if (!currentTab && type) { + const tabInfo = packTabInfo(type); + if (tabInfo) { + addTab(tabInfo); + return; + } + } + + // keep active tab in sync with the current route + if (currentTab) { + const currentActiveTabId = appTabsRef?.current?.[organizationId]?.activeTabId; + if (currentActiveTabId !== currentTab.id) { + changeActiveTab(currentTab.id); + } + } + }, [addTab, appTabsRef, changeActiveTab, getCurrentTab, location.pathname, organizationId, packTabInfo]); + +}; diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 7074714771c..f42f68661e3 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -68,10 +68,11 @@ import { isWebSocketRequestId, type WebSocketRequest, } from '../../models/websocket-request'; -import { isScratchpad } from '../../models/workspace'; +import { isDesign, isScratchpad } from '../../models/workspace'; import { getGrpcConnectionErrorDetails, isGrpcConnectionError } from '../../utils/grpc'; import { invariant } from '../../utils/invariant'; import { DropdownHint } from '../components/base/dropdown/dropdown-hint'; +import { DocumentTab } from '../components/document-tab'; import { RequestActionsDropdown } from '../components/dropdowns/request-actions-dropdown'; import { RequestGroupActionsDropdown } from '../components/dropdowns/request-group-actions-dropdown'; import { WorkspaceDropdown } from '../components/dropdowns/workspace-dropdown'; @@ -98,9 +99,11 @@ import { PlaceholderRequestPane } from '../components/panes/placeholder-request- import { RequestGroupPane } from '../components/panes/request-group-pane'; import { RequestPane } from '../components/panes/request-pane'; import { ResponsePane } from '../components/panes/response-pane'; +import { OrganizationTabList } from '../components/tabs/tabList'; import { getMethodShortHand } from '../components/tags/method-tag'; import { RealtimeResponsePane } from '../components/websockets/realtime-response-pane'; import { WebSocketRequestPane } from '../components/websockets/websocket-request-pane'; +import { useInsomniaTab } from '../hooks/tab'; import { useExecutionState } from '../hooks/use-execution-state'; import { useReadyState } from '../hooks/use-ready-state'; import { @@ -110,11 +113,13 @@ import { useRequestMetaPatcher, useRequestPatcher, } from '../hooks/use-request'; +import { useOrganizationLoaderData } from './organization'; import type { GrpcRequestLoaderData, RequestLoaderData, WebSocketRequestLoaderData, } from './request'; +import type { RequestGroupLoaderData } from './request-group'; import { useRootLoaderData } from './root'; import Runner from './runner'; import type { Child, WorkspaceLoaderData } from './workspace'; @@ -214,6 +219,12 @@ export const Debug: FC = () => { requestId?: string; requestGroupId?: string; }; + + const { activeRequestGroup } = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData || {}; + + const { organizations } = useOrganizationLoaderData(); + const activeOrganization = organizations.find(o => o.id === organizationId); + const [grpcStates, setGrpcStates] = useState( grpcRequests.map(r => ({ requestId: r._id, @@ -744,13 +755,24 @@ export const Debug: FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useInsomniaTab({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeRequest, + activeRequestGroup, + activeOrganization, + }); + return (
-
-
- +
+
+ {
+ {isDesign(activeWorkspace) && ( + + )}
{ + { activeCookieJar, caCertificate, clientCertificates, + activeWorkspace, } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; const { settings } = useRootLoaderData(); @@ -450,6 +455,18 @@ const Design: FC = () => { } }, [settings.forceVerticalLayout, direction]); + const { organizations } = useOrganizationLoaderData(); + const activeOrganization = organizations.find(o => o.id === organizationId); + + useInsomniaTab({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeOrganization, + }); + return ( @@ -470,29 +487,35 @@ const Design: FC = () => { +
-
+
setEnvironmentModalOpen(true)} />
- - + -
+ +
Spec @@ -983,6 +1006,7 @@ const Design: FC = () => { +
diff --git a/packages/insomnia/src/ui/routes/environments.tsx b/packages/insomnia/src/ui/routes/environments.tsx index 19f8ae16292..e1c61c68bd2 100644 --- a/packages/insomnia/src/ui/routes/environments.tsx +++ b/packages/insomnia/src/ui/routes/environments.tsx @@ -17,11 +17,14 @@ import { handleToggleEnvironmentType } from '../components/editors/environment-u import { Icon } from '../components/icon'; import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder'; import { showAlert } from '../components/modals'; +import { OrganizationTabList } from '../components/tabs/tabList'; +import { useInsomniaTab } from '../hooks/tab'; import { useOrganizationPermissions } from '../hooks/use-organization-features'; +import { useOrganizationLoaderData } from './organization'; import type { WorkspaceLoaderData } from './workspace'; const Environments = () => { - const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); + const { organizationId = '', projectId = '', workspaceId = '' } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); const routeData = useRouteLoaderData( ':workspaceId' ) as WorkspaceLoaderData; @@ -40,6 +43,7 @@ const Environments = () => { activeEnvironment, subEnvironments, activeWorkspaceMeta, + activeWorkspace, } = routeData; const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(activeEnvironment._id); const isUsingInsomniaCloudSync = Boolean(isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId); @@ -256,6 +260,18 @@ const Environments = () => { sidebar_toggle: toggleSidebar, }); + const { organizations } = useOrganizationLoaderData(); + const activeOrganization = organizations.find(o => o.id === organizationId); + + useInsomniaTab({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeOrganization, + }); + return ( @@ -410,6 +426,7 @@ const Environments = () => { +
diff --git a/packages/insomnia/src/ui/routes/mock-server.tsx b/packages/insomnia/src/ui/routes/mock-server.tsx index 9d80107648d..079b01589b7 100644 --- a/packages/insomnia/src/ui/routes/mock-server.tsx +++ b/packages/insomnia/src/ui/routes/mock-server.tsx @@ -2,7 +2,7 @@ import type { IconName } from '@fortawesome/fontawesome-svg-core'; import React, { Suspense, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { Breadcrumb, Breadcrumbs, Button, GridList, GridListItem, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components'; import { type ImperativePanelGroupHandle, Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; -import { type LoaderFunction, NavLink, Route, Routes, useFetcher, useLoaderData, useNavigate, useParams } from 'react-router-dom'; +import { type LoaderFunction, NavLink, Route, Routes, useFetcher, useLoaderData, useNavigate, useParams, useRouteLoaderData } from 'react-router-dom'; import { DEFAULT_SIDEBAR_SIZE } from '../../common/constants'; import * as models from '../../models'; @@ -18,9 +18,13 @@ import { AlertModal } from '../components/modals/alert-modal'; import { AskModal } from '../components/modals/ask-modal'; import { EmptyStatePane } from '../components/panes/empty-state-pane'; import { SvgIcon } from '../components/svg-icon'; +import { OrganizationTabList } from '../components/tabs/tabList'; import { formatMethodName } from '../components/tags/method-tag'; +import { useInsomniaTab } from '../hooks/tab'; import { MockRouteResponse, MockRouteRoute, useMockRoutePatcher } from './mock-route'; +import { useOrganizationLoaderData } from './organization'; import { useRootLoaderData } from './root'; +import type { WorkspaceLoaderData } from './workspace'; export interface MockServerLoaderData { mockServerId: string; mockRoutes: MockRoute[]; @@ -52,6 +56,11 @@ const MockServerRoute = () => { }; const { settings } = useRootLoaderData(); const { mockServerId, mockRoutes } = useLoaderData() as MockServerLoaderData; + + const { activeProject, activeWorkspace } = useRouteLoaderData( + ':workspaceId' + ) as WorkspaceLoaderData; + const fetcher = useFetcher(); const navigate = useNavigate(); const patchMockRoute = useMockRoutePatcher(); @@ -172,6 +181,20 @@ const MockServerRoute = () => { } }, [settings.forceVerticalLayout, direction]); + const { organizations } = useOrganizationLoaderData(); + const activeOrganization = organizations.find(o => o.id === organizationId); + + useInsomniaTab({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeOrganization, + mockRouteId, + activeMockRoute: mockRoutes.find(s => s._id === mockRouteId), + }); + return ( @@ -355,6 +378,7 @@ const MockServerRoute = () => { + diff --git a/packages/insomnia/src/ui/routes/organization.tsx b/packages/insomnia/src/ui/routes/organization.tsx index 88134d0b42f..537403f8fb1 100644 --- a/packages/insomnia/src/ui/routes/organization.tsx +++ b/packages/insomnia/src/ui/routes/organization.tsx @@ -57,6 +57,7 @@ import { PresentUsers } from '../components/present-users'; import { Toast } from '../components/toast'; import { useAIContext } from '../context/app/ai-context'; import { InsomniaEventStreamProvider } from '../context/app/insomnia-event-stream-context'; +import { InsomniaTabProvider } from '../context/app/insomnia-tab-context'; import { useOrganizationPermissions } from '../hooks/use-organization-features'; import { syncProjects } from './project'; import { useRootLoaderData } from './root'; @@ -603,41 +604,17 @@ const OrganizationRoute = () => { return ( +
-
+
{!user ? : null} -
-
- {workspaceData && isDesign(workspaceData?.activeWorkspace) && ( - - )} -
+
{user ? ( @@ -1085,6 +1062,7 @@ const OrganizationRoute = () => {
+
); }; diff --git a/packages/insomnia/src/ui/routes/project.tsx b/packages/insomnia/src/ui/routes/project.tsx index bf0c6dfb373..0f7d06f78bf 100644 --- a/packages/insomnia/src/ui/routes/project.tsx +++ b/packages/insomnia/src/ui/routes/project.tsx @@ -57,7 +57,7 @@ import { LandingPage, SentryMetrics } from '../../common/sentry'; import { descendingNumberSort, sortMethodMap } from '../../common/sorting'; import * as models from '../../models'; import { userSession } from '../../models'; -import type { ApiSpec } from '../../models/api-spec'; +import { type ApiSpec } from '../../models/api-spec'; import { sortProjects } from '../../models/helpers/project'; import type { MockServer } from '../../models/mock-server'; import type { Organization } from '../../models/organization'; @@ -86,6 +86,7 @@ import { GitRepositoryCloneModal } from '../components/modals/git-repository-set import { ImportModal } from '../components/modals/import-modal'; import { MockServerSettingsModal } from '../components/modals/mock-server-settings-modal'; import { EmptyStatePane } from '../components/panes/project-empty-state-pane'; +import { OrganizationTabList } from '../components/tabs/tabList'; import { TimeFromNow } from '../components/time-from-now'; import { useInsomniaEventStreamContext } from '../context/app/insomnia-event-stream-context'; import { useLoaderDeferData } from '../hooks/use-loader-defer-data'; @@ -991,7 +992,7 @@ const ProjectRoute: FC = () => {
-
+
setSelectedProjectId(e.target.value)}> + {projectOptions.map(project => ( + + ))} + + +
+ {!selectedProjectId && ( +

+ Project is required +

+ )} + +
+ +
+ {!selectedWorkspaceId && ( +

+ Workspace is required +

+ )} + {requestFetcher.data?.error && ( +

+ {requestFetcher.data.error} +

+ )} + + +
+ + +
+
+ + + ); +}; diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 371ec3d6327..0b34d7ea51d 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Button, GridList, Menu, MenuItem, MenuTrigger, Popover, type Selection } from 'react-aria-components'; -import { useNavigate } from 'react-router-dom'; +import { useFetcher, useNavigate } from 'react-router-dom'; import { type ChangeBufferEvent, type ChangeType, database } from '../../../common/database'; import * as models from '../../../models/index'; @@ -9,6 +9,7 @@ import type { Request } from '../../../models/request'; import { INSOMNIA_TAB_HEIGHT } from '../../constant'; import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; import { Icon } from '../icon'; +import { AddRequestToCollectionModal } from '../modals/add-request-to-collection-modal'; import { formatMethodName, getRequestMethodShortHand } from '../tags/method-tag'; import { type BaseTab, InsomniaTab, TabEnum } from './tab'; @@ -30,12 +31,16 @@ export const TAB_ROUTER_PATH: Record = { [TabEnum.TESTSUITE]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/*', }; -export const OrganizationTabList = ({ showActiveStatus = true }) => { +export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' }) => { const { currentOrgTabs } = useInsomniaTabContext(); const { tabList, activeTabId } = currentOrgTabs; console.log('activeTabId', activeTabId); const navigate = useNavigate(); + const [showAddRequestModal, setShowAddRequestModal] = useState(false); + + const requestFetcher = useFetcher(); + const { changeActiveTab, deleteTabById, deleteAllTabsUnderWorkspace, deleteAllTabsUnderProject, updateTabById, updateProjectName, updateWorkspaceName } = useInsomniaTabContext(); const handleSelectionChange = (keys: Selection) => { @@ -127,6 +132,25 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { }; }, [deleteTabById, handleDelete, handleUpdate]); + const addRequest = () => { + const currentTab = tabList.find(tab => tab.id === activeTabId); + if (currentTab) { + const { organizationId, projectId, workspaceId } = currentTab; + requestFetcher.submit( + { requestType: 'HTTP', parentId: workspaceId }, + { + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/new`, + method: 'post', + encType: 'application/json', + }, + ); + } + }; + + const addRequestToCollection = () => { + setShowAddRequestModal(true); + }; + if (!tabList.length) { return null; }; @@ -154,16 +178,19 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { - { }}> - save to current workspace - - { }}> - save to other workspace + {currentPage === 'debug' && ( + + add request to current collection + + )} + + add request to other collection
+ {showAddRequestModal && setShowAddRequestModal(false)} />}
); }; diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 81e624da864..3830c569444 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -1151,7 +1151,7 @@ export const Debug: FC = () => {
- + Date: Thu, 14 Nov 2024 18:22:01 +0800 Subject: [PATCH 11/34] move search box to center --- packages/insomnia/src/ui/routes/organization.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/insomnia/src/ui/routes/organization.tsx b/packages/insomnia/src/ui/routes/organization.tsx index f06a1e8ab96..40e413e1c14 100644 --- a/packages/insomnia/src/ui/routes/organization.tsx +++ b/packages/insomnia/src/ui/routes/organization.tsx @@ -607,14 +607,14 @@ const OrganizationRoute = () => {
-
+
-
- +
{!user ? : null}
+
{user ? ( From 4ee9901cb3da3d45bfc4072dc4905566a13b021d Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 14 Nov 2024 18:41:04 +0800 Subject: [PATCH 12/34] fix: cannot del request tab --- packages/insomnia/src/ui/components/tabs/tabList.tsx | 5 ++--- packages/insomnia/src/ui/constant.ts | 1 + packages/insomnia/src/ui/hooks/tab.ts | 2 +- packages/insomnia/src/ui/routes/debug.tsx | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 0b34d7ea51d..5ab661b99a4 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -6,7 +6,7 @@ import { type ChangeBufferEvent, type ChangeType, database } from '../../../comm import * as models from '../../../models/index'; import type { MockRoute } from '../../../models/mock-route'; import type { Request } from '../../../models/request'; -import { INSOMNIA_TAB_HEIGHT } from '../../constant'; +import { INNER_TAB_HEIGHT, INSOMNIA_TAB_HEIGHT } from '../../constant'; import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; import { Icon } from '../icon'; import { AddRequestToCollectionModal } from '../modals/add-request-to-collection-modal'; @@ -165,8 +165,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' defaultSelectedKeys={['req_737492dce0c3460a8a55762e5d1bbd99']} selectionMode="single" selectionBehavior='replace' - // Use +1 height to mask the wrapper border, and let the custom element in InsomniaTab act as the fake border.(we need different border for active tab) - className={`flex h-[${INSOMNIA_TAB_HEIGHT + 1}px] bg-[--color-bg] max-w-[calc(100%-50px)] overflow-x-scroll hide-scrollbars`} + className={`flex h-[${INNER_TAB_HEIGHT}px] bg-[--color-bg] max-w-[calc(100%-50px)] overflow-x-scroll hide-scrollbars`} // Use +1 height to mask the wrapper border, and let the custom element in InsomniaTab act as the fake border.(we need different border for active tab) items={tabList} > {item => } diff --git a/packages/insomnia/src/ui/constant.ts b/packages/insomnia/src/ui/constant.ts index ec3ebef8e58..e2003e17d1e 100644 --- a/packages/insomnia/src/ui/constant.ts +++ b/packages/insomnia/src/ui/constant.ts @@ -1,3 +1,4 @@ // this a constant file just for renderer process export const INSOMNIA_TAB_HEIGHT = 40; +export const INNER_TAB_HEIGHT = 41; diff --git a/packages/insomnia/src/ui/hooks/tab.ts b/packages/insomnia/src/ui/hooks/tab.ts index 1ad38b64ebf..191ed654517 100644 --- a/packages/insomnia/src/ui/hooks/tab.ts +++ b/packages/insomnia/src/ui/hooks/tab.ts @@ -51,7 +51,7 @@ export const useInsomniaTab = ({ } if (type === TabEnum.Collection) { - return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`; + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug?doNotSkipToActiveRequest=true`; } if (type === TabEnum.Env) { diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 3830c569444..92d11a822d4 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -164,7 +164,8 @@ export const loader: LoaderFunction = async ({ params, request }) => { const startOfQuery = request.url.indexOf('?'); const urlWithoutQuery = startOfQuery > 0 ? request.url.slice(0, startOfQuery) : request.url; const isDisplayingRunner = urlWithoutQuery.includes('/runner'); - if (activeRequest && !isDisplayingRunner) { + const doNotSkipToActiveRequest = request.url.includes('doNotSkipToActiveRequest=true'); + if (activeRequest && !isDisplayingRunner && !doNotSkipToActiveRequest) { return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${activeRequestId}`); } } From 4c1a28a1f1f5112ec06a8c15e5da37d224939e56 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Fri, 15 Nov 2024 15:04:08 +0800 Subject: [PATCH 13/34] fix ui --- .../modals/add-request-to-collection-modal.tsx | 10 +++++----- packages/insomnia/src/ui/components/tabs/tabList.tsx | 8 +++++--- packages/insomnia/src/ui/constant.ts | 1 - packages/insomnia/src/ui/hooks/tab.ts | 6 +++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/insomnia/src/ui/components/modals/add-request-to-collection-modal.tsx b/packages/insomnia/src/ui/components/modals/add-request-to-collection-modal.tsx index a8349b094e6..7de0d4c5508 100644 --- a/packages/insomnia/src/ui/components/modals/add-request-to-collection-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/add-request-to-collection-modal.tsx @@ -51,7 +51,7 @@ export const AddRequestToCollectionModal: FC = ({ onHide } }, []); const isBtnDisabled = requestFetcher.state !== 'idle' - || !selectedProjectId; + || !selectedProjectId || !selectedWorkspaceId; const createNewRequest = async () => { requestFetcher.submit( @@ -71,7 +71,7 @@ export const AddRequestToCollectionModal: FC = ({ onHide }
diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 5ab661b99a4..ac69998022d 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -6,7 +6,7 @@ import { type ChangeBufferEvent, type ChangeType, database } from '../../../comm import * as models from '../../../models/index'; import type { MockRoute } from '../../../models/mock-route'; import type { Request } from '../../../models/request'; -import { INNER_TAB_HEIGHT, INSOMNIA_TAB_HEIGHT } from '../../constant'; +import { INSOMNIA_TAB_HEIGHT } from '../../constant'; import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; import { Icon } from '../icon'; import { AddRequestToCollectionModal } from '../modals/add-request-to-collection-modal'; @@ -156,7 +156,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' }; return ( -
+
{item => } diff --git a/packages/insomnia/src/ui/constant.ts b/packages/insomnia/src/ui/constant.ts index e2003e17d1e..ec3ebef8e58 100644 --- a/packages/insomnia/src/ui/constant.ts +++ b/packages/insomnia/src/ui/constant.ts @@ -1,4 +1,3 @@ // this a constant file just for renderer process export const INSOMNIA_TAB_HEIGHT = 40; -export const INNER_TAB_HEIGHT = 41; diff --git a/packages/insomnia/src/ui/hooks/tab.ts b/packages/insomnia/src/ui/hooks/tab.ts index 191ed654517..fa652853fd1 100644 --- a/packages/insomnia/src/ui/hooks/tab.ts +++ b/packages/insomnia/src/ui/hooks/tab.ts @@ -171,7 +171,7 @@ export const useInsomniaTab = ({ if (type === TabEnum.Request) { return { type, - name: activeRequest?.name || 'Untitled request', + name: activeRequest?.name || 'New Request', url: generateTabUrl(type), organizationId: organizationId, projectId: projectId, @@ -187,7 +187,7 @@ export const useInsomniaTab = ({ if (type === TabEnum.Folder) { return { type, - name: 'My folder', + name: activeRequestGroup?.name || 'My Folder', url: generateTabUrl(type), organizationId: organizationId, projectId: projectId, @@ -257,7 +257,7 @@ export const useInsomniaTab = ({ } return; - }, [activeMockRoute?.method, activeMockRoute?.name, activeProject.name, activeRequest, activeWorkspace.name, generateTabUrl, getTabId, organizationId, projectId, unitTestSuite?.name, workspaceId]); + }, [activeMockRoute?.method, activeMockRoute?.name, activeProject.name, activeRequest, activeRequestGroup?.name, activeWorkspace.name, generateTabUrl, getTabId, organizationId, projectId, unitTestSuite?.name, workspaceId]); useEffect(() => { const type = getTabType(location.pathname); From e6df7a1f762ac99188bffba4daad869f4f09efe6 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Fri, 15 Nov 2024 15:41:17 +0800 Subject: [PATCH 14/34] tab background improvement --- packages/insomnia/src/ui/components/tabs/tab.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index 648873cfdac..218c9ff06f3 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -111,20 +111,20 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { {({ isSelected, isHovered }) => (
{renderTabIcon(tab.type)} {tab.name} -
- {isHovered && ( + {isHovered && ( +
- )} -
+
+ )}
From 1da43eef97f814ec9ae80cb64dfa1cad52e9e88d Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Mon, 18 Nov 2024 15:16:36 +0800 Subject: [PATCH 15/34] change tab background --- packages/insomnia/src/ui/components/tabs/tab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index 218c9ff06f3..c332a05c566 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -111,7 +111,7 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { {({ isSelected, isHovered }) => ( From 53397d160356f843f4043f6f84f162dbf8374f33 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Mon, 18 Nov 2024 18:10:14 +0800 Subject: [PATCH 16/34] feat: add list scroll --- .../src/ui/components/tabs/tabList.tsx | 77 +++++++++++++++---- .../src/ui/hooks/use-resize-observer.tsx | 27 +++++++ 2 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 packages/insomnia/src/ui/hooks/use-resize-observer.tsx diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index ac69998022d..c4177ee3da1 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -1,3 +1,4 @@ +import _ from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; import { Button, GridList, Menu, MenuItem, MenuTrigger, Popover, type Selection } from 'react-aria-components'; import { useFetcher, useNavigate } from 'react-router-dom'; @@ -8,6 +9,7 @@ import type { MockRoute } from '../../../models/mock-route'; import type { Request } from '../../../models/request'; import { INSOMNIA_TAB_HEIGHT } from '../../constant'; import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; +import { type Size, useResizeObserver } from '../../hooks/use-resize-observer'; import { Icon } from '../icon'; import { AddRequestToCollectionModal } from '../modals/add-request-to-collection-modal'; import { formatMethodName, getRequestMethodShortHand } from '../tags/method-tag'; @@ -38,6 +40,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' const navigate = useNavigate(); const [showAddRequestModal, setShowAddRequestModal] = useState(false); + const [isOverFlow, setIsOverFlow] = useState(false); const requestFetcher = useFetcher(); @@ -151,27 +154,69 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' setShowAddRequestModal(true); }; + const tabListInnerRef = React.useRef(null); + const tabListWrapperRef = React.useRef(null); + const componentWrapperRef = React.useRef(null); + + const onResize = () => { + console.log('resize'); + const innerWidth = tabListInnerRef.current?.clientWidth; + const componentWrapperWidth = componentWrapperRef.current?.clientWidth; + if (innerWidth && componentWrapperWidth && innerWidth > componentWrapperWidth - 50) { + setIsOverFlow(true); + } else { + setIsOverFlow(false); + } + }; + + const debouncedOnResize = _.debounce<(size: Size) => void>(onResize, 500); + + useResizeObserver(tabListWrapperRef, debouncedOnResize); + + const scrollLeft = () => { + if (!tabListWrapperRef.current) { + return; + } + tabListWrapperRef.current.scrollLeft -= 150; + }; + + const scrollRight = () => { + if (!tabListWrapperRef.current) { + return; + } + tabListWrapperRef.current.scrollLeft += 150; + }; + if (!tabList.length) { return null; }; return ( -
- - {item => } - +
+ +
+ + {item => } + +
+
-
+
From 530be35e5cf57659b4472caa37585bf212b996f9 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Wed, 20 Nov 2024 18:03:04 +0800 Subject: [PATCH 18/34] feat: add tab contextmenu --- packages/insomnia/src/main/ipc/electron.ts | 27 +++++++--- packages/insomnia/src/main/ipc/main.ts | 6 ++- packages/insomnia/src/preload.ts | 3 +- .../ui/components/codemirror/code-editor.tsx | 6 +-- .../components/codemirror/one-line-editor.tsx | 6 +-- .../insomnia/src/ui/components/tabs/tab.tsx | 36 +++++++++---- .../src/ui/components/tabs/tabList.tsx | 44 ++++++++++++--- .../ui/context/app/insomnia-tab-context.tsx | 53 +++++++++++++++---- 8 files changed, 139 insertions(+), 42 deletions(-) diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index 206433aa79c..241cb6bc51d 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -57,7 +57,8 @@ export type MainOnChannels = | 'restart' | 'set-hidden-window-busy-status' | 'setMenuBarVisibility' - | 'show-context-menu' + | 'show-nunjucks-context-menu' + | 'showContextMenu' | 'showItemInFolder' | 'showOpenDialog' | 'showSaveDialog' @@ -73,7 +74,8 @@ export type MainOnChannels = export type RendererOnChannels = 'clear-all-models' | 'clear-model' - | 'context-menu-command' + | 'nunjucks-context-menu-command' + | 'contextMenuCommand' | 'grpc.data' | 'grpc.end' | 'grpc.error' @@ -114,10 +116,10 @@ const getTemplateValue = (arg: NunjucksParsedTagArg) => { return arg.defaultValue; }; export function registerElectronHandlers() { - ipcMainOn('show-context-menu', (event, options: { key: string; nunjucksTag: ReturnType }) => { + ipcMainOn('show-nunjucks-context-menu', (event, options: { key: string; nunjucksTag: ReturnType }) => { const { key, nunjucksTag } = options; const sendNunjuckTagContextMsg = (type: NunjucksTagContextMenuAction) => { - event.sender.send('context-menu-command', { key, nunjucksTag: { ...nunjucksTag, type } }); + event.sender.send('nunjucks-context-menu-command', { key, nunjucksTag: { ...nunjucksTag, type } }); }; try { const baseTemplate: MenuItemConstructorOptions[] = nunjucksTag ? @@ -170,7 +172,7 @@ export function registerElectronHandlers() { { click: () => { const tag = `{% ${l.templateTag.name} ${l.templateTag.args?.map(getTemplateValue).join(', ')} %}`; - event.sender.send('context-menu-command', { key, tag }); + event.sender.send('nunjucks-context-menu-command', { key, tag }); }, } : { @@ -179,7 +181,7 @@ export function registerElectronHandlers() { click: () => { const additionalTagFields = additionalArgs.length ? ', ' + additionalArgs.map(getTemplateValue).join(', ') : ''; const tag = `{% ${l.templateTag.name} '${action.value}'${additionalTagFields} %}`; - event.sender.send('context-menu-command', { key, tag }); + event.sender.send('nunjucks-context-menu-command', { key, tag }); }, })), }), @@ -236,4 +238,17 @@ export function registerElectronHandlers() { ipcMainOn('getAppPath', event => { event.returnValue = app.getAppPath(); }); + + ipcMainOn('showContextMenu', (event, options: { key: string; menuItems: MenuItemConstructorOptions[]; extra?: Record }) => { + const menuItems = options.menuItems.map(item => { + return { + ...item, + click: () => { + event.sender.send('contextMenuCommand', { key: options.key, label: item.label, extra: options.extra }); + }, + }; + }); + const menu = Menu.buildFromTemplate(menuItems); + menu.popup(); + }); } diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index 216a90bd6e3..4421514de28 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/electron/main'; import type { MarkerRange } from 'codemirror'; -import { app, BrowserWindow, type IpcRendererEvent, shell } from 'electron'; +import { app, BrowserWindow, type IpcRendererEvent, type MenuItemConstructorOptions, shell } from 'electron'; import fs from 'fs'; import { APP_START_TIME, LandingPage, SentryMetrics } from '../../common/sentry'; @@ -39,7 +39,9 @@ export interface RendererToMainBridgeAPI { curl: CurlBridgeAPI; trackSegmentEvent: (options: { event: string; properties?: Record }) => void; trackPageView: (options: { name: string }) => void; - showContextMenu: (options: { key: string; nunjucksTag?: { template: string; range: MarkerRange } }) => void; + showNunjucksContextMenu: (options: { key: string; nunjucksTag?: { template: string; range: MarkerRange } }) => void; + showContextMenu: (options: { key: string; menuItems: MenuItemConstructorOptions[]; extra?: Record }) => void; + database: { caCertificate: { create: (options: { parentId: string; path: string }) => Promise; diff --git a/packages/insomnia/src/preload.ts b/packages/insomnia/src/preload.ts index fc994eb4346..cb1203b9fb2 100644 --- a/packages/insomnia/src/preload.ts +++ b/packages/insomnia/src/preload.ts @@ -69,7 +69,8 @@ const main: Window['main'] = { curl, trackSegmentEvent: options => ipcRenderer.send('trackSegmentEvent', options), trackPageView: options => ipcRenderer.send('trackPageView', options), - showContextMenu: options => ipcRenderer.send('show-context-menu', options), + showNunjucksContextMenu: options => ipcRenderer.send('show-nunjucks-context-menu', options), + showContextMenu: options => ipcRenderer.send('showContextMenu', options), database: { caCertificate: { create: options => ipcRenderer.invoke('database.caCertificate.create', options), diff --git a/packages/insomnia/src/ui/components/codemirror/code-editor.tsx b/packages/insomnia/src/ui/components/codemirror/code-editor.tsx index b63ae0d441e..fe3abd77097 100644 --- a/packages/insomnia/src/ui/components/codemirror/code-editor.tsx +++ b/packages/insomnia/src/ui/components/codemirror/code-editor.tsx @@ -546,7 +546,7 @@ export const CodeEditor = memo(forwardRef(({ } }; useEffect(() => { - const unsubscribe = window.main.on('context-menu-command', (_, { key, tag, nunjucksTag }) => { + const unsubscribe = window.main.on('nunjucks-context-menu-command', (_, { key, tag, nunjucksTag }) => { if (id === key) { if (nunjucksTag) { const { type, template, range } = nunjucksTag as nunjucksTagContextMenuOptions; @@ -657,10 +657,10 @@ export const CodeEditor = memo(forwardRef(({ const nunjucksTag = extractNunjucksTagFromCoords({ left: clientX, top: clientY }, codeMirror); if (nunjucksTag) { // show context menu for nunjucks tag - window.main.showContextMenu({ key: id, nunjucksTag }); + window.main.showNunjucksContextMenu({ key: id, nunjucksTag }); } } else { - window.main.showContextMenu({ key: id }); + window.main.showNunjucksContextMenu({ key: id }); } }} > diff --git a/packages/insomnia/src/ui/components/codemirror/one-line-editor.tsx b/packages/insomnia/src/ui/components/codemirror/one-line-editor.tsx index 694d4bd15dd..0ee5767fe9d 100644 --- a/packages/insomnia/src/ui/components/codemirror/one-line-editor.tsx +++ b/packages/insomnia/src/ui/components/codemirror/one-line-editor.tsx @@ -247,7 +247,7 @@ export const OneLineEditor = forwardRef }, [onChange]); useEffect(() => { - const unsubscribe = window.main.on('context-menu-command', (_, { key, tag, nunjucksTag }) => { + const unsubscribe = window.main.on('nunjucks-context-menu-command', (_, { key, tag, nunjucksTag }) => { if (id === key) { if (nunjucksTag) { const { type, template, range } = nunjucksTag as nunjucksTagContextMenuOptions; @@ -310,10 +310,10 @@ export const OneLineEditor = forwardRef const nunjucksTag = extractNunjucksTagFromCoords({ left: clientX, top: clientY }, codeMirror); if (nunjucksTag) { // show context menu for nunjucks tag - window.main.showContextMenu({ key: id, nunjucksTag }); + window.main.showNunjucksContextMenu({ key: id, nunjucksTag }); } } else { - window.main.showContextMenu({ key: id }); + window.main.showNunjucksContextMenu({ key: id }); } }} > diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index c332a05c566..e710d88c967 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -72,7 +72,7 @@ const WORKSPACE_TAB_UI_MAP: Record = { export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { - const { deleteTabById } = useInsomniaTabContext(); + const { closeTabById } = useInsomniaTabContext(); const renderTabIcon = (type: TabEnum) => { if (WORKSPACE_TAB_UI_MAP[type]) { @@ -104,7 +104,25 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { }; const handleClose = (id: string) => { - deleteTabById(id); + closeTabById(id); + }; + + const handleContextMenu = (e: React.MouseEvent) => { + e.preventDefault(); + window.main.showContextMenu({ + key: 'insomniaTab', + menuItems: [ + { + label: 'Close All', + }, + { + label: 'Close Others', + }, + ], + extra: { + currentTabId: tab.id, + }, + }); }; return ( @@ -115,16 +133,12 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { > {({ isSelected, isHovered }) => ( -
+
{renderTabIcon(tab.type)} - {tab.name} - {isHovered && ( -
- -
- )} + {tab.name} +
diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index b70987b6068..2191238e363 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -44,7 +44,17 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' const requestFetcher = useFetcher(); - const { changeActiveTab, deleteTabById, deleteAllTabsUnderWorkspace, deleteAllTabsUnderProject, updateTabById, updateProjectName, updateWorkspaceName } = useInsomniaTabContext(); + const { + changeActiveTab, + closeTabById, + closeAllTabsUnderWorkspace, + closeAllTabsUnderProject, + updateTabById, + updateProjectName, + updateWorkspaceName, + closeAllTabs, + closeOtherTabs, + } = useInsomniaTabContext(); const handleSelectionChange = (keys: Selection) => { console.log('changeActiveTab'); @@ -80,16 +90,16 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' const handleDelete = useCallback((docId: string, docType: string) => { if (docType === models.project.type) { // delete all tabs of this project - deleteAllTabsUnderProject?.(docId); + closeAllTabsUnderProject?.(docId); } if (docType === models.workspace.type) { // delete all tabs of this workspace - deleteAllTabsUnderWorkspace?.(docId); + closeAllTabsUnderWorkspace?.(docId); } else { // delete tab by id - deleteTabById(docId); + closeTabById(docId); } - }, [deleteAllTabsUnderProject, deleteAllTabsUnderWorkspace, deleteTabById]); + }, [closeAllTabsUnderProject, closeAllTabsUnderWorkspace, closeTabById]); const handleUpdate = useCallback((doc: models.BaseModel) => { // currently have 2 types of update, rename and change request method @@ -133,7 +143,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' return () => { database.offChange(callback); }; - }, [deleteTabById, handleDelete, handleUpdate]); + }, [handleDelete, handleUpdate]); const addRequest = () => { const currentTab = tabList.find(tab => tab.id === activeTabId); @@ -187,6 +197,28 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' tabListWrapperRef.current.scrollLeft += 150; }; + useEffect(() => { + const unsubscribe = window.main.on('contextMenuCommand', (_, { key, label, extra }) => { + if (key !== 'insomniaTab') { + return; + } + switch (label) { + case 'Close All': + closeAllTabs?.(); + break; + case 'Close Others': + closeOtherTabs?.(extra?.currentTabId); + break; + default: + break; + } + }); + + return () => { + unsubscribe(); + }; + }, [closeAllTabs, closeOtherTabs]); + if (!tabList.length) { return null; }; diff --git a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx index 1e2f02ef2c8..36646824206 100644 --- a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -14,14 +14,16 @@ interface UpdateInsomniaTabParams { interface ContextProps { currentOrgTabs: OrganizationTabs; appTabsRef?: React.MutableRefObject; - deleteTabById: (id: string) => void; + closeTabById: (id: string) => void; addTab: (tab: BaseTab) => void; changeActiveTab: (id: string) => void; - deleteAllTabsUnderWorkspace?: (workspaceId: string) => void; - deleteAllTabsUnderProject?: (projectId: string) => void; + closeAllTabsUnderWorkspace?: (workspaceId: string) => void; + closeAllTabsUnderProject?: (projectId: string) => void; updateProjectName?: (projectId: string, name: string) => void; updateWorkspaceName?: (projectId: string, name: string) => void; updateTabById?: (tabId: string, name: string, method?: string, tag?: string) => void; + closeAllTabs?: () => void; + closeOtherTabs?: (id: string) => void; } const InsomniaTabContext = createContext({ @@ -29,7 +31,7 @@ const InsomniaTabContext = createContext({ tabList: [], activeTabId: '', }, - deleteTabById: () => { }, + closeTabById: () => { }, addTab: () => { }, changeActiveTab: () => { }, }); @@ -79,7 +81,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { }); }, [organizationId, updateInsomniaTabs]); - const deleteTabById = useCallback((id: string) => { + const closeTabById = useCallback((id: string) => { const currentTabs = appTabsRef?.current?.[organizationId]; if (!currentTabs) { return; @@ -111,7 +113,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { }); }, [navigate, organizationId, projectId, updateInsomniaTabs]); - const deleteAllTabsUnderWorkspace = useCallback((workspaceId: string) => { + const closeAllTabsUnderWorkspace = useCallback((workspaceId: string) => { const currentTabs = appTabsRef?.current?.[organizationId]; if (!currentTabs) { return; @@ -125,7 +127,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { }); }, [organizationId, updateInsomniaTabs]); - const deleteAllTabsUnderProject = useCallback((projectId: string) => { + const closeAllTabsUnderProject = useCallback((projectId: string) => { const currentTabs = appTabsRef?.current?.[organizationId]; if (!currentTabs) { return; @@ -139,6 +141,35 @@ export const InsomniaTabProvider: FC = ({ children }) => { }); }, [organizationId, updateInsomniaTabs]); + const closeAllTabs = useCallback(() => { + navigate(`/organization/${organizationId}/project/${projectId}`); + updateInsomniaTabs({ + organizationId, + tabList: [], + activeTabId: '', + }); + }, [navigate, organizationId, projectId, updateInsomniaTabs]); + + const closeOtherTabs = useCallback((id: string) => { + const currentTabs = appTabsRef?.current?.[organizationId]; + if (!currentTabs) { + return; + } + const reservedTab = currentTabs.tabList.find(tab => tab.id === id); + if (!reservedTab) { + return; + } + + if (currentTabs.activeTabId !== id) { + navigate(reservedTab.url); + } + updateInsomniaTabs({ + organizationId, + tabList: [reservedTab], + activeTabId: id, + }); + }, [navigate, organizationId, updateInsomniaTabs]); + const updateTabById = useCallback((tabId: string, name: string, method: string = '', tag: string = '') => { const currentTabs = appTabsRef?.current?.[organizationId]; if (!currentTabs) { @@ -221,15 +252,17 @@ export const InsomniaTabProvider: FC = ({ children }) => { {children} From 1fe4abd8c1e79e9d264d2e229f26dd7f090fb230 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Wed, 20 Nov 2024 18:07:27 +0800 Subject: [PATCH 19/34] modify menu text --- packages/insomnia/src/ui/components/tabs/tab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index e710d88c967..3614a05bb55 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -113,10 +113,10 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { key: 'insomniaTab', menuItems: [ { - label: 'Close All', + label: 'Close all', }, { - label: 'Close Others', + label: 'Close others', }, ], extra: { From 154a3c58534a379d0413da31fc8e5724ed68f151 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 21 Nov 2024 12:01:04 +0800 Subject: [PATCH 20/34] fix(ui): tab disappear in panelgroup --- packages/insomnia/src/ui/routes/debug.tsx | 2 +- packages/insomnia/src/ui/routes/design.tsx | 2 +- packages/insomnia/src/ui/routes/environments.tsx | 4 ++-- packages/insomnia/src/ui/routes/mock-server.tsx | 2 +- packages/insomnia/src/ui/routes/project.tsx | 6 +++--- packages/insomnia/src/ui/routes/unit-test.tsx | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 92d11a822d4..47488423768 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -1151,7 +1151,7 @@ export const Debug: FC = () => {
- + diff --git a/packages/insomnia/src/ui/routes/design.tsx b/packages/insomnia/src/ui/routes/design.tsx index 0850af2a3df..bc37356e1b2 100644 --- a/packages/insomnia/src/ui/routes/design.tsx +++ b/packages/insomnia/src/ui/routes/design.tsx @@ -999,7 +999,7 @@ const Design: FC = () => {
- + diff --git a/packages/insomnia/src/ui/routes/environments.tsx b/packages/insomnia/src/ui/routes/environments.tsx index 4dca529dab9..9e33a4185b7 100644 --- a/packages/insomnia/src/ui/routes/environments.tsx +++ b/packages/insomnia/src/ui/routes/environments.tsx @@ -421,9 +421,9 @@ const Environments = () => { - + -
+
diff --git a/packages/insomnia/src/ui/routes/mock-server.tsx b/packages/insomnia/src/ui/routes/mock-server.tsx index faaa00a8d15..c3614a2e8d5 100644 --- a/packages/insomnia/src/ui/routes/mock-server.tsx +++ b/packages/insomnia/src/ui/routes/mock-server.tsx @@ -372,7 +372,7 @@ const MockServerRoute = () => {
- + diff --git a/packages/insomnia/src/ui/routes/project.tsx b/packages/insomnia/src/ui/routes/project.tsx index 0f7d06f78bf..895de4b5bec 100644 --- a/packages/insomnia/src/ui/routes/project.tsx +++ b/packages/insomnia/src/ui/routes/project.tsx @@ -1197,10 +1197,10 @@ const ProjectRoute: FC = () => {
- + {activeProject ? ( -
+
{billing.isActive ? null :

@@ -1483,7 +1483,7 @@ const ProjectRoute: FC = () => {

) : ( -
+

This is an empty Organization. To get started create your first project.

diff --git a/packages/insomnia/src/ui/routes/unit-test.tsx b/packages/insomnia/src/ui/routes/unit-test.tsx index 4558f8c038b..f48d1d78b94 100644 --- a/packages/insomnia/src/ui/routes/unit-test.tsx +++ b/packages/insomnia/src/ui/routes/unit-test.tsx @@ -472,7 +472,7 @@ const TestRoute: FC = () => { - + From 394aa87efa2d7ce725cdce85222111eab8d45db1 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 21 Nov 2024 16:02:24 +0800 Subject: [PATCH 21/34] feat: optimize tablist scroll button --- .../src/ui/components/tabs/tabList.tsx | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 2191238e363..f2e234d2d26 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -41,6 +41,8 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' const [showAddRequestModal, setShowAddRequestModal] = useState(false); const [isOverFlow, setIsOverFlow] = useState(false); + const [leftScrollDisable, setLeftScrollDisable] = useState(false); + const [rightScrollDisable, setRightScrollDisable] = useState(false); const requestFetcher = useFetcher(); @@ -166,13 +168,12 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' const tabListInnerRef = React.useRef(null); const tabListWrapperRef = React.useRef(null); - const componentWrapperRef = React.useRef(null); const onResize = () => { console.log('resize'); const innerWidth = tabListInnerRef.current?.clientWidth; - const componentWrapperWidth = componentWrapperRef.current?.clientWidth; - if (innerWidth && componentWrapperWidth && innerWidth > componentWrapperWidth - 50) { + const wrapperWidth = tabListWrapperRef.current?.clientWidth; + if (innerWidth && wrapperWidth && innerWidth > wrapperWidth) { setIsOverFlow(true); } else { setIsOverFlow(false); @@ -219,22 +220,46 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' }; }, [closeAllTabs, closeOtherTabs]); + const calculateScrollButtonStatus = (target: HTMLDivElement) => { + const { scrollLeft, scrollWidth, clientWidth } = target; + if (scrollLeft === 0) { + setLeftScrollDisable(true); + } else { + setLeftScrollDisable(false); + } + + if (scrollLeft + clientWidth >= scrollWidth - 1) { + setRightScrollDisable(true); + } else { + setRightScrollDisable(false); + } + }; + + const handleScroll = (e: React.UIEvent) => { + calculateScrollButtonStatus(e.target as HTMLDivElement); + }; + + useEffect(() => { + if (isOverFlow && tabListWrapperRef?.current) { + calculateScrollButtonStatus(tabListWrapperRef?.current); + } + }, [isOverFlow]); + if (!tabList.length) { return null; }; return ( -
- -
+
}
- -
+
From 9cbd86b88de335ef39fef0f014e7ac3dca57088a Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 21 Nov 2024 16:08:23 +0800 Subject: [PATCH 22/34] add context menu enum --- packages/insomnia/src/ui/components/tabs/tab.tsx | 5 +++-- packages/insomnia/src/ui/components/tabs/tabList.tsx | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index 3614a05bb55..e4035471a25 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -4,6 +4,7 @@ import { Button, GridListItem } from 'react-aria-components'; import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; import { Icon } from '../icon'; import { Tooltip } from '../tooltip'; +import { TAB_CONTEXT_MENU_COMMAND } from './tabList'; export enum TabEnum { Request = 'request', @@ -113,10 +114,10 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { key: 'insomniaTab', menuItems: [ { - label: 'Close all', + label: TAB_CONTEXT_MENU_COMMAND.CLOSE_ALL, }, { - label: 'Close others', + label: TAB_CONTEXT_MENU_COMMAND.CLOSE_OTHERS, }, ], extra: { diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index f2e234d2d26..64e03907ac1 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -20,6 +20,11 @@ export interface OrganizationTabs { activeTabId?: string; } +export const enum TAB_CONTEXT_MENU_COMMAND { + CLOSE_ALL = 'Close all', + CLOSE_OTHERS = 'Close others', +} + export const TAB_ROUTER_PATH: Record = { [TabEnum.Collection]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug', [TabEnum.Folder]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request-group/:requestGroupId', @@ -204,10 +209,10 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' return; } switch (label) { - case 'Close All': + case TAB_CONTEXT_MENU_COMMAND.CLOSE_ALL: closeAllTabs?.(); break; - case 'Close Others': + case TAB_CONTEXT_MENU_COMMAND.CLOSE_OTHERS: closeOtherTabs?.(extra?.currentTabId); break; default: From f631130beaf8b2f747213c74a377119bd169cda6 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Mon, 25 Nov 2024 11:45:01 +0800 Subject: [PATCH 23/34] del log --- packages/insomnia/src/ui/components/tabs/tabList.tsx | 5 ----- .../insomnia/src/ui/context/app/insomnia-tab-context.tsx | 1 - packages/insomnia/src/ui/hooks/tab.ts | 4 ---- 3 files changed, 10 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 64e03907ac1..54c9054ebce 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -41,7 +41,6 @@ export const TAB_ROUTER_PATH: Record = { export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' }) => { const { currentOrgTabs } = useInsomniaTabContext(); const { tabList, activeTabId } = currentOrgTabs; - console.log('activeTabId', activeTabId); const navigate = useNavigate(); const [showAddRequestModal, setShowAddRequestModal] = useState(false); @@ -64,9 +63,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' } = useInsomniaTabContext(); const handleSelectionChange = (keys: Selection) => { - console.log('changeActiveTab'); if (keys !== 'all') { - console.log('tab change', keys); const key = [...keys.values()]?.[0] as string; const tab = tabList.find(tab => tab.id === key); tab?.url && navigate(tab?.url); @@ -132,7 +129,6 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' useEffect(() => { // sync tabList with database const callback = async (changes: ChangeBufferEvent[]) => { - console.log('database change', changes); for (const change of changes) { const changeType = change[0]; const doc = change[1]; @@ -175,7 +171,6 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' const tabListWrapperRef = React.useRef(null); const onResize = () => { - console.log('resize'); const innerWidth = tabListInnerRef.current?.clientWidth; const wrapperWidth = tabListWrapperRef.current?.clientWidth; if (innerWidth && wrapperWidth && innerWidth > wrapperWidth) { diff --git a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx index 36646824206..253ca68bd95 100644 --- a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -71,7 +71,6 @@ export const InsomniaTabProvider: FC = ({ children }) => { }, [setAppTabs]); const addTab = useCallback((tab: BaseTab) => { - console.log('addTab'); const currentTabs = appTabsRef?.current?.[organizationId] || { tabList: [], activeTabId: '' }; updateInsomniaTabs({ diff --git a/packages/insomnia/src/ui/hooks/tab.ts b/packages/insomnia/src/ui/hooks/tab.ts index fa652853fd1..64ff5f2dc86 100644 --- a/packages/insomnia/src/ui/hooks/tab.ts +++ b/packages/insomnia/src/ui/hooks/tab.ts @@ -38,7 +38,6 @@ export const useInsomniaTab = ({ unitTestSuite, }: InsomniaTabProps) => { - console.log(activeMockRoute, 'activeMockRoute'); const { appTabsRef, addTab, changeActiveTab } = useInsomniaTabContext(); const generateTabUrl = useCallback((type: TabEnum) => { @@ -87,7 +86,6 @@ export const useInsomniaTab = ({ const location = useLocation(); const getTabType = (pathname: string) => { - console.log(pathname); for (const type in TAB_ROUTER_PATH) { const ifMatch = matchPath({ path: TAB_ROUTER_PATH[type as TabEnum], @@ -261,9 +259,7 @@ export const useInsomniaTab = ({ useEffect(() => { const type = getTabType(location.pathname); - console.log('tabType:', type); const currentTab = getCurrentTab(type); - console.log('currentTabExist:', currentTab); if (!currentTab && type) { const tabInfo = packTabInfo(type); if (tabInfo) { From 24e205f2b9aa0805c2c52f14171e19d149d8168e Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Tue, 26 Nov 2024 17:09:35 +0800 Subject: [PATCH 24/34] fix: rename workspace --- packages/insomnia/src/ui/components/tabs/tab.tsx | 2 +- packages/insomnia/src/ui/components/tabs/tabList.tsx | 2 +- packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index e4035471a25..8d9a2c9fca5 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -133,7 +133,7 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { className="outline-none aria-selected:text-[--color-font] aria-selected:bg-[--hl-sm] hover:bg-[--hl-xs]" > {({ isSelected, isHovered }) => ( - +
{renderTabIcon(tab.type)} {tab.name} diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 54c9054ebce..4c1c69ddae8 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -119,7 +119,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' // update project name(for tooltip) updateProjectName?.(doc._id, doc.name); } else if (doc.type === models.workspace.type) { - // update workspace name(for tooltip) + // update workspace name(for tooltip) & update name for workspace tab updateWorkspaceName?.(doc._id, doc.name); } else { updateTabById?.(doc._id, doc.name); diff --git a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx index 253ca68bd95..ee20784de5f 100644 --- a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -236,6 +236,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { return { ...tab, workspaceName: name, + name: tab.id === workspaceId ? name : tab.name, }; } return tab; From 45bbd0ff1e5d1bcb52e46fd8544e5bb197a0f3aa Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Wed, 27 Nov 2024 11:39:09 +0800 Subject: [PATCH 25/34] feat: scroll into view if needed --- packages/insomnia/src/ui/components/tabs/tab.tsx | 12 ++++++++++-- packages/insomnia/src/ui/components/tabs/tabList.tsx | 8 ++++++-- packages/insomnia/src/ui/routes/debug.tsx | 10 +++++++++- packages/insomnia/src/utils/index.ts | 6 ++++++ 4 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 packages/insomnia/src/utils/index.ts diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index 8d9a2c9fca5..33bdc2edd81 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { Button, GridListItem } from 'react-aria-components'; +import { scrollElementIntoView } from '../../../utils'; import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; import { Icon } from '../icon'; import { Tooltip } from '../tooltip'; @@ -73,7 +74,7 @@ const WORKSPACE_TAB_UI_MAP: Record = { export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { - const { closeTabById } = useInsomniaTabContext(); + const { closeTabById, currentOrgTabs } = useInsomniaTabContext(); const renderTabIcon = (type: TabEnum) => { if (WORKSPACE_TAB_UI_MAP[type]) { @@ -126,11 +127,18 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { }); }; + const scrollIntoView = useCallback((node: HTMLDivElement) => { + if (node && currentOrgTabs.activeTabId === tab.id) { + scrollElementIntoView(node, { behavior: 'instant' }); + } + }, [currentOrgTabs.activeTabId, tab.id]); + return ( {({ isSelected, isHovered }) => ( diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 4c1c69ddae8..4bf87d7aae0 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import _, { set } from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; import { Button, GridList, Menu, MenuItem, MenuTrigger, Popover, type Selection } from 'react-aria-components'; import { useFetcher, useNavigate } from 'react-router-dom'; @@ -188,14 +188,18 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' if (!tabListWrapperRef.current) { return; } + tabListWrapperRef.current.style.scrollBehavior = 'smooth'; tabListWrapperRef.current.scrollLeft -= 150; + tabListWrapperRef.current.style.scrollBehavior = 'auto'; }; const scrollRight = () => { if (!tabListWrapperRef.current) { return; } + tabListWrapperRef.current.style.scrollBehavior = 'smooth'; tabListWrapperRef.current.scrollLeft += 150; + tabListWrapperRef.current.style.scrollBehavior = 'auto'; }; useEffect(() => { @@ -254,7 +258,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' -
+
{ + if (isSelected && node) { + scrollElementIntoView(node, { behavior: 'instant' }); + } + }, [isSelected]); + return (
{ e.preventDefault(); setIsContextMenuOpen(true); diff --git a/packages/insomnia/src/utils/index.ts b/packages/insomnia/src/utils/index.ts new file mode 100644 index 00000000000..94b3ba88edc --- /dev/null +++ b/packages/insomnia/src/utils/index.ts @@ -0,0 +1,6 @@ +export const scrollElementIntoView = (element: HTMLElement, options?: ScrollIntoViewOptions) => { + if (element) { + // @ts-expect-error -- scrollIntoViewIfNeeded is not a standard method + element.scrollIntoViewIfNeeded ? element.scrollIntoViewIfNeeded() : element.scrollIntoView(options); + } +}; From dc4e3237341086ee0eb8124646a1159c5e9a3250 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Wed, 27 Nov 2024 17:07:51 +0800 Subject: [PATCH 26/34] fix: runner request list not update after switch tab --- .../insomnia/src/ui/components/tabs/tabList.tsx | 2 +- packages/insomnia/src/ui/routes/runner.tsx | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 4bf87d7aae0..7439a7f3bb7 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -1,4 +1,4 @@ -import _, { set } from 'lodash'; +import _ from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; import { Button, GridList, Menu, MenuItem, MenuTrigger, Popover, type Selection } from 'react-aria-components'; import { useFetcher, useNavigate } from 'react-router-dom'; diff --git a/packages/insomnia/src/ui/routes/runner.tsx b/packages/insomnia/src/ui/routes/runner.tsx index 3b148c41609..9f1b2df2c0a 100644 --- a/packages/insomnia/src/ui/routes/runner.tsx +++ b/packages/insomnia/src/ui/routes/runner.tsx @@ -1,6 +1,6 @@ import type { RequestContext } from 'insomnia-sdk'; import porderedJSON from 'json-order'; -import React, { type FC, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { type FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Checkbox, DropIndicator, GridList, GridListItem, type GridListItemProps, Heading, type Key, Tab, TabList, TabPanel, Tabs, Toolbar, TooltipTrigger, useDragAndDrop } from 'react-aria-components'; import { Panel, PanelResizeHandle } from 'react-resizable-panels'; import { type ActionFunction, type LoaderFunction, redirect, useNavigate, useParams, useRouteLoaderData, useSearchParams, useSubmit } from 'react-router-dom'; @@ -254,6 +254,18 @@ export const Runner: FC<{}> = () => { return requestRows.some((row: RequestRow, index: number) => row.id !== reqList.items[index].id); }, [requestRows, reqList]); + const previousWorkspaceId = useRef(''); + + useEffect(() => { + if (previousWorkspaceId.current && previousWorkspaceId.current !== workspaceId) { + // reset the list when workspace changes + const keys = reqList.items.map(item => item.id); + reqList.remove(...keys); + reqList.append(...requestRows); + } + previousWorkspaceId.current = workspaceId; + }, [reqList, requestRows, workspaceId]); + const { dragAndDropHooks: requestsDnD } = useDragAndDrop({ getItems: keys => { return [...keys].map(key => { From 2ea4b20fb126fba1e4d931cc4d9ca61965731776 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 28 Nov 2024 17:13:33 +0800 Subject: [PATCH 27/34] fix: runner not update --- .../ui/hooks/{tab.ts => use-insomnia-tab.ts} | 9 +- .../src/ui/hooks/use-runner-request-list.tsx | 77 +++++++++++++++ packages/insomnia/src/ui/routes/debug.tsx | 2 +- packages/insomnia/src/ui/routes/design.tsx | 2 +- .../insomnia/src/ui/routes/environments.tsx | 2 +- .../insomnia/src/ui/routes/mock-server.tsx | 2 +- packages/insomnia/src/ui/routes/runner.tsx | 98 +++---------------- packages/insomnia/src/ui/routes/unit-test.tsx | 2 +- 8 files changed, 100 insertions(+), 94 deletions(-) rename packages/insomnia/src/ui/hooks/{tab.ts => use-insomnia-tab.ts} (97%) create mode 100644 packages/insomnia/src/ui/hooks/use-runner-request-list.tsx diff --git a/packages/insomnia/src/ui/hooks/tab.ts b/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts similarity index 97% rename from packages/insomnia/src/ui/hooks/tab.ts rename to packages/insomnia/src/ui/hooks/use-insomnia-tab.ts index 64ff5f2dc86..bb93478e5b7 100644 --- a/packages/insomnia/src/ui/hooks/tab.ts +++ b/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts @@ -39,6 +39,7 @@ export const useInsomniaTab = ({ }: InsomniaTabProps) => { const { appTabsRef, addTab, changeActiveTab } = useInsomniaTabContext(); + const location = useLocation(); const generateTabUrl = useCallback((type: TabEnum) => { if (type === TabEnum.Request) { @@ -58,7 +59,7 @@ export const useInsomniaTab = ({ } if (type === TabEnum.Runner) { - return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner`; + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner${location.search}`; } if (type === TabEnum.Mock) { @@ -81,9 +82,7 @@ export const useInsomniaTab = ({ return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite?._id}`; } return ''; - }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, organizationId, projectId, unitTestSuite?._id, workspaceId]); - - const location = useLocation(); + }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, location.search, organizationId, projectId, unitTestSuite?._id, workspaceId]); const getTabType = (pathname: string) => { for (const type in TAB_ROUTER_PATH) { @@ -275,5 +274,5 @@ export const useInsomniaTab = ({ changeActiveTab(currentTab.id); } } - }, [addTab, appTabsRef, changeActiveTab, getCurrentTab, location.pathname, organizationId, packTabInfo]); + }, [addTab, appTabsRef, changeActiveTab, getCurrentTab, location.pathname, location.search, organizationId, packTabInfo]); }; diff --git a/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx b/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx new file mode 100644 index 00000000000..962ad184800 --- /dev/null +++ b/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx @@ -0,0 +1,77 @@ +import { useEffect, useMemo, useRef } from 'react'; +import { useRouteLoaderData } from 'react-router-dom'; +import { useListData } from 'react-stately'; +import { usePrevious } from 'react-use'; + +import { isRequest, type Request } from '../../models/request'; +import { isRequestGroup } from '../../models/request-group'; +import { invariant } from '../../utils/invariant'; +import type { RequestRow } from '../routes/runner'; +import type { Child, WorkspaceLoaderData } from '../routes/workspace'; + +export const useRunnerRequestList = (workspaceId: string, targetFolderId: string) => { + const { collection } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; + const entityMapRef = useRef(new Map()); + + const requestRows: RequestRow[] = useMemo(() => { + return collection + .filter(item => { + entityMapRef.current.set(item.doc._id, item); + return isRequest(item.doc); + }) + .map((item: Child) => { + const ancestorNames: string[] = []; + const ancestorIds: string[] = []; + if (item.ancestors) { + item.ancestors.forEach(ancestorId => { + const ancestor = entityMapRef.current.get(ancestorId); + if (ancestor && isRequestGroup(ancestor?.doc)) { + ancestorNames.push(ancestor?.doc.name); + ancestorIds.push(ancestor?.doc._id); + } + }); + } + + const requestDoc = item.doc as Request; + invariant('method' in item.doc, 'Only Request is supported at the moment'); + return { + id: item.doc._id, + name: item.doc.name, + ancestorNames, + ancestorIds, + method: requestDoc.method, + url: item.doc.url, + parentId: item.doc.parentId, + }; + }) + .filter(item => { + if (targetFolderId) { + return item.ancestorIds.includes(targetFolderId); + } + return true; + }); + }, [collection, targetFolderId]); + + const reqList = useListData({ + initialItems: requestRows, + }); + + const previousWorkspaceId = usePrevious(workspaceId); + const previousTargetFolderId = usePrevious(targetFolderId); + + useEffect(() => { + if ((previousWorkspaceId && previousWorkspaceId !== workspaceId) || (previousTargetFolderId !== undefined && previousTargetFolderId !== targetFolderId)) { + console.log('reset list'); + // reset the list when workspace changes + const keys = reqList.items.map(item => item.id); + reqList.remove(...keys); + reqList.append(...requestRows); + } + }, [reqList, requestRows, workspaceId, targetFolderId, previousWorkspaceId, previousTargetFolderId]); + + return { + reqList, + requestRows, + entityMap: entityMapRef.current, + }; +}; diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 605e4dfacde..a7ba72fc447 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -105,8 +105,8 @@ import { getMethodShortHand } from '../components/tags/method-tag'; import { RealtimeResponsePane } from '../components/websockets/realtime-response-pane'; import { WebSocketRequestPane } from '../components/websockets/websocket-request-pane'; import { INSOMNIA_TAB_HEIGHT } from '../constant'; -import { useInsomniaTab } from '../hooks/tab'; import { useExecutionState } from '../hooks/use-execution-state'; +import { useInsomniaTab } from '../hooks/use-insomnia-tab'; import { useReadyState } from '../hooks/use-ready-state'; import { type CreateRequestType, diff --git a/packages/insomnia/src/ui/routes/design.tsx b/packages/insomnia/src/ui/routes/design.tsx index bc37356e1b2..477cc6df02e 100644 --- a/packages/insomnia/src/ui/routes/design.tsx +++ b/packages/insomnia/src/ui/routes/design.tsx @@ -70,7 +70,7 @@ import { OrganizationTabList } from '../components/tabs/tabList'; import { formatMethodName } from '../components/tags/method-tag'; import { INSOMNIA_TAB_HEIGHT } from '../constant'; import { useAIContext } from '../context/app/ai-context'; -import { useInsomniaTab } from '../hooks/tab'; +import { useInsomniaTab } from '../hooks/use-insomnia-tab'; import { useActiveApiSpecSyncVCSVersion, useGitVCSVersion, diff --git a/packages/insomnia/src/ui/routes/environments.tsx b/packages/insomnia/src/ui/routes/environments.tsx index 9e33a4185b7..97596f4d297 100644 --- a/packages/insomnia/src/ui/routes/environments.tsx +++ b/packages/insomnia/src/ui/routes/environments.tsx @@ -19,7 +19,7 @@ import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder'; import { showAlert } from '../components/modals'; import { OrganizationTabList } from '../components/tabs/tabList'; import { INSOMNIA_TAB_HEIGHT } from '../constant'; -import { useInsomniaTab } from '../hooks/tab'; +import { useInsomniaTab } from '../hooks/use-insomnia-tab'; import { useOrganizationPermissions } from '../hooks/use-organization-features'; import type { WorkspaceLoaderData } from './workspace'; diff --git a/packages/insomnia/src/ui/routes/mock-server.tsx b/packages/insomnia/src/ui/routes/mock-server.tsx index c3614a2e8d5..568aadfad88 100644 --- a/packages/insomnia/src/ui/routes/mock-server.tsx +++ b/packages/insomnia/src/ui/routes/mock-server.tsx @@ -21,7 +21,7 @@ import { SvgIcon } from '../components/svg-icon'; import { OrganizationTabList } from '../components/tabs/tabList'; import { formatMethodName } from '../components/tags/method-tag'; import { INSOMNIA_TAB_HEIGHT } from '../constant'; -import { useInsomniaTab } from '../hooks/tab'; +import { useInsomniaTab } from '../hooks/use-insomnia-tab'; import { MockRouteResponse, MockRouteRoute, useMockRoutePatcher } from './mock-route'; import { useRootLoaderData } from './root'; import type { WorkspaceLoaderData } from './workspace'; diff --git a/packages/insomnia/src/ui/routes/runner.tsx b/packages/insomnia/src/ui/routes/runner.tsx index 9f1b2df2c0a..c2d7562f24a 100644 --- a/packages/insomnia/src/ui/routes/runner.tsx +++ b/packages/insomnia/src/ui/routes/runner.tsx @@ -4,7 +4,6 @@ import React, { type FC, useCallback, useEffect, useMemo, useRef, useState } fro import { Button, Checkbox, DropIndicator, GridList, GridListItem, type GridListItemProps, Heading, type Key, Tab, TabList, TabPanel, Tabs, Toolbar, TooltipTrigger, useDragAndDrop } from 'react-aria-components'; import { Panel, PanelResizeHandle } from 'react-resizable-panels'; import { type ActionFunction, type LoaderFunction, redirect, useNavigate, useParams, useRouteLoaderData, useSearchParams, useSubmit } from 'react-router-dom'; -import { useListData } from 'react-stately'; import { useInterval } from 'react-use'; import { v4 as uuidv4 } from 'uuid'; @@ -14,8 +13,6 @@ import type { ResponseTimelineEntry } from '../../main/network/libcurl-promise'; import type { TimingStep } from '../../main/network/request-timing'; import * as models from '../../models'; import type { UserUploadEnvironment } from '../../models/environment'; -import { isRequest, type Request } from '../../models/request'; -import { isRequestGroup } from '../../models/request-group'; import type { RunnerResultPerRequest, RunnerTestResult } from '../../models/runner-test-result'; import { cancelRequestById } from '../../network/cancellation'; import { invariant } from '../../utils/invariant'; @@ -33,10 +30,10 @@ import { RunnerTestResultPane } from '../components/panes/runner-test-result-pan import { ResponseTimer } from '../components/response-timer'; import { getTimeAndUnit } from '../components/tags/time-tag'; import { ResponseTimelineViewer } from '../components/viewers/response-timeline-viewer'; +import { useRunnerRequestList } from '../hooks/use-runner-request-list'; import type { OrganizationLoaderData } from './organization'; import { type CollectionRunnerContext, type RunnerSource, sendActionImplementation } from './request'; import { useRootLoaderData } from './root'; -import type { Child, WorkspaceLoaderData } from './workspace'; const inputStyle = 'placeholder:italic py-0.5 mr-1.5 px-1 w-24 rounded-sm border-2 border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors'; const iterationInputStyle = 'placeholder:italic py-0.5 mr-1.5 px-1 w-16 rounded-sm border-2 border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors'; @@ -101,7 +98,7 @@ export const repositionInArray = (allItems: string[], itemsToMove: string[], tar return items; }; -interface RequestRow { +export interface RequestRow { id: string; name: string; ancestorNames: string[]; @@ -112,16 +109,17 @@ interface RequestRow { }; export const Runner: FC<{}> = () => { - const [searchParams, setSearchParams] = useSearchParams(); - const [shouldRefresh, setShouldRefresh] = useState(false); + const [searchParams] = useSearchParams(); const [errorMsg, setErrorMsg] = useState(null); - const [targetFolderId, setTargetFolderId] = useState(null); const { currentPlan } = useRouteLoaderData('/organization') as OrganizationLoaderData; + const targetFolderId = searchParams.get('folder') || ''; + const shouldRefreshRef = useRef(false); - if (searchParams.has('refresh-pane') || searchParams.has('error') || searchParams.has('folder')) { + if (searchParams.has('refresh-pane') || searchParams.has('error')) { + console.log('searchParams', searchParams.toString()); if (searchParams.has('refresh-pane')) { - setShouldRefresh(true); + shouldRefreshRef.current = true; searchParams.delete('refresh-pane'); } @@ -143,15 +141,6 @@ export const Runner: FC<{}> = () => { } else { setErrorMsg(null); } - - if (searchParams.has('folder')) { - setTargetFolderId(searchParams.get('folder')); - searchParams.delete('folder'); - } else { - setTargetFolderId(null); - } - - setSearchParams({}); } const { organizationId, projectId, workspaceId } = useParams() as { @@ -170,10 +159,12 @@ export const Runner: FC<{}> = () => { invariant(iterationCount, 'iterationCount should not be null'); const { settings } = useRootLoaderData(); - const { collection } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; const [showUploadModal, setShowUploadModal] = useState(false); const [showCLIModal, setShowCLIModal] = useState(false); const [direction, setDirection] = useState<'horizontal' | 'vertical'>(settings.forceVerticalLayout ? 'vertical' : 'horizontal'); + + const { reqList, requestRows, entityMap } = useRunnerRequestList(workspaceId, targetFolderId); + useEffect(() => { if (settings.forceVerticalLayout) { setDirection('vertical'); @@ -195,55 +186,6 @@ export const Runner: FC<{}> = () => { } }, [settings.forceVerticalLayout, direction]); - const getEntityById = new Map(); - - const requestRows: RequestRow[] = collection - .filter(item => { - getEntityById.set(item.doc._id, item); - return isRequest(item.doc); - }) - .map((item: Child) => { - const ancestorNames: string[] = []; - const ancestorIds: string[] = []; - if (item.ancestors) { - item.ancestors.forEach(ancestorId => { - const ancestor = getEntityById.get(ancestorId); - if (ancestor && isRequestGroup(ancestor?.doc)) { - ancestorNames.push(ancestor?.doc.name); - ancestorIds.push(ancestor?.doc._id); - } - }); - } - - const requestDoc = item.doc as Request; - invariant('method' in item.doc, 'Only Request is supported at the moment'); - return { - id: item.doc._id, - name: item.doc.name, - ancestorNames, - ancestorIds, - method: requestDoc.method, - url: item.doc.url, - parentId: item.doc.parentId, - }; - }) - .filter(item => { - if (targetFolderId) { - return item.ancestorIds.includes(targetFolderId); - } - return true; - }); - - const reqList = useListData({ - initialItems: requestRows, - filter: item => { - if (targetFolderId) { - return item.ancestorIds.includes(targetFolderId); - } - return true; - }, - }); - const isConsistencyChanged = useMemo(() => { if (requestRows.length !== reqList.items.length) { return true; @@ -254,22 +196,10 @@ export const Runner: FC<{}> = () => { return requestRows.some((row: RequestRow, index: number) => row.id !== reqList.items[index].id); }, [requestRows, reqList]); - const previousWorkspaceId = useRef(''); - - useEffect(() => { - if (previousWorkspaceId.current && previousWorkspaceId.current !== workspaceId) { - // reset the list when workspace changes - const keys = reqList.items.map(item => item.id); - reqList.remove(...keys); - reqList.append(...requestRows); - } - previousWorkspaceId.current = workspaceId; - }, [reqList, requestRows, workspaceId]); - const { dragAndDropHooks: requestsDnD } = useDragAndDrop({ getItems: keys => { return [...keys].map(key => { - const name = getEntityById.get(key as string)?.doc.name || ''; + const name = entityMap.get(key as string)?.doc.name || ''; return { 'text/plain': key.toString(), name, @@ -424,14 +354,14 @@ export const Runner: FC<{}> = () => { unit: durationUnit, }); } else { - if (shouldRefresh) { + if (shouldRefreshRef.current) { const results = await models.runnerTestResult.findByParentId(workspaceId) || []; setTestHistory(results.reverse()); if (results.length > 0) { const latestResult = results[0]; setExecutionResult(latestResult); } - setShouldRefresh(false); + shouldRefreshRef.current = false; } } } diff --git a/packages/insomnia/src/ui/routes/unit-test.tsx b/packages/insomnia/src/ui/routes/unit-test.tsx index f48d1d78b94..c60b33d2098 100644 --- a/packages/insomnia/src/ui/routes/unit-test.tsx +++ b/packages/insomnia/src/ui/routes/unit-test.tsx @@ -50,7 +50,7 @@ import { CertificatesModal } from '../components/modals/workspace-certificates-m import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal'; import { OrganizationTabList } from '../components/tabs/tabList'; import { INSOMNIA_TAB_HEIGHT } from '../constant'; -import { useInsomniaTab } from '../hooks/tab'; +import { useInsomniaTab } from '../hooks/use-insomnia-tab'; import { useRootLoaderData } from './root'; import { TestRunStatus } from './test-results'; import TestSuiteRoute from './test-suite'; From e133d3c24217e722c37a56bcd43d575addf0874d Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 28 Nov 2024 17:47:05 +0800 Subject: [PATCH 28/34] feat: use different tab if for collection runner and folder runner --- .../insomnia/src/ui/hooks/use-insomnia-tab.ts | 21 ++++++++++++++----- .../src/ui/hooks/use-runner-request-list.tsx | 1 - 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts b/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts index bb93478e5b7..8bc6f06d71b 100644 --- a/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts +++ b/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect } from 'react'; -import { matchPath, useLocation } from 'react-router-dom'; +import { matchPath, useLocation, useSearchParams } from 'react-router-dom'; import type { GrpcRequest } from '../../models/grpc-request'; import type { MockRoute } from '../../models/mock-route'; @@ -40,6 +40,7 @@ export const useInsomniaTab = ({ const { appTabsRef, addTab, changeActiveTab } = useInsomniaTabContext(); const location = useLocation(); + const [searchParams] = useSearchParams(); const generateTabUrl = useCallback((type: TabEnum) => { if (type === TabEnum.Request) { @@ -98,6 +99,14 @@ export const useInsomniaTab = ({ return null; }; + const getRunnerTabId = useCallback(() => { + const folderId = searchParams.get('folder'); + if (folderId) { + return `runner_${folderId}`; + } + return `runner_${workspaceId}`; + }, [searchParams, workspaceId]); + const getCurrentTab = useCallback((type: TabEnum | null) => { if (!type) { return undefined; @@ -113,7 +122,8 @@ export const useInsomniaTab = ({ if (type === TabEnum.Runner) { // collection runner tab id is prefixed with 'runner_' - return currentOrgTabs?.tabList.find(tab => tab.id === `runner_${workspaceId}`); + const runnerTabId = getRunnerTabId(); + return currentOrgTabs?.tabList.find(tab => tab.id === runnerTabId); } if (type === TabEnum.MockRoute) { @@ -128,7 +138,7 @@ export const useInsomniaTab = ({ return currentOrgTabs?.tabList.find(tab => tab.id === workspaceId); } return undefined; - }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, appTabsRef, organizationId, unitTestSuite?._id, workspaceId]); + }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, appTabsRef, getRunnerTabId, organizationId, unitTestSuite?._id, workspaceId]); const getTabId = useCallback((type: TabEnum | null): string => { if (!type) { @@ -143,7 +153,8 @@ export const useInsomniaTab = ({ } if (type === TabEnum.Runner) { - return `runner_${workspaceId}`; + const runnerTabId = getRunnerTabId(); + return runnerTabId; } if (type === TabEnum.MockRoute) { @@ -159,7 +170,7 @@ export const useInsomniaTab = ({ } return ''; - }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, unitTestSuite?._id, workspaceId]); + }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, getRunnerTabId, unitTestSuite?._id, workspaceId]); const packTabInfo = useCallback((type: TabEnum): BaseTab | undefined => { if (!type) { diff --git a/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx b/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx index 962ad184800..785bf68b422 100644 --- a/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx +++ b/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx @@ -61,7 +61,6 @@ export const useRunnerRequestList = (workspaceId: string, targetFolderId: string useEffect(() => { if ((previousWorkspaceId && previousWorkspaceId !== workspaceId) || (previousTargetFolderId !== undefined && previousTargetFolderId !== targetFolderId)) { - console.log('reset list'); // reset the list when workspace changes const keys = reqList.items.map(item => item.id); reqList.remove(...keys); From cf414829aceec8584aa84207fd8df5f6bafc0e77 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Mon, 2 Dec 2024 15:10:44 +0800 Subject: [PATCH 29/34] fix: update tab data after move request or folder --- packages/insomnia/src/common/database.ts | 14 +-- .../modals/request-group-settings-modal.tsx | 3 + .../modals/request-settings-modal.tsx | 3 + .../src/ui/components/tabs/tabList.tsx | 102 ++++++++++++++---- .../ui/context/app/insomnia-tab-context.tsx | 38 +++++-- packages/insomnia/src/ui/routes/workspace.tsx | 18 ++++ 6 files changed, 142 insertions(+), 36 deletions(-) diff --git a/packages/insomnia/src/common/database.ts b/packages/insomnia/src/common/database.ts index 79f74a3754d..c493e7ac600 100644 --- a/packages/insomnia/src/common/database.ts +++ b/packages/insomnia/src/common/database.ts @@ -116,7 +116,7 @@ export const database = { }, ...patches, ); - return database.update(doc); + return database.update(doc, false, patches); }, /** duplicate doc and its decendents recursively */ @@ -524,7 +524,7 @@ export const database = { notifyOfChange('remove', doc, fromSync); }, - update: async function(doc: T, fromSync = false) { + update: async function (doc: T, fromSync = false, patches: Patch[] = []) { if (db._empty) { return _send('update', ...arguments); } @@ -550,7 +550,7 @@ export const database = { resolve(docWithDefaults); // NOTE: This needs to be after we resolve - notifyOfChange('update', docWithDefaults, fromSync); + notifyOfChange('update', docWithDefaults, fromSync, patches); }, ); }); @@ -698,7 +698,8 @@ let bufferChangesId = 1; export type ChangeBufferEvent = [ event: ChangeType, doc: T, - fromSync: boolean + fromSync: boolean, + patches: Patch[], ]; let changeBuffer: ChangeBufferEvent[] = []; @@ -709,10 +710,11 @@ let changeListeners: ChangeListener[] = []; /** push changes into the buffer, so that changeListeners can get change contents when database.flushChanges is called, * this method should be called whenever a document change happens */ -async function notifyOfChange(event: ChangeType, doc: T, fromSync: boolean) { +async function notifyOfChange(event: ChangeType, doc: T, fromSync: boolean, patches: Patch[] = []) { const updatedDoc = doc; - changeBuffer.push([event, updatedDoc, fromSync]); + // TODO: Use object is better than array + changeBuffer.push([event, updatedDoc, fromSync, patches]); // Flush right away if we're not buffering if (!bufferingChanges) { diff --git a/packages/insomnia/src/ui/components/modals/request-group-settings-modal.tsx b/packages/insomnia/src/ui/components/modals/request-group-settings-modal.tsx index 5f4d71eb29d..f2692fe9c97 100644 --- a/packages/insomnia/src/ui/components/modals/request-group-settings-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/request-group-settings-modal.tsx @@ -7,6 +7,7 @@ import type { RequestGroup } from '../../../models/request-group'; import { invariant } from '../../../utils/invariant'; import { useRequestGroupPatcher } from '../../hooks/use-request'; import type { ListWorkspacesLoaderData } from '../../routes/project'; +import { revalidateWorkspaceActiveRequestByFolder } from '../../routes/workspace'; import { Modal, type ModalHandle, type ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; import { ModalHeader } from '../base/modal-header'; @@ -49,6 +50,8 @@ export const RequestGroupSettingsModal = ({ requestGroup, onHide }: ModalProps & const handleMoveToWorkspace = async () => { invariant(workspaceToCopyTo, 'Workspace ID is required'); patchRequestGroup(requestGroup._id, { parentId: workspaceToCopyTo }); + // if the folder is moved to a different workspace, we need to revalidate the active request + revalidateWorkspaceActiveRequestByFolder(requestGroup, workspaceId); modalRef.current?.hide(); navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceToCopyTo}/debug`); }; diff --git a/packages/insomnia/src/ui/components/modals/request-settings-modal.tsx b/packages/insomnia/src/ui/components/modals/request-settings-modal.tsx index 8a376337936..5a8b513d431 100644 --- a/packages/insomnia/src/ui/components/modals/request-settings-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/request-settings-modal.tsx @@ -11,6 +11,7 @@ import { isWebSocketRequest, type WebSocketRequest } from '../../../models/webso import { invariant } from '../../../utils/invariant'; import { useRequestPatcher } from '../../hooks/use-request'; import type { ListWorkspacesLoaderData } from '../../routes/project'; +import { revalidateWorkspaceActiveRequest } from '../../routes/workspace'; import { Modal, type ModalHandle, type ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; import { ModalHeader } from '../base/modal-header'; @@ -52,6 +53,8 @@ export const RequestSettingsModal = ({ request, onHide }: ModalProps & RequestSe async function handleMoveToWorkspace() { invariant(workspaceToCopyTo, 'Workspace ID is required'); patchRequest(request._id, { parentId: workspaceToCopyTo }); + // if active request is moved, clear the active request in the workspace + revalidateWorkspaceActiveRequest(request._id, workspaceId); modalRef.current?.hide(); navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceToCopyTo}/debug`); } diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 7439a7f3bb7..4c1d9e171bc 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -1,12 +1,13 @@ import _ from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; import { Button, GridList, Menu, MenuItem, MenuTrigger, Popover, type Selection } from 'react-aria-components'; -import { useFetcher, useNavigate } from 'react-router-dom'; +import { useFetcher, useNavigate, useParams } from 'react-router-dom'; import { type ChangeBufferEvent, type ChangeType, database } from '../../../common/database'; import * as models from '../../../models/index'; import type { MockRoute } from '../../../models/mock-route'; -import type { Request } from '../../../models/request'; +import { isRequest, type Request } from '../../../models/request'; +import { isRequestGroup } from '../../../models/request-group'; import { INSOMNIA_TAB_HEIGHT } from '../../constant'; import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; import { type Size, useResizeObserver } from '../../hooks/use-resize-observer'; @@ -39,7 +40,7 @@ export const TAB_ROUTER_PATH: Record = { }; export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' }) => { - const { currentOrgTabs } = useInsomniaTabContext(); + const { currentOrgTabs, batchUpdateTabs } = useInsomniaTabContext(); const { tabList, activeTabId } = currentOrgTabs; const navigate = useNavigate(); @@ -49,6 +50,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' const [rightScrollDisable, setRightScrollDisable] = useState(false); const requestFetcher = useFetcher(); + const { organizationId, projectId } = useParams(); const { changeActiveTab, @@ -105,26 +107,79 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' } }, [closeAllTabsUnderProject, closeAllTabsUnderWorkspace, closeTabById]); - const handleUpdate = useCallback((doc: models.BaseModel) => { - // currently have 2 types of update, rename and change request method - if (doc.type === models.request.type || doc.type === models.grpcRequest.type || doc.type === models.webSocketRequest.type) { - const tag = getRequestMethodShortHand(doc as Request); - const method = (doc as Request).method; - updateTabById?.(doc._id, doc.name, method, tag); - } else if (doc.type === models.mockRoute.type) { - const method = (doc as MockRoute).method; - const tag = formatMethodName(method); - updateTabById?.(doc._id, doc.name, method, tag); - } else if (doc.type === models.project.type) { - // update project name(for tooltip) - updateProjectName?.(doc._id, doc.name); - } else if (doc.type === models.workspace.type) { - // update workspace name(for tooltip) & update name for workspace tab - updateWorkspaceName?.(doc._id, doc.name); - } else { - updateTabById?.(doc._id, doc.name); + const handleUpdate = useCallback(async (doc: models.BaseModel, patches: Partial[]) => { + const patchObj: Record = {}; + patches.forEach(patch => { + Object.assign(patchObj, patch); + }); + // only need to handle name, method, parentId change + if (!patchObj.name && !patchObj.method && !patchObj.parentId) { + return; } - }, [updateProjectName, updateTabById, updateWorkspaceName]); + if (patchObj.name) { + if (doc.type === models.project.type) { + // update project name(for tooltip) + updateProjectName?.(doc._id, doc.name); + } else if (doc.type === models.workspace.type) { + // update workspace name(for tooltip) & update name for workspace tab + updateWorkspaceName?.(doc._id, doc.name); + } else { + updateTabById?.(doc._id, { + name: doc.name, + }); + } + } + + if (patchObj.method) { + if (doc.type === models.request.type || doc.type === models.grpcRequest.type || doc.type === models.webSocketRequest.type) { + const tag = getRequestMethodShortHand(doc as Request); + const method = (doc as Request).method; + updateTabById?.(doc._id, { + method, + tag, + }); + } else if (doc.type === models.mockRoute.type) { + const method = (doc as MockRoute).method; + const tag = formatMethodName(method); + updateTabById?.(doc._id, { + method, + tag, + }); + } + } + + // move request or requestGroup to another collection + if (patchObj.parentId && !patchObj.metaSortKey && (patchObj.parentId as string).startsWith('wrk_')) { + const workspace = await models.workspace.getById(patchObj.parentId); + if (workspace) { + if (isRequest(doc)) { + debugger; + updateTabById?.(doc._id, { + workspaceId: workspace._id, + workspaceName: workspace.name, + url: `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/debug/request/${doc._id}`, + }); + } else if (isRequestGroup(doc)) { + const folderEntities = await database.withDescendants(doc, models.request.type, [models.request.type, models.requestGroup.type]); + console.log('folderEntities:', folderEntities); + const batchUpdates = [doc, ...folderEntities].map(entity => { + return { + id: entity._id, + fields: { + workspaceId: workspace._id, + workspaceName: workspace.name, + url: isRequestGroup(entity) + ? `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/debug/request-group/${entity._id}` + : `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/debug/request/${entity._id}`, + }, + }; + }); + batchUpdateTabs?.(batchUpdates); + } + } + } + + }, [organizationId, projectId, updateProjectName, updateTabById, updateWorkspaceName, batchUpdateTabs]); useEffect(() => { // sync tabList with database @@ -136,7 +191,8 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' if (changeType === 'remove') { handleDelete(doc._id, doc.type); } else if (changeType === 'update') { - handleUpdate(doc); + const patches = change[3]; + handleUpdate(doc, patches); } } } diff --git a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx index ee20784de5f..51a143bea3f 100644 --- a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -21,7 +21,8 @@ interface ContextProps { closeAllTabsUnderProject?: (projectId: string) => void; updateProjectName?: (projectId: string, name: string) => void; updateWorkspaceName?: (projectId: string, name: string) => void; - updateTabById?: (tabId: string, name: string, method?: string, tag?: string) => void; + updateTabById?: (tabId: string, patches: Partial) => void; + batchUpdateTabs?: (updates: { id: string; fields: Partial }[]) => void; closeAllTabs?: () => void; closeOtherTabs?: (id: string) => void; } @@ -169,7 +170,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { }); }, [navigate, organizationId, updateInsomniaTabs]); - const updateTabById = useCallback((tabId: string, name: string, method: string = '', tag: string = '') => { + const updateTabById = useCallback((tabId: string, patches: Partial) => { const currentTabs = appTabsRef?.current?.[organizationId]; if (!currentTabs) { return; @@ -178,9 +179,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { if (tab.id === tabId) { return { ...tab, - name, - tag, - method, + ...patches, }; } return tab; @@ -248,6 +247,30 @@ export const InsomniaTabProvider: FC = ({ children }) => { }); }, [organizationId, updateInsomniaTabs]); + const batchUpdateTabs = useCallback((updates: { id: string; fields: Partial }[]) => { + const currentTabs = appTabsRef?.current?.[organizationId]; + if (!currentTabs) { + return; + } + + const newTabList = currentTabs.tabList.map(tab => { + const update = updates.find(update => update.id === tab.id); + if (update) { + return { + ...tab, + ...update.fields, + }; + } + return tab; + }); + + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: currentTabs.activeTabId || '', + }); + }, [organizationId, updateInsomniaTabs]); + return ( = ({ children }) => { closeTabById, closeAllTabsUnderWorkspace, closeAllTabsUnderProject, + closeAllTabs, + closeOtherTabs, addTab, updateTabById, changeActiveTab, updateProjectName, updateWorkspaceName, + batchUpdateTabs, appTabsRef, - closeAllTabs, - closeOtherTabs, }} > {children} diff --git a/packages/insomnia/src/ui/routes/workspace.tsx b/packages/insomnia/src/ui/routes/workspace.tsx index 8bc97ffea36..2926753f614 100644 --- a/packages/insomnia/src/ui/routes/workspace.tsx +++ b/packages/insomnia/src/ui/routes/workspace.tsx @@ -315,6 +315,24 @@ export const workspaceLoader: LoaderFunction = async ({ }; }; +export const revalidateWorkspaceActiveRequest = async (requestId: string, workspaceId: string) => { + const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId); + if (workspaceMeta?.activeRequestId === requestId) { + await models.workspaceMeta.update(workspaceMeta, { activeRequestId: null }); + } +}; + +export const revalidateWorkspaceActiveRequestByFolder = async (requestGroup: RequestGroup, workspaceId: string) => { + const docs = await database.withDescendants(requestGroup, models.request.type, [models.request.type, models.requestGroup.type]); + const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId); + for (const doc of docs) { + if (workspaceMeta?.activeRequestId === doc._id) { + await models.workspaceMeta.update(workspaceMeta, { activeRequestId: null }); + return; + } + } +}; + const WorkspaceRoute = () => { const { activeWorkspace } = useLoaderData() as WorkspaceLoaderData; From eb0314c36a1d57ef994fca1f6c785ad83d77be1a Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Mon, 2 Dec 2024 15:38:35 +0800 Subject: [PATCH 30/34] fix: database test --- .../src/common/__tests__/database.test.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/insomnia/src/common/__tests__/database.test.ts b/packages/insomnia/src/common/__tests__/database.test.ts index 7fcc543773e..82ae96358af 100644 --- a/packages/insomnia/src/common/__tests__/database.test.ts +++ b/packages/insomnia/src/common/__tests__/database.test.ts @@ -37,8 +37,8 @@ describe('onChange()', () => { name: 'bar', }); expect(changesSeen).toEqual([ - [['insert', newDoc, false]], - [['update', updatedDoc, false]], + [['insert', newDoc, false, []]], + [['update', updatedDoc, false, [{ name: 'bar' }]]], ]); db.offChange(callback); await models.request.create(doc); @@ -71,16 +71,16 @@ describe('bufferChanges()', () => { await db.flushChanges(); expect(changesSeen).toEqual([ [ - ['insert', newDoc, false], - ['update', updatedDoc, false], + ['insert', newDoc, false, []], + ['update', updatedDoc, false, [true]], ], ]); // Assert no more changes seen after flush again await db.flushChanges(); expect(changesSeen).toEqual([ [ - ['insert', newDoc, false], - ['update', updatedDoc, false], + ['insert', newDoc, false, []], + ['update', updatedDoc, false, [true]], ], ]); }); @@ -106,8 +106,8 @@ describe('bufferChanges()', () => { await new Promise(resolve => setTimeout(resolve, 1500)); expect(changesSeen).toEqual([ [ - ['insert', newDoc, false], - ['update', updatedDoc, false], + ['insert', newDoc, false, []], + ['update', updatedDoc, false, [true]], ], ]); }); @@ -132,8 +132,8 @@ describe('bufferChanges()', () => { await new Promise(resolve => setTimeout(resolve, 1000)); expect(changesSeen).toEqual([ [ - ['insert', newDoc, false], - ['update', updatedDoc, false], + ['insert', newDoc, false, []], + ['update', updatedDoc, false, [true]], ], ]); }); @@ -166,8 +166,8 @@ describe('bufferChangesIndefinitely()', () => { await db.flushChanges(); expect(changesSeen).toEqual([ [ - ['insert', newDoc, false], - ['update', updatedDoc, false], + ['insert', newDoc, false, []], + ['update', updatedDoc, false, [true]], ], ]); }); From 85b6ac45cb4f529ca3e119541c661c36db945765 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Tue, 3 Dec 2024 10:42:17 +0800 Subject: [PATCH 31/34] feat: support drag and drop --- .../insomnia/src/ui/components/tabs/tab.tsx | 1 + .../src/ui/components/tabs/tabList.tsx | 25 ++++++++++- .../ui/context/app/insomnia-tab-context.tsx | 42 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index 33bdc2edd81..605308462a1 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -151,6 +151,7 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => {
+
-
@@ -344,20 +344,18 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' selectionBehavior='replace' className="flex h-[41px] w-fit" dragAndDropHooks={dragAndDropHooks} - // Use +1 height to mask the wrapper border, and let the custom element in InsomniaTab act as the fake border.(we need different border for active tab) - style={{ height: `${INSOMNIA_TAB_HEIGHT + 1}px` }} items={tabList} ref={tabListInnerRef} > {item => }
- -
+
- From 91e3eca88365086dea2cd2ed940ed2d0b987ffbd Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 5 Dec 2024 16:11:43 +0800 Subject: [PATCH 33/34] cr feedback --- .../src/ui/components/document-tab.tsx | 11 ++-- .../src/ui/components/tabs/tabList.tsx | 5 +- .../ui/context/app/insomnia-tab-context.tsx | 10 +++- .../insomnia/src/ui/hooks/use-insomnia-tab.ts | 2 +- packages/insomnia/src/ui/routes/runner.tsx | 56 ++++++++++--------- 5 files changed, 44 insertions(+), 40 deletions(-) diff --git a/packages/insomnia/src/ui/components/document-tab.tsx b/packages/insomnia/src/ui/components/document-tab.tsx index e9e5711e2b7..ee2d7220744 100644 --- a/packages/insomnia/src/ui/components/document-tab.tsx +++ b/packages/insomnia/src/ui/components/document-tab.tsx @@ -1,3 +1,4 @@ +import classnames from 'classnames'; import React from 'react'; import { NavLink } from 'react-router-dom'; @@ -19,12 +20,10 @@ export const DocumentTab = ({ organizationId, projectId, workspaceId, className - `${isActive - ? 'text-[--color-font] bg-[--color-surprise]' - : '' - } ${isPending ? 'animate-pulse' : ''} text-center rounded-full px-2` - } + className={({ isActive, isPending }) => classnames('text-center rounded-full px-2', { + 'text-[--color-font] bg-[--color-surprise]': isActive, + 'animate-pulse': isPending, + })} data-testid={`workspace-${item.id}`} > {item.name} diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 6487cc37e25..cc7e283afc0 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; import { Button, DropIndicator, GridList, Menu, MenuItem, MenuTrigger, Popover, type Selection, useDragAndDrop } from 'react-aria-components'; -import { useFetcher, useNavigate, useParams } from 'react-router-dom'; +import { useFetcher, useParams } from 'react-router-dom'; import { type ChangeBufferEvent, type ChangeType, database } from '../../../common/database'; import * as models from '../../../models/index'; @@ -42,7 +42,6 @@ export const TAB_ROUTER_PATH: Record = { export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' }) => { const { currentOrgTabs, batchUpdateTabs, moveBefore, moveAfter } = useInsomniaTabContext(); const { tabList, activeTabId } = currentOrgTabs; - const navigate = useNavigate(); const [showAddRequestModal, setShowAddRequestModal] = useState(false); const [isOverFlow, setIsOverFlow] = useState(false); @@ -67,8 +66,6 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' const handleSelectionChange = (keys: Selection) => { if (keys !== 'all') { const key = [...keys.values()]?.[0] as string; - const tab = tabList.find(tab => tab.id === key); - tab?.url && navigate(tab?.url); changeActiveTab(key); } }; diff --git a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx index 43421a8e8f9..2a1f99baf69 100644 --- a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -106,12 +106,13 @@ export const InsomniaTabProvider: FC = ({ children }) => { } const newTabList = currentTabs.tabList.filter(tab => tab.id !== id); if (currentTabs.activeTabId === id) { - navigate(newTabList[index - 1 < 0 ? 0 : index - 1]?.url || ''); + const url = newTabList[Math.max(index - 1, 0)]?.url; + navigate(url); } updateInsomniaTabs({ organizationId, tabList: newTabList, - activeTabId: currentTabs.activeTabId === id ? newTabList[index - 1 < 0 ? 0 : index - 1]?.id : currentTabs.activeTabId as string, + activeTabId: currentTabs.activeTabId === id ? newTabList[Math.max(index - 1, 0)]?.id : currentTabs.activeTabId as string, }); }, [navigate, organizationId, projectId, updateInsomniaTabs]); @@ -198,12 +199,15 @@ export const InsomniaTabProvider: FC = ({ children }) => { if (!currentTabs) { return; } + const tab = currentTabs?.tabList.find(tab => tab.id === id); + // keep the search params when navigate to another tab + tab?.url && navigate(tab.url); updateInsomniaTabs({ organizationId, tabList: currentTabs.tabList, activeTabId: id, }); - }, [organizationId, updateInsomniaTabs]); + }, [navigate, organizationId, updateInsomniaTabs]); const updateProjectName = useCallback((projectId: string, name: string) => { const currentTabs = appTabsRef?.current?.[organizationId]; diff --git a/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts b/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts index 8bc6f06d71b..7bade7f96c5 100644 --- a/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts +++ b/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts @@ -285,5 +285,5 @@ export const useInsomniaTab = ({ changeActiveTab(currentTab.id); } } - }, [addTab, appTabsRef, changeActiveTab, getCurrentTab, location.pathname, location.search, organizationId, packTabInfo]); + }, [addTab, appTabsRef, changeActiveTab, getCurrentTab, location.pathname, organizationId, packTabInfo]); }; diff --git a/packages/insomnia/src/ui/routes/runner.tsx b/packages/insomnia/src/ui/routes/runner.tsx index c2d7562f24a..c87679692ff 100644 --- a/packages/insomnia/src/ui/routes/runner.tsx +++ b/packages/insomnia/src/ui/routes/runner.tsx @@ -1,4 +1,4 @@ -import type { RequestContext } from 'insomnia-sdk'; +import { type RequestContext } from 'insomnia-sdk'; import porderedJSON from 'json-order'; import React, { type FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Checkbox, DropIndicator, GridList, GridListItem, type GridListItemProps, Heading, type Key, Tab, TabList, TabPanel, Tabs, Toolbar, TooltipTrigger, useDragAndDrop } from 'react-aria-components'; @@ -109,39 +109,43 @@ export interface RequestRow { }; export const Runner: FC<{}> = () => { - const [searchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const [errorMsg, setErrorMsg] = useState(null); const { currentPlan } = useRouteLoaderData('/organization') as OrganizationLoaderData; const targetFolderId = searchParams.get('folder') || ''; const shouldRefreshRef = useRef(false); - if (searchParams.has('refresh-pane') || searchParams.has('error')) { - console.log('searchParams', searchParams.toString()); - if (searchParams.has('refresh-pane')) { - shouldRefreshRef.current = true; - searchParams.delete('refresh-pane'); - } + useEffect(() => { + if (searchParams.has('refresh-pane') || searchParams.has('error')) { + const copySearchParams = new URLSearchParams(searchParams); + if (searchParams.has('refresh-pane')) { + shouldRefreshRef.current = true; + copySearchParams.delete('refresh-pane'); + } - if (searchParams.has('error')) { - setErrorMsg(searchParams.get('error')); - // TODO: this should be removed when we are able categorized errors better and display them in different ways. - showAlert({ - title: 'Unexpected Runner Failure', - message: ( -
-

The runner failed due to an unhandled error:

- -
{searchParams.get('error')}
-
-
- ), - }); - searchParams.delete('error'); - } else { - setErrorMsg(null); + if (searchParams.has('error')) { + setErrorMsg(searchParams.get('error')); + // TODO: this should be removed when we are able categorized errors better and display them in different ways. + showAlert({ + title: 'Unexpected Runner Failure', + message: ( +
+

The runner failed due to an unhandled error:

+ +
{searchParams.get('error')}
+
+
+ ), + }); + copySearchParams.delete('error'); + } else { + setErrorMsg(null); + } + + setSearchParams(copySearchParams); } - } + }, [searchParams, setSearchParams]); const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; From f4cbf3c3659d62e134c14b13e1a15b46fe371302 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Mon, 9 Dec 2024 15:37:23 +0800 Subject: [PATCH 34/34] fix tab update --- packages/insomnia/src/ui/components/tabs/tabList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index cc7e283afc0..6c1885e0152 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -104,7 +104,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' } }, [closeAllTabsUnderProject, closeAllTabsUnderWorkspace, closeTabById]); - const handleUpdate = useCallback(async (doc: models.BaseModel, patches: Partial[]) => { + const handleUpdate = useCallback(async (doc: models.BaseModel, patches: Partial[] = []) => { const patchObj: Record = {}; patches.forEach(patch => { Object.assign(patchObj, patch); @@ -181,6 +181,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' useEffect(() => { // sync tabList with database const callback = async (changes: ChangeBufferEvent[]) => { + console.log('tabList changes:', changes); for (const change of changes) { const changeType = change[0]; const doc = change[1];