From 7c5f285f32200cc8442947a14d8ed307fa8675aa 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/47] 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 ( + + {[ + { id: 'spec', name: 'Spec' }, + { id: 'debug', name: 'Collection' }, + { id: 'test', name: 'Tests' }, + ].map(item => ( + + `${isActive + ? 'text-[--color-font] bg-[--color-surprise]' + : '' + } ${isPending ? 'animate-pulse' : ''} text-center rounded-full px-2` + } + data-testid={`workspace-${item.id}`} + > + {item.name} + + ))} + + ); +}; 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 && ( + handleClose(tab.id)}> + + + )} + + {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 f16443cdc92..95e81ee616c 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)} /> - setIsCookieModalOpen(true)} - className="px-4 py-1 max-w-full truncate flex-1 flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm" - > + setIsCookieModalOpen(true)} + className="px-4 py-1 max-w-full truncate flex-1 flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm" + > {activeCookieJar.cookies.length === 0 ? 'Add' : 'Manage'} Cookies {activeCookieJar.cookies.length > 0 ? `(${activeCookieJar.cookies.length})` : ''} - - setCertificatesModalOpen(true)} - className="px-4 py-1 max-w-full truncate flex-1 flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm" - > + + setCertificatesModalOpen(true)} + className="px-4 py-1 max-w-full truncate flex-1 flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm" + > {clientCertificates.length === 0 || caCertificate ? 'Add' : 'Manage'} Certificates {[...clientCertificates, caCertificate].filter(cert => !cert?.disabled).filter(isNotNullOrUndefined).length > 0 ? `(${[...clientCertificates, caCertificate].filter(cert => !cert?.disabled).filter(isNotNullOrUndefined).length})` : ''} - - + + 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 f9231e13e5c..ba9defb9fe1 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 6cee3c8efd7..556b363fdc7 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) && ( - - {[ - { id: 'spec', name: 'spec' }, - { name: 'collection', id: 'debug' }, - { id: 'test', name: 'tests' }, - ].map(item => ( - - `${isActive - ? 'text-[--color-font] bg-[--color-bg]' - : '' - } ${isPending ? 'animate-pulse' : ''} no-underline transition-colors text-center outline-none min-w-[4rem] uppercase text-[--color-font] text-xs px-[--padding-xs] py-[--padding-xxs] rounded-full` - } - data-testid={`workspace-${item.id}`} - > - {item.name} - - ))} - - )} - + {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 0f777aeeaa5..fe432426769 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 = () => { - + { @@ -1197,6 +1198,7 @@ const ProjectRoute: FC = () => { + {activeProject ? ( {billing.isActive ? null : @@ -1399,9 +1401,6 @@ const ProjectRoute: FC = () => { {item.label} - {/* {(item.hasUncommittedChanges || item.hasUnpushedChanges) && - - } */} {item.presence.length > 0 && ( { const { activeProject, + activeWorkspace, activeCookieJar, caCertificate, clientCertificates, } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; + const { unitTestSuite } = useRouteLoaderData( + ':testSuiteId' + ) as { unitTestSuite: UnitTestSuite } || {}; + const [isCookieModalOpen, setIsCookieModalOpen] = useState(false); const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false); const [isEnvironmentPickerOpen, setIsEnvironmentPickerOpen] = useState(false); @@ -283,6 +292,19 @@ const TestRoute: FC = () => { } }, [settings.forceVerticalLayout, direction]); + const { organizations } = useOrganizationLoaderData(); + const activeOrganization = organizations.find(o => o.id === organizationId); + + useInsomniaTab({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeOrganization, + unitTestSuite, + }); + return ( @@ -304,6 +326,11 @@ const TestRoute: FC = () => { + { + From 0c442c19f2f0d39e7ae62f09881c50a16a4994bc Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Wed, 6 Nov 2024 11:44:59 +0800 Subject: [PATCH 02/47] fix: lint error --- packages/insomnia/src/ui/routes/organization.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/insomnia/src/ui/routes/organization.tsx b/packages/insomnia/src/ui/routes/organization.tsx index 537403f8fb1..f06a1e8ab96 100644 --- a/packages/insomnia/src/ui/routes/organization.tsx +++ b/packages/insomnia/src/ui/routes/organization.tsx @@ -35,7 +35,7 @@ import { userSession } from '../../models'; import { updateLocalProjectToRemote } from '../../models/helpers/project'; import { findPersonalOrganization, isOwnerOfOrganization, isPersonalOrganization, isScratchpadOrganizationId, type Organization } from '../../models/organization'; import { type Project, type as ProjectType } from '../../models/project'; -import { isDesign, isScratchpad } from '../../models/workspace'; +import { isScratchpad } from '../../models/workspace'; import { VCSInstance } from '../../sync/vcs/insomnia-sync'; import { migrateProjectsIntoOrganization, shouldMigrateProjectUnderOrganization } from '../../sync/vcs/migrate-projects-into-organization'; import { insomniaFetch } from '../../ui/insomniaFetch'; @@ -519,7 +519,7 @@ const OrganizationRoute = () => { isScratchpad(workspaceData.activeWorkspace); const isScratchPadBannerVisible = !isScratchPadBannerDismissed && isScratchpadWorkspace; const untrackedProjectsFetcher = useFetcher(); - const { organizationId, projectId, workspaceId } = useParams() as { + const { organizationId, projectId } = useParams() as { organizationId: string; projectId?: string; workspaceId?: string; From e8d0cd1e0d3b58cfbb265459d0021a1a268c1284 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Wed, 6 Nov 2024 11:56:11 +0800 Subject: [PATCH 03/47] fix: type error --- packages/insomnia/src/ui/routes/mock-server.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/insomnia/src/ui/routes/mock-server.tsx b/packages/insomnia/src/ui/routes/mock-server.tsx index 556b363fdc7..4df4e558656 100644 --- a/packages/insomnia/src/ui/routes/mock-server.tsx +++ b/packages/insomnia/src/ui/routes/mock-server.tsx @@ -191,7 +191,6 @@ const MockServerRoute = () => { activeProject, activeWorkspace, activeOrganization, - mockRouteId, activeMockRoute: mockRoutes.find(s => s._id === mockRouteId), }); From d4afe9e9fac61a267db654c1012c7d9b73582ced Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 7 Nov 2024 11:26:37 +0800 Subject: [PATCH 04/47] fix: ui border align --- packages/insomnia/src/ui/components/tabs/tabList.tsx | 3 ++- packages/insomnia/src/ui/constant.ts | 3 +++ packages/insomnia/src/ui/hooks/tab.ts | 6 ++++++ packages/insomnia/src/ui/routes/debug.tsx | 3 ++- packages/insomnia/src/ui/routes/design.tsx | 7 +++---- packages/insomnia/src/ui/routes/environments.tsx | 3 ++- packages/insomnia/src/ui/routes/mock-server.tsx | 3 ++- packages/insomnia/src/ui/routes/unit-test.tsx | 3 ++- 8 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 packages/insomnia/src/ui/constant.ts diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 29e22f323f9..6c290a77ca4 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { GridList, type Key, type Selection } from 'react-aria-components'; import { useNavigate } from 'react-router-dom'; +import { INSOMNIA_TAB_HEIGHT } from '../../constant'; import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; import { Icon } from '../icon'; import { type BaseTab, InsomniaTab, TabEnum } from './tab'; @@ -57,7 +58,7 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { defaultSelectedKeys={['req_737492dce0c3460a8a55762e5d1bbd99']} selectionMode="single" selectionBehavior='replace' - className="flex h-[40px]" + className={`flex h-[${INSOMNIA_TAB_HEIGHT}] w-[calc(100%-50px)] overflow-x-scroll hide-scrollbars`} items={tabList} > {item => } diff --git a/packages/insomnia/src/ui/constant.ts b/packages/insomnia/src/ui/constant.ts new file mode 100644 index 00000000000..d4745361851 --- /dev/null +++ b/packages/insomnia/src/ui/constant.ts @@ -0,0 +1,3 @@ +// this a constant file just for renderer process + +export const INSOMNIA_TAB_HEIGHT = '40px'; diff --git a/packages/insomnia/src/ui/hooks/tab.ts b/packages/insomnia/src/ui/hooks/tab.ts index 1874ebbe1c0..798a39af77a 100644 --- a/packages/insomnia/src/ui/hooks/tab.ts +++ b/packages/insomnia/src/ui/hooks/tab.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect } from 'react'; import { matchPath, useLocation } from 'react-router-dom'; +import { database } from '../../common/database'; import type { GrpcRequest } from '../../models/grpc-request'; import type { MockRoute } from '../../models/mock-route'; import type { Organization } from '../../models/organization'; @@ -289,4 +290,9 @@ export const useInsomniaTab = ({ } }, [addTab, appTabsRef, changeActiveTab, getCurrentTab, location.pathname, organizationId, packTabInfo]); + useEffect(() => { + database.onChange(async (e) => { + console.log('database change', e); + }); + }, []); }; diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 95e81ee616c..95fdb2c1463 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -103,6 +103,7 @@ 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 { INSOMNIA_TAB_HEIGHT } from '../constant'; import { useInsomniaTab } from '../hooks/tab'; import { useExecutionState } from '../hooks/use-execution-state'; import { useReadyState } from '../hooks/use-ready-state'; @@ -771,7 +772,7 @@ export const Debug: FC = () => { - + { - - + { > {clientCertificates.length === 0 || caCertificate ? 'Add' : 'Manage'} Certificates {[...clientCertificates, caCertificate].filter(cert => !cert?.disabled).filter(isNotNullOrUndefined).length > 0 ? `(${[...clientCertificates, caCertificate].filter(cert => !cert?.disabled).filter(isNotNullOrUndefined).length})` : ''} - - + Spec diff --git a/packages/insomnia/src/ui/routes/environments.tsx b/packages/insomnia/src/ui/routes/environments.tsx index ba9defb9fe1..cc21a9b9aca 100644 --- a/packages/insomnia/src/ui/routes/environments.tsx +++ b/packages/insomnia/src/ui/routes/environments.tsx @@ -22,6 +22,7 @@ import { useInsomniaTab } from '../hooks/tab'; import { useOrganizationPermissions } from '../hooks/use-organization-features'; import { useOrganizationLoaderData } from './organization'; import type { WorkspaceLoaderData } from './workspace'; +import { INSOMNIA_TAB_HEIGHT } from '../constant'; const Environments = () => { const { organizationId = '', projectId = '', workspaceId = '' } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); @@ -275,7 +276,7 @@ const Environments = () => { return ( - + { - + diff --git a/packages/insomnia/src/ui/routes/unit-test.tsx b/packages/insomnia/src/ui/routes/unit-test.tsx index 22bb4735bb1..62b6d70f36d 100644 --- a/packages/insomnia/src/ui/routes/unit-test.tsx +++ b/packages/insomnia/src/ui/routes/unit-test.tsx @@ -55,6 +55,7 @@ import { useRootLoaderData } from './root'; import { TestRunStatus } from './test-results'; import TestSuiteRoute from './test-suite'; import type { WorkspaceLoaderData } from './workspace'; +import { INSOMNIA_TAB_HEIGHT } from '../constant'; interface LoaderData { unitTestSuites: UnitTestSuite[]; @@ -311,7 +312,7 @@ const TestRoute: FC = () => { - + Date: Thu, 7 Nov 2024 17:31:30 +0800 Subject: [PATCH 05/47] feat: sync with db --- .../insomnia/src/ui/components/tabs/tab.tsx | 12 +-- .../src/ui/components/tabs/tabList.tsx | 78 +++++++++++++++++-- .../ui/context/app/insomnia-tab-context.tsx | 55 +++++++++++-- packages/insomnia/src/ui/hooks/tab.ts | 25 ++---- packages/insomnia/src/ui/routes/debug.tsx | 7 +- packages/insomnia/src/ui/routes/design.tsx | 9 +-- .../insomnia/src/ui/routes/environments.tsx | 9 +-- .../insomnia/src/ui/routes/mock-server.tsx | 9 +-- packages/insomnia/src/ui/routes/unit-test.tsx | 9 +-- 9 files changed, 142 insertions(+), 71 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index 86144e5a64a..42eca5d34be 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -5,7 +5,7 @@ import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; import { Icon } from '../icon'; import { Tooltip } from '../tooltip'; -export const enum TabEnum { +export enum TabEnum { Request = 'request', Folder = 'folder', Env = 'environment', @@ -25,14 +25,16 @@ export interface BaseTab { organizationId: string; projectId: string; workspaceId: string; - organizationName: string; projectName: string; workspaceName: string; id: string; - [key: string]: string; + // tag is used to display the request method in the tab + // method is used to display the tag color + tag?: string; + method?: string; }; -const REQUEST_TAG_MAP: Record = { +const REQUEST_METHOD_STYLE_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)]', @@ -83,7 +85,7 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { if (type === TabEnum.Request || type === TabEnum.MockRoute) { return ( - {tab.tag} + {tab.tag} ); } diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 6c290a77ca4..ccd5e20ee89 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -1,15 +1,19 @@ -import React from 'react'; -import { GridList, type Key, type Selection } from 'react-aria-components'; +import React, { useCallback, useEffect } from 'react'; +import { GridList, type Selection } from 'react-aria-components'; import { useNavigate } from 'react-router-dom'; +import { type ChangeBufferEvent, type ChangeType, database } from '../../../common/database'; +import * as models from '../../../models/index'; +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 { getMethodShortHand } from '../tags/method-tag'; import { type BaseTab, InsomniaTab, TabEnum } from './tab'; export interface OrganizationTabs { tabList: BaseTab[]; - activeTabId?: Key | null; + activeTabId?: string; } export const TAB_ROUTER_PATH: Record = { @@ -31,7 +35,7 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { console.log('activeTabId', activeTabId); const navigate = useNavigate(); - const { changeActiveTab } = useInsomniaTabContext(); + const { changeActiveTab, deleteTabById, deleteAllTabsUnderWorkspace, updateTabById } = useInsomniaTabContext(); const handleSelectionChange = (keys: Selection) => { console.log('changeActiveTab'); @@ -44,6 +48,70 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { } }; + const needHandleChange = (changeType: ChangeType, docType: string) => { + // only handle update and delete + if (changeType !== 'update' && changeType !== 'remove') { + return false; + } + // only handle the following types + const list = [ + models.request.type, + models.grpcRequest.type, + models.webSocketRequest.type, + models.requestGroup.type, + models.unitTestSuite.type, + models.workspace.type, + models.environment.type, + models.mockRoute.type, + ]; + return list.includes(docType); + }; + + const handleDelete = useCallback((docId: string, docType: string) => { + if (docType === models.workspace.type) { + // delete all tabs of this workspace + deleteAllTabsUnderWorkspace?.(docId); + } else { + // delete tab by id + deleteTabById(docId); + } + }, [deleteAllTabsUnderWorkspace, deleteTabById]); + + const haneldUpdate = useCallback((docId: string, newName: string, method?: string, tag?: string) => { + updateTabById?.(docId, newName, method, tag); + }, [updateTabById]); + + 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]; + + if (needHandleChange(changeType, doc.type)) { + if (changeType === 'remove') { + handleDelete(doc._id, doc.type); + } else if (changeType === 'update') { + // currently have 2 types of update, rename and change request method + if (doc.type === models.request.type) { + const tag = getMethodShortHand(doc as Request); + const method = (doc as Request).method; + haneldUpdate(doc._id, doc.name, method, tag); + } else { + haneldUpdate(doc._id, doc.name); + } + } + } + } + }; + database.onChange(callback); + + return () => { + database.offChange(callback); + }; + }, [deleteTabById, handleDelete, haneldUpdate]); + if (!tabList.length) { return null; }; @@ -58,7 +126,7 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { defaultSelectedKeys={['req_737492dce0c3460a8a55762e5d1bbd99']} selectionMode="single" selectionBehavior='replace' - className={`flex h-[${INSOMNIA_TAB_HEIGHT}] w-[calc(100%-50px)] overflow-x-scroll hide-scrollbars`} + className={`flex h-[${INSOMNIA_TAB_HEIGHT}] max-w-[calc(100%-50px)] overflow-x-scroll hide-scrollbars`} items={tabList} > {item => } 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 7db50910bab..0fbb66e234c 100644 --- a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -1,4 +1,4 @@ -import React, { createContext, type FC, type PropsWithChildren, useCallback, useContext, useEffect, useRef } from 'react'; +import React, { createContext, type FC, type PropsWithChildren, useCallback, useContext, useRef } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useLocalStorage } from 'react-use'; @@ -17,6 +17,8 @@ interface ContextProps { deleteTabById: (id: string) => void; addTab: (tab: BaseTab) => void; changeActiveTab: (id: string) => void; + deleteAllTabsUnderWorkspace?: (workspaceId: string) => void; + updateTabById?: (tabId: string, name: string, method?: string, tag?: string) => void; } const InsomniaTabContext = createContext({ @@ -64,6 +66,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { }, [setAppTabs]); const addTab = useCallback((tab: BaseTab) => { + console.log('addTab'); const currentTabs = appTabsRef?.current?.[organizationId] || { tabList: [], activeTabId: '' }; updateInsomniaTabs({ @@ -73,12 +76,8 @@ export const InsomniaTabProvider: FC = ({ children }) => { }); }, [organizationId, updateInsomniaTabs]); - useEffect(() => { - console.log('addTab change'); - }, [addTab]); - const deleteTabById = useCallback((id: string) => { - const currentTabs = appTabs?.[organizationId]; + const currentTabs = appTabsRef?.current?.[organizationId]; if (!currentTabs) { return; } @@ -95,6 +94,9 @@ export const InsomniaTabProvider: FC = ({ children }) => { } const index = currentTabs.tabList.findIndex(tab => tab.id === id); + if (index === -1) { + return; + } const newTabList = currentTabs.tabList.filter(tab => tab.id !== id); if (currentTabs.activeTabId === id) { navigate(newTabList[index - 1 < 0 ? 0 : index - 1]?.url || ''); @@ -104,7 +106,44 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: newTabList, activeTabId: currentTabs.activeTabId === id ? newTabList[index - 1 < 0 ? 0 : index - 1]?.id : currentTabs.activeTabId as string, }); - }, [appTabs, navigate, organizationId, projectId, updateInsomniaTabs]); + }, [navigate, organizationId, projectId, updateInsomniaTabs]); + + const deleteAllTabsUnderWorkspace = useCallback((workspaceId: string) => { + const currentTabs = appTabsRef?.current?.[organizationId]; + if (!currentTabs) { + return; + } + const newTabList = currentTabs.tabList.filter(tab => tab.workspaceId !== workspaceId); + + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: '', + }); + }, [organizationId, updateInsomniaTabs]); + + const updateTabById = useCallback((tabId: string, name: string, method: string = '', tag: string = '') => { + const currentTabs = appTabsRef?.current?.[organizationId]; + if (!currentTabs) { + return; + } + const newTabList = currentTabs.tabList.map(tab => { + if (tab.id === tabId) { + return { + ...tab, + name, + tag, + method, + }; + } + return tab; + }); + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: currentTabs.activeTabId || '', + }); + }, [organizationId, updateInsomniaTabs]); const changeActiveTab = useCallback((id: string) => { const currentTabs = appTabsRef?.current?.[organizationId] || { tabList: [], activeTabId: '' }; @@ -123,7 +162,9 @@ export const InsomniaTabProvider: FC = ({ children }) => { value={{ currentOrgTabs: appTabs?.[organizationId] || { tabList: [], activeTabId: '' }, deleteTabById, + deleteAllTabsUnderWorkspace, addTab, + updateTabById, changeActiveTab, appTabsRef, }} diff --git a/packages/insomnia/src/ui/hooks/tab.ts b/packages/insomnia/src/ui/hooks/tab.ts index 798a39af77a..a5c4ff86dc3 100644 --- a/packages/insomnia/src/ui/hooks/tab.ts +++ b/packages/insomnia/src/ui/hooks/tab.ts @@ -1,10 +1,8 @@ import { useCallback, useEffect } from 'react'; import { matchPath, useLocation } from 'react-router-dom'; -import { database } from '../../common/database'; 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'; @@ -24,7 +22,6 @@ interface InsomniaTabProps { activeWorkspace: Workspace; activeRequest?: Request | GrpcRequest | WebSocketRequest; activeRequestGroup?: RequestGroup; - activeOrganization?: Organization; activeMockRoute?: MockRoute; unitTestSuite?: UnitTestSuite; } @@ -37,7 +34,6 @@ export const useInsomniaTab = ({ activeWorkspace, activeRequest, activeRequestGroup, - activeOrganization, activeMockRoute, unitTestSuite, }: InsomniaTabProps) => { @@ -181,10 +177,10 @@ export const useInsomniaTab = ({ projectId: projectId, workspaceId: workspaceId, id: getTabId(type), - tag: getRequestMethodShortHand(activeRequest), - organizationName: activeOrganization?.name || '', projectName: activeProject.name, workspaceName: activeWorkspace.name, + tag: getRequestMethodShortHand(activeRequest), + method: (activeRequest as Request)?.method || '', }; } @@ -197,10 +193,9 @@ export const useInsomniaTab = ({ projectId: projectId, workspaceId: workspaceId, id: getTabId(type), - tag: getRequestMethodShortHand(activeRequest), - organizationName: activeOrganization?.name || '', projectName: activeProject.name, workspaceName: activeWorkspace.name, + tag: getRequestMethodShortHand(activeRequest), }; } @@ -213,7 +208,6 @@ export const useInsomniaTab = ({ projectId: projectId, workspaceId: workspaceId, id: getTabId(type), - organizationName: activeOrganization?.name || '', projectName: activeProject.name, workspaceName: activeWorkspace.name, }; @@ -228,7 +222,6 @@ export const useInsomniaTab = ({ projectId: projectId, workspaceId: workspaceId, id: getTabId(type), - organizationName: activeOrganization?.name || '', projectName: activeProject.name, workspaceName: activeWorkspace.name, }; @@ -243,10 +236,9 @@ export const useInsomniaTab = ({ projectId: projectId, workspaceId: workspaceId, id: getTabId(type), - organizationName: activeOrganization?.name || '', + tag: formatMethodName(activeMockRoute?.method || ''), projectName: activeProject.name, workspaceName: activeWorkspace.name, - tag: formatMethodName(activeMockRoute?.method || ''), }; } @@ -259,14 +251,13 @@ export const useInsomniaTab = ({ 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]); + }, [activeMockRoute?.method, activeMockRoute?.name, activeProject.name, activeRequest, activeWorkspace.name, generateTabUrl, getTabId, organizationId, projectId, unitTestSuite?.name, workspaceId]); useEffect(() => { const type = getTabType(location.pathname); @@ -289,10 +280,4 @@ export const useInsomniaTab = ({ } } }, [addTab, appTabsRef, changeActiveTab, getCurrentTab, location.pathname, organizationId, packTabInfo]); - - useEffect(() => { - database.onChange(async (e) => { - console.log('database change', e); - }); - }, []); }; diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 95fdb2c1463..a5a987194ab 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -114,7 +114,6 @@ import { useRequestMetaPatcher, useRequestPatcher, } from '../hooks/use-request'; -import { useOrganizationLoaderData } from './organization'; import type { GrpcRequestLoaderData, RequestLoaderData, @@ -223,9 +222,6 @@ export const Debug: FC = () => { 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, @@ -760,11 +756,10 @@ export const Debug: FC = () => { organizationId, projectId, workspaceId, - activeProject, activeWorkspace, + activeProject, activeRequest, activeRequestGroup, - activeOrganization, }); return ( diff --git a/packages/insomnia/src/ui/routes/design.tsx b/packages/insomnia/src/ui/routes/design.tsx index 3ce4b436348..28f78eab36e 100644 --- a/packages/insomnia/src/ui/routes/design.tsx +++ b/packages/insomnia/src/ui/routes/design.tsx @@ -68,6 +68,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 { 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 { @@ -75,10 +76,8 @@ import { useGitVCSVersion, } from '../hooks/use-vcs-version'; import { SpectralRunner } from '../worker/spectral-run'; -import { useOrganizationLoaderData } from './organization'; import { useRootLoaderData } from './root'; import type { WorkspaceLoaderData } from './workspace'; -import { INSOMNIA_TAB_HEIGHT } from '../constant'; interface LoaderData { apiSpec: ApiSpec; @@ -456,16 +455,12 @@ const Design: FC = () => { } }, [settings.forceVerticalLayout, direction]); - const { organizations } = useOrganizationLoaderData(); - const activeOrganization = organizations.find(o => o.id === organizationId); - useInsomniaTab({ organizationId, projectId, workspaceId, - activeProject, activeWorkspace, - activeOrganization, + activeProject, }); return ( diff --git a/packages/insomnia/src/ui/routes/environments.tsx b/packages/insomnia/src/ui/routes/environments.tsx index cc21a9b9aca..5d7164c200d 100644 --- a/packages/insomnia/src/ui/routes/environments.tsx +++ b/packages/insomnia/src/ui/routes/environments.tsx @@ -18,11 +18,10 @@ import { Icon } from '../components/icon'; 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 { useOrganizationPermissions } from '../hooks/use-organization-features'; -import { useOrganizationLoaderData } from './organization'; import type { WorkspaceLoaderData } from './workspace'; -import { INSOMNIA_TAB_HEIGHT } from '../constant'; const Environments = () => { const { organizationId = '', projectId = '', workspaceId = '' } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); @@ -261,16 +260,12 @@ const Environments = () => { sidebar_toggle: toggleSidebar, }); - const { organizations } = useOrganizationLoaderData(); - const activeOrganization = organizations.find(o => o.id === organizationId); - useInsomniaTab({ organizationId, projectId, workspaceId, - activeProject, activeWorkspace, - activeOrganization, + activeProject, }); return ( diff --git a/packages/insomnia/src/ui/routes/mock-server.tsx b/packages/insomnia/src/ui/routes/mock-server.tsx index af019a08c56..dfd6d73e9c7 100644 --- a/packages/insomnia/src/ui/routes/mock-server.tsx +++ b/packages/insomnia/src/ui/routes/mock-server.tsx @@ -20,12 +20,11 @@ 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 { INSOMNIA_TAB_HEIGHT } from '../constant'; 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'; -import { INSOMNIA_TAB_HEIGHT } from '../constant'; export interface MockServerLoaderData { mockServerId: string; mockRoutes: MockRoute[]; @@ -182,16 +181,12 @@ const MockServerRoute = () => { } }, [settings.forceVerticalLayout, direction]); - const { organizations } = useOrganizationLoaderData(); - const activeOrganization = organizations.find(o => o.id === organizationId); - useInsomniaTab({ organizationId, projectId, workspaceId, - activeProject, activeWorkspace, - activeOrganization, + activeProject, activeMockRoute: mockRoutes.find(s => s._id === mockRouteId), }); diff --git a/packages/insomnia/src/ui/routes/unit-test.tsx b/packages/insomnia/src/ui/routes/unit-test.tsx index 62b6d70f36d..fb5a9fe503f 100644 --- a/packages/insomnia/src/ui/routes/unit-test.tsx +++ b/packages/insomnia/src/ui/routes/unit-test.tsx @@ -49,13 +49,12 @@ import { CookiesModal } from '../components/modals/cookies-modal'; import { CertificatesModal } from '../components/modals/workspace-certificates-modal'; 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 { useOrganizationLoaderData } from './organization'; import { useRootLoaderData } from './root'; import { TestRunStatus } from './test-results'; import TestSuiteRoute from './test-suite'; import type { WorkspaceLoaderData } from './workspace'; -import { INSOMNIA_TAB_HEIGHT } from '../constant'; interface LoaderData { unitTestSuites: UnitTestSuite[]; @@ -293,17 +292,13 @@ const TestRoute: FC = () => { } }, [settings.forceVerticalLayout, direction]); - const { organizations } = useOrganizationLoaderData(); - const activeOrganization = organizations.find(o => o.id === organizationId); - useInsomniaTab({ organizationId, projectId, workspaceId, - activeProject, activeWorkspace, - activeOrganization, unitTestSuite, + activeProject, }); return ( From 7b047bb359b69b7880ebbbeaa25f363df66ba7c7 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 7 Nov 2024 18:09:19 +0800 Subject: [PATCH 06/47] feat: sync with database --- .../src/ui/components/tabs/tabList.tsx | 41 +++++++----- .../ui/context/app/insomnia-tab-context.tsx | 63 +++++++++++++++++++ 2 files changed, 89 insertions(+), 15 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index ccd5e20ee89..3e1e2f03d3c 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -35,7 +35,7 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { console.log('activeTabId', activeTabId); const navigate = useNavigate(); - const { changeActiveTab, deleteTabById, deleteAllTabsUnderWorkspace, updateTabById } = useInsomniaTabContext(); + const { changeActiveTab, deleteTabById, deleteAllTabsUnderWorkspace, deleteAllTabsUnderProject, updateTabById, updateProjectName, updateWorkspaceName } = useInsomniaTabContext(); const handleSelectionChange = (keys: Selection) => { console.log('changeActiveTab'); @@ -63,11 +63,16 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { models.workspace.type, models.environment.type, models.mockRoute.type, + models.project.type, ]; return list.includes(docType); }; const handleDelete = useCallback((docId: string, docType: string) => { + if (docType === models.project.type) { + // delete all tabs of this project + deleteAllTabsUnderProject?.(docId); + } if (docType === models.workspace.type) { // delete all tabs of this workspace deleteAllTabsUnderWorkspace?.(docId); @@ -75,11 +80,24 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { // delete tab by id deleteTabById(docId); } - }, [deleteAllTabsUnderWorkspace, deleteTabById]); + }, [deleteAllTabsUnderProject, deleteAllTabsUnderWorkspace, deleteTabById]); - const haneldUpdate = useCallback((docId: string, newName: string, method?: string, tag?: string) => { - updateTabById?.(docId, newName, method, tag); - }, [updateTabById]); + const handleUpdate = useCallback((doc: models.BaseModel) => { + // currently have 2 types of update, rename and change request method + if (doc.type === models.request.type) { + const tag = getMethodShortHand(doc as Request); + const method = (doc as Request).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) + updateWorkspaceName?.(doc._id, doc.name); + } else { + updateTabById?.(doc._id, doc.name); + } + }, [updateProjectName, updateTabById, updateWorkspaceName]); useEffect(() => { // sync tabList with database @@ -88,19 +106,12 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { for (const change of changes) { const changeType = change[0]; const doc = change[1]; - + debugger; if (needHandleChange(changeType, doc.type)) { if (changeType === 'remove') { handleDelete(doc._id, doc.type); } else if (changeType === 'update') { - // currently have 2 types of update, rename and change request method - if (doc.type === models.request.type) { - const tag = getMethodShortHand(doc as Request); - const method = (doc as Request).method; - haneldUpdate(doc._id, doc.name, method, tag); - } else { - haneldUpdate(doc._id, doc.name); - } + handleUpdate(doc); } } } @@ -110,7 +121,7 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { return () => { database.offChange(callback); }; - }, [deleteTabById, handleDelete, haneldUpdate]); + }, [deleteTabById, handleDelete, handleUpdate]); 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 0fbb66e234c..1e2f02ef2c8 100644 --- a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -18,6 +18,9 @@ interface ContextProps { addTab: (tab: BaseTab) => void; changeActiveTab: (id: string) => void; deleteAllTabsUnderWorkspace?: (workspaceId: string) => void; + deleteAllTabsUnderProject?: (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; } @@ -122,6 +125,20 @@ export const InsomniaTabProvider: FC = ({ children }) => { }); }, [organizationId, updateInsomniaTabs]); + const deleteAllTabsUnderProject = useCallback((projectId: string) => { + const currentTabs = appTabsRef?.current?.[organizationId]; + if (!currentTabs) { + return; + } + const newTabList = currentTabs.tabList.filter(tab => tab.projectId !== projectId); + + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: '', + }); + }, [organizationId, updateInsomniaTabs]); + const updateTabById = useCallback((tabId: string, name: string, method: string = '', tag: string = '') => { const currentTabs = appTabsRef?.current?.[organizationId]; if (!currentTabs) { @@ -157,15 +174,61 @@ export const InsomniaTabProvider: FC = ({ children }) => { }); }, [organizationId, updateInsomniaTabs]); + const updateProjectName = useCallback((projectId: string, name: string) => { + const currentTabs = appTabsRef?.current?.[organizationId]; + + if (!currentTabs) { + return; + } + const newTabList = currentTabs.tabList.map(tab => { + if (tab.projectId === projectId) { + return { + ...tab, + projectName: name, + }; + } + return tab; + }); + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: currentTabs.activeTabId || '', + }); + }, [organizationId, updateInsomniaTabs]); + + const updateWorkspaceName = useCallback((workspaceId: string, name: string) => { + const currentTabs = appTabsRef?.current?.[organizationId]; + if (!currentTabs) { + return; + } + const newTabList = currentTabs.tabList.map(tab => { + if (tab.workspaceId === workspaceId) { + return { + ...tab, + workspaceName: name, + }; + } + return tab; + }); + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: currentTabs.activeTabId || '', + }); + }, [organizationId, updateInsomniaTabs]); + return ( From 9ac36f8c50b55dfda1d94f31621914a0ea4861f7 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Mon, 11 Nov 2024 16:06:15 +0800 Subject: [PATCH 07/47] ui improvement --- packages/insomnia/src/ui/components/tabs/tab.tsx | 3 ++- packages/insomnia/src/ui/components/tabs/tabList.tsx | 10 ++++++---- packages/insomnia/src/ui/constant.ts | 2 +- packages/insomnia/src/ui/routes/debug.tsx | 2 +- packages/insomnia/src/ui/routes/design.tsx | 2 +- packages/insomnia/src/ui/routes/environments.tsx | 2 +- packages/insomnia/src/ui/routes/mock-server.tsx | 2 +- packages/insomnia/src/ui/routes/unit-test.tsx | 2 +- 8 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index 42eca5d34be..648873cfdac 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -125,7 +125,8 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { )} - {isSelected && } + + )} diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 3e1e2f03d3c..213327e719b 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -106,7 +106,6 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { for (const change of changes) { const changeType = change[0]; const doc = change[1]; - debugger; if (needHandleChange(changeType, doc.type)) { if (changeType === 'remove') { handleDelete(doc._id, doc.type); @@ -128,7 +127,7 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { }; return ( - + { defaultSelectedKeys={['req_737492dce0c3460a8a55762e5d1bbd99']} selectionMode="single" selectionBehavior='replace' - className={`flex h-[${INSOMNIA_TAB_HEIGHT}] 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) + className={`flex h-[${INSOMNIA_TAB_HEIGHT + 1}px] bg-[--color-bg] max-w-[calc(100%-50px)] overflow-x-scroll hide-scrollbars`} items={tabList} > {item => } - + + + ); }; diff --git a/packages/insomnia/src/ui/constant.ts b/packages/insomnia/src/ui/constant.ts index d4745361851..ec3ebef8e58 100644 --- a/packages/insomnia/src/ui/constant.ts +++ b/packages/insomnia/src/ui/constant.ts @@ -1,3 +1,3 @@ // this a constant file just for renderer process -export const INSOMNIA_TAB_HEIGHT = '40px'; +export const INSOMNIA_TAB_HEIGHT = 40; diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index a5a987194ab..81a79cf6812 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -767,7 +767,7 @@ export const Debug: FC = () => { - + { - + { return ( - + { - + diff --git a/packages/insomnia/src/ui/routes/unit-test.tsx b/packages/insomnia/src/ui/routes/unit-test.tsx index fb5a9fe503f..4eb6ed0ccd6 100644 --- a/packages/insomnia/src/ui/routes/unit-test.tsx +++ b/packages/insomnia/src/ui/routes/unit-test.tsx @@ -307,7 +307,7 @@ const TestRoute: FC = () => { - + Date: Mon, 11 Nov 2024 16:44:48 +0800 Subject: [PATCH 08/47] fix: mock route display --- packages/insomnia/src/ui/components/tabs/tabList.tsx | 11 ++++++++--- packages/insomnia/src/ui/hooks/tab.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 213327e719b..3d70c1681bb 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -4,11 +4,12 @@ import { useNavigate } 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 { INSOMNIA_TAB_HEIGHT } from '../../constant'; import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; import { Icon } from '../icon'; -import { getMethodShortHand } from '../tags/method-tag'; +import { formatMethodName, getRequestMethodShortHand } from '../tags/method-tag'; import { type BaseTab, InsomniaTab, TabEnum } from './tab'; export interface OrganizationTabs { @@ -84,10 +85,14 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { const handleUpdate = useCallback((doc: models.BaseModel) => { // currently have 2 types of update, rename and change request method - if (doc.type === models.request.type) { - const tag = getMethodShortHand(doc as Request); + 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); diff --git a/packages/insomnia/src/ui/hooks/tab.ts b/packages/insomnia/src/ui/hooks/tab.ts index a5c4ff86dc3..1ad38b64ebf 100644 --- a/packages/insomnia/src/ui/hooks/tab.ts +++ b/packages/insomnia/src/ui/hooks/tab.ts @@ -195,7 +195,6 @@ export const useInsomniaTab = ({ id: getTabId(type), projectName: activeProject.name, workspaceName: activeWorkspace.name, - tag: getRequestMethodShortHand(activeRequest), }; } @@ -239,6 +238,7 @@ export const useInsomniaTab = ({ tag: formatMethodName(activeMockRoute?.method || ''), projectName: activeProject.name, workspaceName: activeWorkspace.name, + method: activeMockRoute?.method || '', }; } From c217731b3b024e56e49e72f791978c99071dc174 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Tue, 12 Nov 2024 15:05:36 +0800 Subject: [PATCH 09/47] add dropdown --- .../src/ui/components/tabs/tabList.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 3d70c1681bb..371ec3d6327 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect } from 'react'; -import { GridList, type Selection } from 'react-aria-components'; +import { Button, GridList, Menu, MenuItem, MenuTrigger, Popover, type Selection } from 'react-aria-components'; import { useNavigate } from 'react-router-dom'; import { type ChangeBufferEvent, type ChangeType, database } from '../../../common/database'; @@ -148,7 +148,21 @@ export const OrganizationTabList = ({ showActiveStatus = true }) => { {item => } - + + + + + + + { }}> + save to current workspace + + { }}> + save to other workspace + + + + ); From d6d24c2cd9728c2177ba800d5631f42ddca3cdbb Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Tue, 12 Nov 2024 16:59:45 +0800 Subject: [PATCH 10/47] + button --- .../add-request-to-collection-modal.tsx | 146 ++++++++++++++++++ .../src/ui/components/tabs/tabList.tsx | 43 +++++- packages/insomnia/src/ui/routes/debug.tsx | 2 +- 3 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 packages/insomnia/src/ui/components/modals/add-request-to-collection-modal.tsx 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 new file mode 100644 index 00000000000..a8349b094e6 --- /dev/null +++ b/packages/insomnia/src/ui/components/modals/add-request-to-collection-modal.tsx @@ -0,0 +1,146 @@ +import React, { type FC, type MouseEventHandler, useEffect, useRef, useState } from 'react'; +import { OverlayContainer } from 'react-aria'; +import { useFetcher, useParams } from 'react-router-dom'; + +import { database } from '../../../common/database'; +import { strings } from '../../../common/strings'; +import { sortProjects } from '../../../models/helpers/project'; +import * as models from '../../../models/index'; +import type { Project } from '../../../models/project'; +import { Modal, type ModalHandle, type ModalProps } from '../base/modal'; +import { ModalBody } from '../base/modal-body'; +import { ModalFooter } from '../base/modal-footer'; +import { ModalHeader } from '../base/modal-header'; +import { Icon } from '../icon'; + +interface AddRequestModalProps extends ModalProps { + onHide: Function; +} + +export const AddRequestToCollectionModal: FC = ({ onHide }) => { + const { organizationId, projectId: currentProjectId, workspaceId: currentWorkspaceId } = useParams(); + const [projectOptions, setProjectOptions] = useState([]); + const [workspaceOptions, setWorkspaceOptions] = useState([]); + const [selectedProjectId, setSelectedProjectId] = useState(''); + const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(''); + + const requestFetcher = useFetcher(); + + useEffect(() => { + (async () => { + const organizationProjects = await database.find(models.project.type, { + parentId: organizationId, + }); + setProjectOptions(sortProjects(organizationProjects)); + setSelectedProjectId(organizationProjects[0]?._id || ''); + })(); + }, [organizationId]); + + useEffect(() => { + (async () => { + const workspaces = await models.workspace.findByParentId(selectedProjectId); + const requestCollections = workspaces.filter(workspace => workspace.scope === 'collection'); + setWorkspaceOptions(requestCollections); + setSelectedWorkspaceId(requestCollections[0]?._id || ''); + })(); + }, [selectedProjectId]); + + const modalRef = useRef(null); + useEffect(() => { + modalRef.current?.show(); + }, []); + + const isBtnDisabled = requestFetcher.state !== 'idle' + || !selectedProjectId; + + const createNewRequest = async () => { + requestFetcher.submit( + { requestType: 'HTTP', parentId: selectedWorkspaceId }, + { + action: `/organization/${organizationId}/project/${selectedProjectId}/workspace/${selectedWorkspaceId}/debug/request/new`, + method: 'post', + encType: 'application/json', + }, + ); + }; + + return ( + e.stopPropagation()}> + + Add Request + + + + {strings.project.singular}: + setSelectedProjectId(e.target.value)}> + {projectOptions.map(project => ( + + {project.name}{project._id === currentProjectId && ' (current)'} + + ))} + + + + {!selectedProjectId && ( + + Project is required + + )} + + + + {strings.workspace.singular}: + setSelectedWorkspaceId(e.target.value)}> + {workspaceOptions.map(workspace => ( + + {workspace.name}{workspace._id === currentWorkspaceId && ' (current)'} + + ))} + + + + {!selectedWorkspaceId && ( + + Workspace is required + + )} + {requestFetcher.data?.error && ( + + {requestFetcher.data.error} + + )} + + + + } + className="btn btn--no-background" + > + Cancel + + + {requestFetcher.state !== 'idle' && } Save + + + + + + ); +}; 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 81a79cf6812..8cb0bf68c30 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/47] 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 35dc4fdbda107400e990a4421f696d32f004bb78 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/47] 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 8cb0bf68c30..4b0495b8f72 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 860590b86ffd332b58f507c6107d58de51a2e7f1 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/47] 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 } - {strings.project.singular}: + {strings.project.plural}: setSelectedProjectId(e.target.value)}> {projectOptions.map(project => ( @@ -94,7 +94,7 @@ export const AddRequestToCollectionModal: FC = ({ onHide } - {strings.workspace.singular}: + {strings.collection.plural}: setSelectedWorkspaceId(e.target.value)}> {workspaceOptions.map(workspace => ( @@ -111,7 +111,7 @@ export const AddRequestToCollectionModal: FC = ({ onHide } color: 'var(--color-danger)', }} > - Workspace is required + Collection is required )} {requestFetcher.data?.error && ( @@ -136,7 +136,7 @@ export const AddRequestToCollectionModal: FC = ({ onHide } className="btn" onClick={createNewRequest} > - {requestFetcher.state !== 'idle' && } Save + {requestFetcher.state !== 'idle' && } Add 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 1941ba652125d454ef214ea20b3deb8bf309684f 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/47] 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 && ( + handleClose(tab.id)}> - )} - + + )} From cda2967f21542b20640fed768f3895bbcf858d46 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/47] 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 a31c19589fa8b87ded1a1e6d97148d0fa20a3a4a 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/47] 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 => } + + + + + diff --git a/packages/insomnia/src/ui/hooks/use-resize-observer.tsx b/packages/insomnia/src/ui/hooks/use-resize-observer.tsx new file mode 100644 index 00000000000..ceabf1b58e0 --- /dev/null +++ b/packages/insomnia/src/ui/hooks/use-resize-observer.tsx @@ -0,0 +1,27 @@ +import { useEffect, useRef } from 'react'; + +export interface Size { + width: number | undefined; + height: number | undefined; +} + +export const useResizeObserver = (ref: React.RefObject, onResize: (size: Size) => void) => { + const onResizeRef = useRef<((size: Size) => void) | undefined>(undefined); + onResizeRef.current = onResize; + + useEffect(() => { + if (!ref.current) { + return; + } + const observer = new ResizeObserver(([entry]) => { + const { width, height } = entry.contentRect; + onResizeRef.current?.({ width, height }); + }); + + observer.observe(ref.current); + + return () => { + observer.disconnect(); + }; + }, [ref]); +}; From 6a5682e469fd87fa736c78a0a7a2a709db087eec Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Mon, 18 Nov 2024 18:27:55 +0800 Subject: [PATCH 17/47] ui improvement --- .../modals/add-request-to-collection-modal.tsx | 11 +++++++++++ packages/insomnia/src/ui/components/tabs/tabList.tsx | 6 +++--- 2 files changed, 14 insertions(+), 3 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 7de0d4c5508..a81e01a3f7c 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 @@ -53,6 +53,8 @@ export const AddRequestToCollectionModal: FC = ({ onHide } const isBtnDisabled = requestFetcher.state !== 'idle' || !selectedProjectId || !selectedWorkspaceId; + const previousRequestFetcherState = useRef('idle'); + const createNewRequest = async () => { requestFetcher.submit( { requestType: 'HTTP', parentId: selectedWorkspaceId }, @@ -62,8 +64,17 @@ export const AddRequestToCollectionModal: FC = ({ onHide } encType: 'application/json', }, ); + previousRequestFetcherState.current = 'loading'; }; + useEffect(() => { + if (previousRequestFetcherState?.current === 'loading' && requestFetcher.state === 'idle') { + // when action is completed, close the modal + onHide(); + previousRequestFetcherState.current = 'idle'; + } + }, [onHide, requestFetcher.state]); + return ( e.stopPropagation()}> diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index c4177ee3da1..b70987b6068 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -194,9 +194,9 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' return ( - + - + - + From 61756fa6b2b930c7666ce7b24d5b5729e4e3386a 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/47] 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 38a137b33e3..707a753debf 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 aa95273a93a..cd0925fe30f 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 5ffdf985c83..07cfc2b2cb8 100644 --- a/packages/insomnia/src/ui/components/codemirror/one-line-editor.tsx +++ b/packages/insomnia/src/ui/components/codemirror/one-line-editor.tsx @@ -257,7 +257,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; @@ -320,10 +320,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 && ( - - handleClose(tab.id)}> - - - - )} + {tab.name} + handleClose(tab.id)}> + + 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 4a70830ef520d65e85db8fbd444559155a06ebb9 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/47] 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 8aa8e909d322ecda3ccb51c7eee601b7b3ce63cc 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/47] 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 4b0495b8f72..5ad515ee140 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 7c19d3ba9ca..76934b78d4e 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 437257e7b8a..1ba17d87967 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 16a40b789b4..bef03f4f42d 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 fe432426769..5c65398a3a1 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 4eb6ed0ccd6..6aab83b9f66 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 a388ad0b8086ec1cecb4940eac3da078070ebeba 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/47] 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 fdb0d4227e85eef14e2008774cfc605594353383 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/47] 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 ff45756befedf9de9a0cb93950d1301edfc60378 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/47] 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 f7fb6e3165c63a108aab8d05033d2f3a5e98bae4 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/47] 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 8ff85a9a688bbdd680b5dcae1c6f2efd326d2ec0 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/47] 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 f7e8c88866c5403c81b91a637af68ad983d2b5cd 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/47] 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 ccd3129246b..c764b876301 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'; @@ -255,6 +255,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 ca6047961347da76f3cba27ad58c5b91bae37e4d 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/47] 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 b1d27a794a7..f29d70a797c 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 76934b78d4e..4c1deade0c4 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 1ba17d87967..13e10596a6c 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 bef03f4f42d..ef0c8afad23 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 c764b876301..143ac3cf01d 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, defaultSendActionRuntime, 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 { @@ -171,10 +160,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'); @@ -196,55 +187,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; @@ -255,22 +197,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, @@ -426,14 +356,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 6aab83b9f66..987c393886c 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 3b6c4b585d8d7408a1da066db8953cb61b30540c 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/47] 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 c354037bea368b72ba9e2776d6bae262d9a93ff9 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/47] 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 0bcc84b6a0a78d97478fedf54f44e1ba1497aad6 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/47] 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 87370e8b0b4497e081e0e0dddcdae1bf8c85afd7 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/47] 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 }) => { + )} diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 4c1d9e171bc..d23f7f0d8b3 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -1,6 +1,6 @@ import _ from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; -import { Button, GridList, Menu, MenuItem, MenuTrigger, Popover, type Selection } from 'react-aria-components'; +import { Button, DropIndicator, GridList, Menu, MenuItem, MenuTrigger, Popover, type Selection, useDragAndDrop } from 'react-aria-components'; import { useFetcher, useNavigate, useParams } from 'react-router-dom'; import { type ChangeBufferEvent, type ChangeType, database } from '../../../common/database'; @@ -40,7 +40,7 @@ export const TAB_ROUTER_PATH: Record = { }; export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' }) => { - const { currentOrgTabs, batchUpdateTabs } = useInsomniaTabContext(); + const { currentOrgTabs, batchUpdateTabs, moveBefore, moveAfter } = useInsomniaTabContext(); const { tabList, activeTabId } = currentOrgTabs; const navigate = useNavigate(); @@ -305,6 +305,26 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' } }, [isOverFlow]); + const { dragAndDropHooks } = useDragAndDrop({ + getItems: keys => [...keys].map(key => ({ 'text/plain': key.toString() })), + onReorder: e => { + const moveKey = Array.from(e.keys)[0].toString(); + if (e.target.dropPosition === 'before') { + moveBefore?.(e.target.key.toString(), moveKey);; + } else if (e.target.dropPosition === 'after') { + moveAfter?.(e.target.key.toString(), moveKey); + } + }, + renderDropIndicator(target) { + return ( + + ); + }, + }); + if (!tabList.length) { return null; }; @@ -323,6 +343,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' selectionMode="single" 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} 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 51a143bea3f..43421a8e8f9 100644 --- a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -25,6 +25,8 @@ interface ContextProps { batchUpdateTabs?: (updates: { id: string; fields: Partial }[]) => void; closeAllTabs?: () => void; closeOtherTabs?: (id: string) => void; + moveBefore?: (targetId: string, movingId: string) => void; + moveAfter?: (targetId: string, movingId: string) => void; } const InsomniaTabContext = createContext({ @@ -271,6 +273,44 @@ export const InsomniaTabProvider: FC = ({ children }) => { }); }, [organizationId, updateInsomniaTabs]); + const moveBefore = useCallback((targetId: string, movingId: string) => { + const currentTabs = appTabsRef?.current?.[organizationId]; + if (!currentTabs || targetId === movingId) { + return; + } + + const newTabList = [...currentTabs.tabList]; + const movingIndex = newTabList.findIndex(tab => tab.id === movingId); + const [movingTab] = newTabList.splice(movingIndex, 1); + const targetIndex = newTabList.findIndex(tab => tab.id === targetId); + newTabList.splice(targetIndex, 0, movingTab); + + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: currentTabs.activeTabId || '', + }); + }, [organizationId, updateInsomniaTabs]); + + const moveAfter = useCallback((targetId: string, movingId: string) => { + const currentTabs = appTabsRef?.current?.[organizationId]; + if (!currentTabs || targetId === movingId) { + return; + } + + const newTabList = [...currentTabs.tabList]; + const movingIndex = newTabList.findIndex(tab => tab.id === movingId); + const [movingTab] = newTabList.splice(movingIndex, 1); + const targetIndex = newTabList.findIndex(tab => tab.id === targetId); + newTabList.splice(targetIndex + 1, 0, movingTab); + + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: currentTabs.activeTabId || '', + }); + }, [organizationId, updateInsomniaTabs]); + return ( = ({ children }) => { updateWorkspaceName, batchUpdateTabs, appTabsRef, + moveBefore, + moveAfter, }} > {children} From 951d589d9227f77c470e8db01483fd1b147c2074 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Tue, 3 Dec 2024 14:45:44 +0800 Subject: [PATCH 32/47] fix: ui --- packages/insomnia/src/ui/components/tabs/tab.tsx | 2 +- packages/insomnia/src/ui/components/tabs/tabList.tsx | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index 605308462a1..8695a2b302d 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -151,7 +151,7 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { - + )} diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index d23f7f0d8b3..6487cc37e25 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -330,8 +330,8 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' }; return ( - - + + @@ -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 151e0b61a67998d0e1b80a1c662f73e4305d0649 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/47] 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 143ac3cf01d..28a4aad27bd 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 50727d4dbda008a3c0dfaba5fbbec5b770049b3a 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/47] 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]; From ab82e60ffca47c97b7121eb0f9debd989ba0f7f7 Mon Sep 17 00:00:00 2001 From: Curry Yang <163384738+CurryYangxx@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:32:13 +0800 Subject: [PATCH 35/47] runner improvements (multiple tabs)-[INS-4779] (#8244) * runner improvements * runner improvement * maintain runner state in context * fix lint * save file object to context * add request list to runner context to keep order * fix ts error * add eventbus & clear runner context state * also delete folder runner tab when delete a folder * clean * change runner context data structure * clean up --- packages/insomnia-inso/tsconfig.json | 2 +- packages/insomnia-sdk/tsconfig.json | 2 +- .../src/ui/components/document-tab.tsx | 2 +- .../src/ui/components/tabs/tabList.tsx | 6 +- .../ui/context/app/insomnia-tab-context.tsx | 44 +++ .../src/ui/context/app/runner-context.tsx | 99 ++++++ packages/insomnia/src/ui/eventBus.ts | 37 +++ .../src/ui/hooks/use-runner-request-list.tsx | 25 +- .../insomnia/src/ui/routes/organization.tsx | 5 +- packages/insomnia/src/ui/routes/runner.tsx | 298 ++++++++++-------- packages/insomnia/src/utils/index.ts | 70 ++++ 11 files changed, 429 insertions(+), 161 deletions(-) create mode 100644 packages/insomnia/src/ui/context/app/runner-context.tsx create mode 100644 packages/insomnia/src/ui/eventBus.ts diff --git a/packages/insomnia-inso/tsconfig.json b/packages/insomnia-inso/tsconfig.json index 73b98caaf00..5c1b3820903 100644 --- a/packages/insomnia-inso/tsconfig.json +++ b/packages/insomnia-inso/tsconfig.json @@ -21,7 +21,7 @@ "sourceMap": true, /* Runs in the DOM NOTE: this is inconsistent with reality */ "lib": [ - "ES2020", + "ES2023", "DOM", "DOM.Iterable" ], diff --git a/packages/insomnia-sdk/tsconfig.json b/packages/insomnia-sdk/tsconfig.json index 1d7f225d8c6..92caa82ffaa 100644 --- a/packages/insomnia-sdk/tsconfig.json +++ b/packages/insomnia-sdk/tsconfig.json @@ -20,7 +20,7 @@ "jsx": "react", /* If your code runs in the DOM: */ "lib": [ - "es2022", + "es2023", "dom", "dom.iterable" ], diff --git a/packages/insomnia/src/ui/components/document-tab.tsx b/packages/insomnia/src/ui/components/document-tab.tsx index ee2d7220744..d77e1be7a94 100644 --- a/packages/insomnia/src/ui/components/document-tab.tsx +++ b/packages/insomnia/src/ui/components/document-tab.tsx @@ -21,7 +21,7 @@ export const DocumentTab = ({ organizationId, projectId, workspaceId, className key={item.id} to={`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${item.id}`} className={({ isActive, isPending }) => classnames('text-center rounded-full px-2', { - 'text-[--color-font] bg-[--color-surprise]': isActive, + 'text-[--color-font-surprise] bg-[--color-surprise]': isActive, 'animate-pulse': isPending, })} data-testid={`workspace-${item.id}`} diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 6c1885e0152..2d9c2219c97 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -56,6 +56,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' closeTabById, closeAllTabsUnderWorkspace, closeAllTabsUnderProject, + batchCloseTabs, updateTabById, updateProjectName, updateWorkspaceName, @@ -98,11 +99,14 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' if (docType === models.workspace.type) { // delete all tabs of this workspace closeAllTabsUnderWorkspace?.(docId); + } else if (docType === models.requestGroup.type) { + // when delete a folder, we need also delete the corresponding folder runner tab(if exists) + batchCloseTabs?.([docId, `runner_${docId}`]); } else { // delete tab by id closeTabById(docId); } - }, [closeAllTabsUnderProject, closeAllTabsUnderWorkspace, closeTabById]); + }, [batchCloseTabs, closeAllTabsUnderProject, closeAllTabsUnderWorkspace, closeTabById]); const handleUpdate = useCallback(async (doc: models.BaseModel, patches: Partial[] = []) => { const patchObj: Record = {}; 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 2a1f99baf69..68b634407f7 100644 --- a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -4,6 +4,7 @@ import { useLocalStorage } from 'react-use'; import type { BaseTab } from '../../components/tabs/tab'; import type { OrganizationTabs } from '../../components/tabs/tabList'; +import uiEventBus, { UIEventType } from '../../eventBus'; interface UpdateInsomniaTabParams { organizationId: string; @@ -19,6 +20,7 @@ interface ContextProps { changeActiveTab: (id: string) => void; closeAllTabsUnderWorkspace?: (workspaceId: string) => void; closeAllTabsUnderProject?: (projectId: string) => void; + batchCloseTabs?: (ids: string[]) => void; updateProjectName?: (projectId: string, name: string) => void; updateWorkspaceName?: (projectId: string, name: string) => void; updateTabById?: (tabId: string, patches: Partial) => void; @@ -97,6 +99,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: [], activeTabId: '', }); + uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, [id]); return; } @@ -114,6 +117,39 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: newTabList, activeTabId: currentTabs.activeTabId === id ? newTabList[Math.max(index - 1, 0)]?.id : currentTabs.activeTabId as string, }); + uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, [id]); + }, [navigate, organizationId, projectId, updateInsomniaTabs]); + + const batchCloseTabs = useCallback((deleteIds: string[]) => { + const currentTabs = appTabsRef?.current?.[organizationId]; + if (!currentTabs) { + return; + } + + if (currentTabs.tabList.every(tab => deleteIds.includes(tab.id))) { + navigate(`/organization/${organizationId}/project/${projectId}`); + updateInsomniaTabs({ + organizationId, + tabList: [], + activeTabId: '', + }); + uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, 'all'); + return; + } + + const index = currentTabs.tabList.findIndex(tab => deleteIds.includes(tab.id)); + const newTabList = currentTabs.tabList.filter(tab => !deleteIds.includes(tab.id)); + if (deleteIds.includes(currentTabs.activeTabId || '')) { + const url = newTabList[Math.max(index - 1, 0)]?.url; + navigate(url); + } + + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: deleteIds.includes(currentTabs.activeTabId || '') ? newTabList[Math.max(index - 1, 0)]?.id : currentTabs.activeTabId as string, + }); + uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, deleteIds); }, [navigate, organizationId, projectId, updateInsomniaTabs]); const closeAllTabsUnderWorkspace = useCallback((workspaceId: string) => { @@ -121,6 +157,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { if (!currentTabs) { return; } + const closeIds = currentTabs.tabList.filter(tab => tab.workspaceId === workspaceId).map(tab => tab.id); const newTabList = currentTabs.tabList.filter(tab => tab.workspaceId !== workspaceId); updateInsomniaTabs({ @@ -128,6 +165,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: newTabList, activeTabId: '', }); + uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, closeIds); }, [organizationId, updateInsomniaTabs]); const closeAllTabsUnderProject = useCallback((projectId: string) => { @@ -135,6 +173,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { if (!currentTabs) { return; } + const closeIds = currentTabs.tabList.filter(tab => tab.projectId === projectId).map(tab => tab.id); const newTabList = currentTabs.tabList.filter(tab => tab.projectId !== projectId); updateInsomniaTabs({ @@ -142,6 +181,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: newTabList, activeTabId: '', }); + uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, closeIds); }, [organizationId, updateInsomniaTabs]); const closeAllTabs = useCallback(() => { @@ -151,6 +191,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: [], activeTabId: '', }); + uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, 'all'); }, [navigate, organizationId, projectId, updateInsomniaTabs]); const closeOtherTabs = useCallback((id: string) => { @@ -171,6 +212,8 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: [reservedTab], activeTabId: id, }); + const closeIds = currentTabs.tabList.filter(tab => tab.id !== id).map(tab => tab.id); + uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, closeIds); }, [navigate, organizationId, updateInsomniaTabs]); const updateTabById = useCallback((tabId: string, patches: Partial) => { @@ -324,6 +367,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { closeAllTabsUnderProject, closeAllTabs, closeOtherTabs, + batchCloseTabs, addTab, updateTabById, changeActiveTab, diff --git a/packages/insomnia/src/ui/context/app/runner-context.tsx b/packages/insomnia/src/ui/context/app/runner-context.tsx new file mode 100644 index 00000000000..16bd2afb250 --- /dev/null +++ b/packages/insomnia/src/ui/context/app/runner-context.tsx @@ -0,0 +1,99 @@ +import React, { createContext, type FC, type PropsWithChildren, useCallback, useContext, useEffect } from 'react'; +import type { Selection } from 'react-aria-components'; + +import type { UploadDataType } from '../../components/modals/upload-runner-data-modal'; +import uiEventBus, { UIEventType } from '../../eventBus'; +import useStateRef from '../../hooks/use-state-ref'; +import type { RequestRow } from '../../routes/runner'; + +interface RunnerState { + selectedKeys: Selection; + iterationCount: number; + delay: number; + uploadData: UploadDataType[]; + advancedConfig: Record; + file: File | null; + reqList: RequestRow[]; +} + +interface OrgRunnerStateMap { + [runnerId: string]: Partial; +} + +interface RunnerStateMap { + [orgId: string]: OrgRunnerStateMap; +}; +interface ContextProps { + runnerStateMap: RunnerStateMap; + runnerStateRef?: React.MutableRefObject; + updateRunnerState: (organizationId: string, runnerId: string, patch: Partial) => void; +} +const RunnerContext = createContext({ + runnerStateMap: {}, + updateRunnerState: () => { }, +}); + +export const RunnerProvider: FC = ({ children }) => { + + const [runnerState, setRunnerState, runnerStateRef] = useStateRef({}); + + const updateRunnerState = useCallback((organizationId: string, runnerId: string, patch: Partial) => { + setRunnerState(prevState => { + const newState = { + ...prevState, + [organizationId]: { + ...prevState[organizationId], + [runnerId]: { ...prevState[organizationId]?.[runnerId], ...patch }, + }, + }; + return newState; + }); + }, [setRunnerState]); + + const handleTabClose = useCallback((organizationId: string, ids: 'all' | string[]) => { + if (ids === 'all') { + setRunnerState(prevState => { + const newState = { ...prevState }; + delete newState[organizationId]; + return newState; + }); + return; + } + + setRunnerState(prevState => { + const newOrgState = { ...prevState?.[organizationId] }; + ids.forEach(id => { + // runner tab id starts with 'runner' prefix, but the runnerId in this context doesn't have the prefix, so we need to remove it + if (id.startsWith('runner')) { + const runnerId = id.replace('runner_', ''); + delete newOrgState[runnerId]; + } + }); + return { + ...prevState, + [organizationId]: newOrgState, + }; + }); + }, [setRunnerState]); + + useEffect(() => { + uiEventBus.on(UIEventType.CLOSE_TAB, handleTabClose); + return () => { + uiEventBus.off(UIEventType.CLOSE_TAB, handleTabClose); + }; + }, [handleTabClose]); + + return ( + + {children} + + ); +}; + +export const useRunnerContext = () => useContext(RunnerContext); diff --git a/packages/insomnia/src/ui/eventBus.ts b/packages/insomnia/src/ui/eventBus.ts new file mode 100644 index 00000000000..90b354e126c --- /dev/null +++ b/packages/insomnia/src/ui/eventBus.ts @@ -0,0 +1,37 @@ +type EventHandler = (...args: any[]) => void; + +export enum UIEventType { + CLOSE_TAB = 'closeTab', +} +class EventBus { + private events: Record = { + [UIEventType.CLOSE_TAB]: [], + }; + + // Subscribe to event + on(event: UIEventType, handler: EventHandler): void { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(handler); + } + + // Unsubscribe from event + off(event: UIEventType, handler: EventHandler): void { + if (!this.events[event]) { + return; + } + this.events[event] = this.events[event].filter(h => h !== handler); + } + + // emit event + emit(event: UIEventType, ...args: any[]): void { + if (!this.events[event]) { + return; + } + this.events[event].forEach(handler => handler(...args)); + } +} + +const uiEventBus = new EventBus(); +export default uiEventBus; 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 785bf68b422..64a05997593 100644 --- a/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx +++ b/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx @@ -1,15 +1,14 @@ 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 { useRunnerContext } from '../context/app/runner-context'; import type { RequestRow } from '../routes/runner'; import type { Child, WorkspaceLoaderData } from '../routes/workspace'; -export const useRunnerRequestList = (workspaceId: string, targetFolderId: string) => { +export const useRunnerRequestList = (organizationId: string, targetFolderId: string, runnerId: string) => { const { collection } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; const entityMapRef = useRef(new Map()); @@ -52,24 +51,18 @@ export const useRunnerRequestList = (workspaceId: string, targetFolderId: string }); }, [collection, targetFolderId]); - const reqList = useListData({ - initialItems: requestRows, - }); - - const previousWorkspaceId = usePrevious(workspaceId); - const previousTargetFolderId = usePrevious(targetFolderId); + const { runnerStateMap, runnerStateRef, updateRunnerState } = useRunnerContext(); useEffect(() => { - if ((previousWorkspaceId && previousWorkspaceId !== workspaceId) || (previousTargetFolderId !== undefined && previousTargetFolderId !== targetFolderId)) { - // reset the list when workspace changes - const keys = reqList.items.map(item => item.id); - reqList.remove(...keys); - reqList.append(...requestRows); + if (!runnerStateRef?.current?.[organizationId]?.[runnerId]) { + updateRunnerState(organizationId, runnerId, { + reqList: requestRows, + }); } - }, [reqList, requestRows, workspaceId, targetFolderId, previousWorkspaceId, previousTargetFolderId]); + }, [organizationId, requestRows, runnerId, runnerStateRef, updateRunnerState]); return { - reqList, + reqList: runnerStateMap[organizationId]?.[runnerId]?.reqList || [], requestRows, entityMap: entityMapRef.current, }; diff --git a/packages/insomnia/src/ui/routes/organization.tsx b/packages/insomnia/src/ui/routes/organization.tsx index 40e413e1c14..78eb964b1ca 100644 --- a/packages/insomnia/src/ui/routes/organization.tsx +++ b/packages/insomnia/src/ui/routes/organization.tsx @@ -58,6 +58,7 @@ 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 { RunnerProvider } from '../context/app/runner-context'; import { useOrganizationPermissions } from '../hooks/use-organization-features'; import { syncProjects } from './project'; import { useRootLoaderData } from './root'; @@ -879,7 +880,9 @@ const OrganizationRoute = () => { } - + + + diff --git a/packages/insomnia/src/ui/routes/runner.tsx b/packages/insomnia/src/ui/routes/runner.tsx index 28a4aad27bd..0b2d0083c37 100644 --- a/packages/insomnia/src/ui/routes/runner.tsx +++ b/packages/insomnia/src/ui/routes/runner.tsx @@ -1,9 +1,9 @@ import { type RequestContext } from 'insomnia-sdk'; import porderedJSON from 'json-order'; -import React, { type FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { type FC, useCallback, useEffect, useMemo, 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'; +import { type ActionFunction, type LoaderFunction, useNavigate, useParams, useRouteLoaderData, useSearchParams, useSubmit } from 'react-router-dom'; import { useInterval } from 'react-use'; import { v4 as uuidv4 } from 'uuid'; @@ -15,6 +15,7 @@ import * as models from '../../models'; import type { UserUploadEnvironment } from '../../models/environment'; import type { RunnerResultPerRequest, RunnerTestResult } from '../../models/runner-test-result'; import { cancelRequestById } from '../../network/cancellation'; +import { moveAfter, moveBefore } from '../../utils'; import { invariant } from '../../utils/invariant'; import { SegmentEvent } from '../analytics'; import { Dropdown, DropdownItem, ItemContent } from '../components/base/dropdown'; @@ -30,6 +31,7 @@ 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 { useRunnerContext } from '../context/app/runner-context'; import { useRunnerRequestList } from '../hooks/use-runner-request-list'; import type { OrganizationLoaderData } from './organization'; import { type CollectionRunnerContext, defaultSendActionRuntime, type RunnerSource, sendActionImplementation } from './request'; @@ -108,44 +110,16 @@ export interface RequestRow { parentId: string; }; +const defaultAdvancedConfig = { + bail: true, +}; + export const Runner: FC<{}> = () => { - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams] = useSearchParams(); const [errorMsg, setErrorMsg] = useState(null); const { currentPlan } = useRouteLoaderData('/organization') as OrganizationLoaderData; const targetFolderId = searchParams.get('folder') || ''; - const shouldRefreshRef = useRef(false); - - 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')} - - - ), - }); - copySearchParams.delete('error'); - } else { - setErrorMsg(null); - } - - setSearchParams(copySearchParams); - } - }, [searchParams, setSearchParams]); const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; @@ -153,22 +127,22 @@ export const Runner: FC<{}> = () => { workspaceId: string; direction: 'vertical' | 'horizontal'; }; - const [iterationCount, setIterationCount] = useState(1); - const [delay, setDelay] = useState(0); - const [uploadData, setUploadData] = useState([]); - const [file, setFile] = useState(null); - const [bail, setBail] = useState(true); const [keepLog, setKeepLog] = useState(true); const [isRunning, setIsRunning] = useState(false); - invariant(iterationCount, 'iterationCount should not be null'); + // For backward compatibility,the runnerId we use for testResult in database is no prefix with 'runner_' + const runnerId = targetFolderId ? targetFolderId : workspaceId; const { settings } = useRootLoaderData(); 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); + const { runnerStateMap, updateRunnerState } = useRunnerContext(); + const { iterationCount = 1, delay = 0, selectedKeys = new Set(), advancedConfig = defaultAdvancedConfig, uploadData = [], file } = runnerStateMap?.[organizationId]?.[runnerId] || {}; + invariant(iterationCount, 'iterationCount should not be null'); + + const { reqList, requestRows, entityMap } = useRunnerRequestList(organizationId, targetFolderId, runnerId); useEffect(() => { if (settings.forceVerticalLayout) { @@ -192,14 +166,14 @@ export const Runner: FC<{}> = () => { }, [settings.forceVerticalLayout, direction]); const isConsistencyChanged = useMemo(() => { - if (requestRows.length !== reqList.items.length) { + if (requestRows.length !== reqList.length) { return true; - } else if (reqList.selectedKeys !== 'all' && Array.from(reqList.selectedKeys).length !== requestRows.length) { + } else if (selectedKeys !== 'all' && Array.from(selectedKeys).length !== requestRows.length) { return true; } - return requestRows.some((row: RequestRow, index: number) => row.id !== reqList.items[index].id); - }, [requestRows, reqList]); + return requestRows.some((row: RequestRow, index: number) => row.id !== reqList[index].id); + }, [reqList, requestRows, selectedKeys]); const { dragAndDropHooks: requestsDnD } = useDragAndDrop({ getItems: keys => { @@ -212,11 +186,13 @@ export const Runner: FC<{}> = () => { }); }, onReorder: event => { + let newList = reqList; if (event.target.dropPosition === 'before') { - reqList.moveBefore(event.target.key, event.keys); + newList = moveBefore(reqList, event.target.key, event.keys); } else if (event.target.dropPosition === 'after') { - reqList.moveAfter(event.target.key, event.keys); + newList = moveAfter(reqList, event.target.key, event.keys); } + updateRunnerState(organizationId, runnerId, { reqList: newList }); }, renderDragPreview(items) { return ( @@ -227,7 +203,7 @@ export const Runner: FC<{}> = () => { }, renderDropIndicator(target) { if (target.type === 'item') { - const item = reqList.items.find(item => item.id === target.key); + const item = reqList.find(item => item.id === target.key); if (item) { return ( = () => { window.main.trackSegmentEvent({ event: SegmentEvent.collectionRunExecute, properties: { plan: currentPlan?.type || 'scratchpad', iterations: iterationCount } }); - const selected = new Set(reqList.selectedKeys); - const requests = Array.from(reqList.items) - .filter(item => selected.has(item.id)); + const selected = new Set(selectedKeys); + const requests = reqList.filter(item => selected.has(item.id)); // convert uploadData to environment data const userUploadEnvs = uploadData.map(data => { @@ -274,7 +249,7 @@ export const Runner: FC<{}> = () => { iterationCount, userUploadEnvs, delay, - bail, + bail: advancedConfig?.bail, keepLog, targetFolderId: targetFolderId || '', }; @@ -284,6 +259,7 @@ export const Runner: FC<{}> = () => { method: 'post', encType: 'application/json', action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner/run`, + navigate: false, } ); }; @@ -293,29 +269,24 @@ export const Runner: FC<{}> = () => { navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}`); }; const onToggleSelection = () => { - if (Array.from(reqList.selectedKeys).length === Array.from(reqList.items).length) { + if (Array.from(selectedKeys).length === reqList.length) { // unselect all - reqList.setSelectedKeys(new Set([])); + updateRunnerState(organizationId, runnerId, { selectedKeys: new Set([]) }); } else { // select all - reqList.setSelectedKeys(new Set(reqList.items.map(item => item.id))); + const allKeys = reqList.map(item => item.id); + updateRunnerState(organizationId, runnerId, { selectedKeys: new Set(allKeys) }); } }; const [testHistory, setTestHistory] = useState([]); useEffect(() => { const readResults = async () => { - const results = await models.runnerTestResult.findByParentId(workspaceId) || []; + const results = await models.runnerTestResult.findByParentId(runnerId) || []; setTestHistory(results.reverse()); }; readResults(); - }, [workspaceId]); - - useEffect(() => { - if (uploadData.length >= 1) { - setIterationCount(uploadData.length); - } - }, [setIterationCount, uploadData]); + }, [runnerId]); const [timingSteps, setTimingSteps] = useState([]); const [totalTime, setTotalTime] = useState({ @@ -337,44 +308,72 @@ export const Runner: FC<{}> = () => { if (executionResult) { const mergedTimelines = await aggregateAllTimelines(errorMsg, executionResult); setTimelines(mergedTimelines); + } else { + setTimelines([]); } }; refreshTimeline(); }, [executionResult, errorMsg]); - useInterval(() => { - const refreshPanes = async () => { - const latestTimingSteps = await window.main.getExecution({ requestId: workspaceId }); - if (latestTimingSteps) { - // there is a timingStep item and it is not ended (duration is not assigned) - const isRunning = latestTimingSteps.length > 0 && latestTimingSteps[latestTimingSteps.length - 1].stepName !== 'Done'; - setIsRunning(isRunning); - - if (isRunning) { - const duration = Date.now() - latestTimingSteps[latestTimingSteps.length - 1].startedAt; - const { number: durationNumber, unit: durationUnit } = getTimeAndUnit(duration); - - setTimingSteps(latestTimingSteps); - setTotalTime({ - duration: durationNumber, - unit: durationUnit, - }); - } else { - if (shouldRefreshRef.current) { - const results = await models.runnerTestResult.findByParentId(workspaceId) || []; - setTestHistory(results.reverse()); - if (results.length > 0) { - const latestResult = results[0]; - setExecutionResult(latestResult); - } - shouldRefreshRef.current = false; - } + const showErrorAlert = (error: string) => { + showAlert({ + title: 'Unexpected Runner Failure', + message: ( + + The runner failed due to an unhandled error: + + {error} + + + ), + }); + }; + + const refreshPanes = useCallback(async () => { + const latestTimingSteps = await window.main.getExecution({ requestId: runnerId }); + let isRunning = false; + if (latestTimingSteps) { + // there is a timingStep item and it is not ended (duration is not assigned) + isRunning = latestTimingSteps.length > 0 && latestTimingSteps[latestTimingSteps.length - 1].stepName !== 'Done'; + } + setIsRunning(isRunning); + + if (isRunning) { + const duration = Date.now() - latestTimingSteps[latestTimingSteps.length - 1].startedAt; + const { number: durationNumber, unit: durationUnit } = getTimeAndUnit(duration); + setTimingSteps(latestTimingSteps); + setTotalTime({ + duration: durationNumber, + unit: durationUnit, + }); + } else { + const results = await models.runnerTestResult.findByParentId(runnerId) || []; + // show execution result + if (results.length > 0) { + setTestHistory(results.reverse()); + const latestResult = results[0]; + setExecutionResult(latestResult); + const { error } = getExecution(runnerId); + if (error) { + setErrorMsg(error); + showErrorAlert(error); + updateExecution(runnerId, { error: '' }); } + } else { + // show initial empty panel + setExecutionResult(null); + setErrorMsg(null); } - }; + } + }, [runnerId]); + + useInterval(() => { + refreshPanes(); + }, isRunning ? 1000 : null); + useEffect(() => { refreshPanes(); - }, 1000); + }, [refreshPanes]); const { passedTestCount, totalTestCount, testResultCountTagColor } = useMemo(() => { let passedTestCount = 0; @@ -407,11 +406,11 @@ export const Runner: FC<{}> = () => { setSelectedTab('test-results'); }, [setSelectedTab]); - const allKeys = reqList.items.map(item => item.id); + const allKeys = reqList.map(item => item.id); const disabledKeys = useMemo(() => { return isRunning ? allKeys : []; }, [isRunning, allKeys]); - const isDisabled = isRunning || Array.from(reqList.selectedKeys).length === 0; + const isDisabled = isRunning || Array.from(selectedKeys).length === 0; const [deletedItems, setDeletedItems] = useState([]); const deleteHistoryItem = (item: RunnerTestResult) => { @@ -421,13 +420,13 @@ export const Runner: FC<{}> = () => { const selectedRequestIdsForCliCommand = targetFolderId !== null && targetFolderId !== '' - ? Array.from(reqList.items) + ? reqList .filter(item => item.ancestorIds.includes(targetFolderId)) .map(item => item.id) - .filter(id => new Set(reqList.selectedKeys).has(id)) - : Array.from(reqList.items) + .filter(id => selectedKeys === 'all' || selectedKeys.has(id)) + : reqList .map(item => item.id) - .filter(id => new Set(reqList.selectedKeys).has(id)); + .filter(id => selectedKeys === 'all' || selectedKeys.has(id)); return ( <> @@ -447,7 +446,7 @@ export const Runner: FC<{}> = () => { onChange={e => { try { if (parseInt(e.target.value, 10) > 0) { - setIterationCount(parseInt(e.target.value, 10)); + updateRunnerState(organizationId, runnerId, { iterationCount: parseInt(e.target.value, 10) }); } } catch (ex) { } }} @@ -465,7 +464,7 @@ export const Runner: FC<{}> = () => { try { const delay = parseInt(e.target.value, 10); if (delay >= 0) { - setDelay(delay); // also update the temp settings + updateRunnerState(organizationId, runnerId, { delay }); // also update the temp settings } } catch (ex) { } }} @@ -546,9 +545,9 @@ export const Runner: FC<{}> = () => { { - Array.from(reqList.selectedKeys).length === Array.from(reqList.items).length ? + Array.from(selectedKeys).length === Array.from(reqList).length ? Unselect All : - Array.from(reqList.selectedKeys).length === 0 ? + Array.from(selectedKeys).length === 0 ? Select All : Select All } @@ -557,11 +556,12 @@ export const Runner: FC<{}> = () => { { + updateRunnerState(organizationId, runnerId, { selectedKeys: keys }); + }} aria-label="Request Collection" dragAndDropHooks={requestsDnD} className="w-full h-full leading-8 text-base overflow-auto" @@ -621,10 +621,17 @@ export const Runner: FC<{}> = () => { setBail(!bail)} + onChange={() => { + updateRunnerState(organizationId, runnerId, { + advancedConfig: { + ...advancedConfig, + bail: !advancedConfig?.bail, + }, + }); + }} type="checkbox" disabled={isRunning} - checked={bail} + checked={advancedConfig?.bail} /> Stop run if an error occurs @@ -672,14 +679,17 @@ export const Runner: FC<{}> = () => { iterationCount={iterationCount} delay={delay} filePath={file?.path || ''} - bail={bail} + bail={advancedConfig?.bail} /> )} {showUploadModal && ( { - setFile(file); - setUploadData(uploadData); // also update the temp settings + updateRunnerState(organizationId, runnerId, { + uploadData, + file, + iterationCount: uploadData.length >= 1 ? uploadData.length : iterationCount, + }); }} userUploadData={uploadData} onClose={() => setShowUploadModal(false)} @@ -734,7 +744,7 @@ export const Runner: FC<{}> = () => { @@ -753,8 +763,8 @@ export const Runner: FC<{}> = () => { {isRunning && cancelExecution(workspaceId)} - activeRequestId={workspaceId} + handleCancel={() => cancelExecution(runnerId)} + activeRequestId={runnerId} steps={timingSteps} /> @@ -801,31 +811,34 @@ const RequestItem = ( // This is required for tracking the active request for one runner execution // Then in runner cancellation, both the active request and the runner execution will be canceled // TODO(george): Potentially it could be merged with maps in request-timing.ts and cancellation.ts -const runnerExecutions = new Map(); +interface ExecutionInfo { + activeRequestId?: string; + error?: string; +}; +const runnerExecutions = new Map(); function startExecution(workspaceId: string) { - runnerExecutions.set(workspaceId, ''); + runnerExecutions.set(workspaceId, {}); } -function stopExecution(workspaceId: string) { - runnerExecutions.delete(workspaceId); -} - -function updateExecution(workspaceId: string, requestId: string) { - runnerExecutions.set(workspaceId, requestId); +function updateExecution(workspaceId: string, executionInfo: ExecutionInfo) { + const info = runnerExecutions.get(workspaceId); + runnerExecutions.set(workspaceId, { + ...info, + ...executionInfo, + }); } function getExecution(workspaceId: string) { - return runnerExecutions.get(workspaceId); + return runnerExecutions.get(workspaceId) || {}; } function cancelExecution(workspaceId: string) { - const activeRequestId = getExecution(workspaceId); + const { activeRequestId } = getExecution(workspaceId); if (activeRequestId) { cancelRequestById(activeRequestId); window.main.completeExecutionStep({ requestId: activeRequestId }); window.main.updateLatestStepName({ requestId: workspaceId, stepName: 'Done' }); window.main.completeExecutionStep({ requestId: workspaceId }); - stopExecution(workspaceId); } } const wrapAroundIterationOverIterationData = (list?: UserUploadEnvironment[], currentIteration?: number): UserUploadEnvironment | undefined => { @@ -856,6 +869,7 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = const { requests, iterationCount, delay, userUploadEnvs, bail, targetFolderId, keepLog } = await request.json() as runCollectionActionParams; const source: RunnerSource = 'runner'; + const runnerId = targetFolderId ? targetFolderId : workspaceId; let testCtx: CollectionRunnerContext = { source, @@ -880,12 +894,12 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = }, }; - window.main.startExecution({ requestId: workspaceId }); + window.main.startExecution({ requestId: runnerId }); window.main.addExecutionStep({ - requestId: workspaceId, + requestId: runnerId, stepName: 'Initializing', }); - startExecution(workspaceId); + startExecution(runnerId); const noLogRuntime = { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -904,7 +918,7 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = let j = 0; while (j < requests.length) { // TODO: we might find a better way to do runner cancellation - if (getExecution(workspaceId) === undefined) { + if (getExecution(runnerId) === undefined) { throw 'Runner has been stopped'; } @@ -939,9 +953,11 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = } } - updateExecution(workspaceId, targetRequest.id); + updateExecution(runnerId, { + activeRequestId: targetRequest.id, + }); window.main.updateLatestStepName({ - requestId: workspaceId, + requestId: runnerId, stepName: `Iteration ${i + 1} - Executing ${j + 1} of ${requests.length} requests - "${targetRequest.name}"`, }); @@ -1035,17 +1051,20 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = }; } - window.main.updateLatestStepName({ requestId: workspaceId, stepName: 'Done' }); - window.main.completeExecutionStep({ requestId: workspaceId }); + window.main.updateLatestStepName({ requestId: runnerId, stepName: 'Done' }); + window.main.completeExecutionStep({ requestId: runnerId }); } catch (e) { // the error could be from third party - const errMsg = encodeURIComponent(e.error || e); - return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner?refresh-pane&error=${errMsg}&folder=${targetFolderId}`); + const errMsg = e.error || e; + updateExecution(runnerId, { + error: errMsg, + }); + return null; } finally { - cancelExecution(workspaceId); + cancelExecution(runnerId); await models.runnerTestResult.create({ - parentId: workspaceId, + parentId: runnerId, source: testCtx.source, iterations: testCtx.iterationCount, duration: testCtx.duration, @@ -1054,8 +1073,7 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = responsesInfo: testCtx.responsesInfo, }); } - - return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner?refresh-pane&folder=${targetFolderId}`); + return null; }; export const collectionRunnerStatusLoader: LoaderFunction = async ({ params }) => { diff --git a/packages/insomnia/src/utils/index.ts b/packages/insomnia/src/utils/index.ts index 94b3ba88edc..cc032437e2d 100644 --- a/packages/insomnia/src/utils/index.ts +++ b/packages/insomnia/src/utils/index.ts @@ -1,6 +1,76 @@ +import type { Key } from 'react-stately'; + 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); } }; + +// modify base on react-spectrum +// https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/data/src/useListData.ts#L279 +function move(list: T[], indices: number[], toIndex: number): T[] { + // Shift the target down by the number of items being moved from before the target + toIndex -= indices.filter(index => index < toIndex).length; + + const moves = indices.map(from => ({ + from, + to: toIndex++, + })); + + // Shift later from indices down if they have a larger index + for (let i = 0; i < moves.length; i++) { + const a = moves[i].from; + for (let j = i; j < moves.length; j++) { + const b = moves[j].from; + + if (b > a) { + moves[j].from--; + } + } + } + + // Interleave the moves so they can be applied one by one rather than all at once + for (let i = 0; i < moves.length; i++) { + const a = moves[i]; + for (let j = moves.length - 1; j > i; j--) { + const b = moves[j]; + + if (b.from < a.to) { + a.to++; + } else { + b.from++; + } + } + } + + const copy = list.slice(); + for (const move of moves) { + const [item] = copy.splice(move.from, 1); + copy.splice(move.to, 0, item); + } + + return copy; +} +export const moveBefore = (list: any[], key: Key, keys: Iterable) => { + const toIndex = list.findIndex(item => item.id === key); + if (toIndex === -1) { + return list; + } + + // Find indices of keys to move. Sort them so that the order in the list is retained. + const keyArray = Array.isArray(keys) ? keys : [...keys]; + const indices = keyArray.map(key => list.findIndex(item => item.id === key)).sort((a, b) => a - b); + return move(list, indices, toIndex); +}; + +export const moveAfter = (list: any[], key: Key, keys: Iterable) => { + const toIndex = list.findIndex(item => item.id === key); + if (toIndex === -1) { + return list; + } + + const keyArray = Array.isArray(keys) ? keys : [...keys]; + const indices = keyArray.map(key => list.findIndex(item => item.id === key)).sort((a, b) => a - b); + return move(list, indices, toIndex + 1); +}; From a8523a2965c4fec0e2e81774a86d90e256cb03f6 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Tue, 7 Jan 2025 15:27:02 +0800 Subject: [PATCH 36/47] remove debugger --- packages/insomnia/src/ui/components/tabs/tabList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 2d9c2219c97..68ba2e30215 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -154,7 +154,6 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' const workspace = await models.workspace.getById(patchObj.parentId); if (workspace) { if (isRequest(doc)) { - debugger; updateTabById?.(doc._id, { workspaceId: workspace._id, workspaceName: workspace.name, From 440591d109b5f94534e175624aafb80c063a68b7 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Tue, 7 Jan 2025 15:55:52 +0800 Subject: [PATCH 37/47] use own debounce instead lodash --- packages/insomnia/src/ui/components/tabs/tabList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 68ba2e30215..f1500236b4f 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -1,9 +1,9 @@ -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, useParams } from 'react-router-dom'; import { type ChangeBufferEvent, type ChangeType, database } from '../../../common/database'; +import { debounce } from '../../../common/misc'; import * as models from '../../../models/index'; import type { MockRoute } from '../../../models/mock-route'; import { isRequest, type Request } from '../../../models/request'; @@ -237,7 +237,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' } }; - const debouncedOnResize = _.debounce<(size: Size) => void>(onResize, 500); + const debouncedOnResize = debounce<(size: Size) => void>(onResize, 500); useResizeObserver(tabListWrapperRef, debouncedOnResize); From 67942c88566b21b2c1858f840ccce6ff57658738 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Tue, 7 Jan 2025 16:07:19 +0800 Subject: [PATCH 38/47] feat: move runner keep log to context --- packages/insomnia/src/ui/routes/runner.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/insomnia/src/ui/routes/runner.tsx b/packages/insomnia/src/ui/routes/runner.tsx index 0b2d0083c37..bb6814bdb5d 100644 --- a/packages/insomnia/src/ui/routes/runner.tsx +++ b/packages/insomnia/src/ui/routes/runner.tsx @@ -112,6 +112,7 @@ export interface RequestRow { const defaultAdvancedConfig = { bail: true, + keepLog: true, }; export const Runner: FC<{}> = () => { @@ -127,7 +128,6 @@ export const Runner: FC<{}> = () => { workspaceId: string; direction: 'vertical' | 'horizontal'; }; - const [keepLog, setKeepLog] = useState(true); const [isRunning, setIsRunning] = useState(false); // For backward compatibility,the runnerId we use for testResult in database is no prefix with 'runner_' @@ -250,7 +250,7 @@ export const Runner: FC<{}> = () => { userUploadEnvs, delay, bail: advancedConfig?.bail, - keepLog, + keepLog: advancedConfig?.keepLog, targetFolderId: targetFolderId || '', }; submit( @@ -608,10 +608,17 @@ export const Runner: FC<{}> = () => { setKeepLog(!keepLog)} + onChange={() => { + updateRunnerState(organizationId, runnerId, { + advancedConfig: { + ...advancedConfig, + keepLog: !advancedConfig?.keepLog, + }, + }); + }} type="checkbox" disabled={isRunning} - checked={keepLog} + checked={advancedConfig?.keepLog} /> Keep logs after run Disabling this will improve the performance while logs are not saved. From 9add6e9cba82d2b59a6120e88f5dac686199c660 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 9 Jan 2025 14:29:05 +0800 Subject: [PATCH 39/47] convert eventbus type enum to union --- .../ui/context/app/insomnia-tab-context.tsx | 18 +++++++++--------- .../src/ui/context/app/runner-context.tsx | 4 ++-- packages/insomnia/src/ui/eventBus.ts | 7 +++---- 3 files changed, 14 insertions(+), 15 deletions(-) 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 68b634407f7..0b51cdefcb5 100644 --- a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -4,7 +4,7 @@ import { useLocalStorage } from 'react-use'; import type { BaseTab } from '../../components/tabs/tab'; import type { OrganizationTabs } from '../../components/tabs/tabList'; -import uiEventBus, { UIEventType } from '../../eventBus'; +import uiEventBus from '../../eventBus'; interface UpdateInsomniaTabParams { organizationId: string; @@ -99,7 +99,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: [], activeTabId: '', }); - uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, [id]); + uiEventBus.emit('CLOSE_TAB', organizationId, [id]); return; } @@ -117,7 +117,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: newTabList, activeTabId: currentTabs.activeTabId === id ? newTabList[Math.max(index - 1, 0)]?.id : currentTabs.activeTabId as string, }); - uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, [id]); + uiEventBus.emit('CLOSE_TAB', organizationId, [id]); }, [navigate, organizationId, projectId, updateInsomniaTabs]); const batchCloseTabs = useCallback((deleteIds: string[]) => { @@ -133,7 +133,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: [], activeTabId: '', }); - uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, 'all'); + uiEventBus.emit('CLOSE_TAB', organizationId, 'all'); return; } @@ -149,7 +149,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: newTabList, activeTabId: deleteIds.includes(currentTabs.activeTabId || '') ? newTabList[Math.max(index - 1, 0)]?.id : currentTabs.activeTabId as string, }); - uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, deleteIds); + uiEventBus.emit('CLOSE_TAB', organizationId, deleteIds); }, [navigate, organizationId, projectId, updateInsomniaTabs]); const closeAllTabsUnderWorkspace = useCallback((workspaceId: string) => { @@ -165,7 +165,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: newTabList, activeTabId: '', }); - uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, closeIds); + uiEventBus.emit('CLOSE_TAB', organizationId, closeIds); }, [organizationId, updateInsomniaTabs]); const closeAllTabsUnderProject = useCallback((projectId: string) => { @@ -181,7 +181,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: newTabList, activeTabId: '', }); - uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, closeIds); + uiEventBus.emit('CLOSE_TAB', organizationId, closeIds); }, [organizationId, updateInsomniaTabs]); const closeAllTabs = useCallback(() => { @@ -191,7 +191,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { tabList: [], activeTabId: '', }); - uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, 'all'); + uiEventBus.emit('CLOSE_TAB', organizationId, 'all'); }, [navigate, organizationId, projectId, updateInsomniaTabs]); const closeOtherTabs = useCallback((id: string) => { @@ -213,7 +213,7 @@ export const InsomniaTabProvider: FC = ({ children }) => { activeTabId: id, }); const closeIds = currentTabs.tabList.filter(tab => tab.id !== id).map(tab => tab.id); - uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, closeIds); + uiEventBus.emit('CLOSE_TAB', organizationId, closeIds); }, [navigate, organizationId, updateInsomniaTabs]); const updateTabById = useCallback((tabId: string, patches: Partial) => { diff --git a/packages/insomnia/src/ui/context/app/runner-context.tsx b/packages/insomnia/src/ui/context/app/runner-context.tsx index 16bd2afb250..d3646cc5dfa 100644 --- a/packages/insomnia/src/ui/context/app/runner-context.tsx +++ b/packages/insomnia/src/ui/context/app/runner-context.tsx @@ -77,9 +77,9 @@ export const RunnerProvider: FC = ({ children }) => { }, [setRunnerState]); useEffect(() => { - uiEventBus.on(UIEventType.CLOSE_TAB, handleTabClose); + uiEventBus.on('CLOSE_TAB', handleTabClose); return () => { - uiEventBus.off(UIEventType.CLOSE_TAB, handleTabClose); + uiEventBus.off('CLOSE_TAB', handleTabClose); }; }, [handleTabClose]); diff --git a/packages/insomnia/src/ui/eventBus.ts b/packages/insomnia/src/ui/eventBus.ts index 90b354e126c..973c6819347 100644 --- a/packages/insomnia/src/ui/eventBus.ts +++ b/packages/insomnia/src/ui/eventBus.ts @@ -1,11 +1,10 @@ type EventHandler = (...args: any[]) => void; -export enum UIEventType { - CLOSE_TAB = 'closeTab', -} +type UIEventType = 'CLOSE_TAB'; + class EventBus { private events: Record = { - [UIEventType.CLOSE_TAB]: [], + CLOSE_TAB: [], }; // Subscribe to event From e90293967fcb0fd38d5ec8c2b6e57058cc5e0480 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 9 Jan 2025 16:01:52 +0800 Subject: [PATCH 40/47] reduce duplicate useInsomniaTabContext --- packages/insomnia/src/ui/components/tabs/tabList.tsx | 8 ++++++-- packages/insomnia/src/ui/context/app/runner-context.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index f1500236b4f..aca0729a8dd 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -40,8 +40,6 @@ export const TAB_ROUTER_PATH: Record = { }; export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' }) => { - const { currentOrgTabs, batchUpdateTabs, moveBefore, moveAfter } = useInsomniaTabContext(); - const { tabList, activeTabId } = currentOrgTabs; const [showAddRequestModal, setShowAddRequestModal] = useState(false); const [isOverFlow, setIsOverFlow] = useState(false); @@ -62,8 +60,14 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' updateWorkspaceName, closeAllTabs, closeOtherTabs, + moveAfter, + moveBefore, + batchUpdateTabs, + currentOrgTabs, } = useInsomniaTabContext(); + const { tabList, activeTabId } = currentOrgTabs; + const handleSelectionChange = (keys: Selection) => { if (keys !== 'all') { const key = [...keys.values()]?.[0] as string; diff --git a/packages/insomnia/src/ui/context/app/runner-context.tsx b/packages/insomnia/src/ui/context/app/runner-context.tsx index d3646cc5dfa..ff9e8052901 100644 --- a/packages/insomnia/src/ui/context/app/runner-context.tsx +++ b/packages/insomnia/src/ui/context/app/runner-context.tsx @@ -2,7 +2,7 @@ import React, { createContext, type FC, type PropsWithChildren, useCallback, use import type { Selection } from 'react-aria-components'; import type { UploadDataType } from '../../components/modals/upload-runner-data-modal'; -import uiEventBus, { UIEventType } from '../../eventBus'; +import uiEventBus from '../../eventBus'; import useStateRef from '../../hooks/use-state-ref'; import type { RequestRow } from '../../routes/runner'; From 62a1028835aa00d9f61c8fe9a1a995c5bc242585 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Thu, 9 Jan 2025 17:35:52 +0800 Subject: [PATCH 41/47] fix search bar style --- packages/insomnia/src/ui/components/command-palette.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/insomnia/src/ui/components/command-palette.tsx b/packages/insomnia/src/ui/components/command-palette.tsx index 8301ad562a4..85345540f2a 100644 --- a/packages/insomnia/src/ui/components/command-palette.tsx +++ b/packages/insomnia/src/ui/components/command-palette.tsx @@ -36,9 +36,11 @@ export const CommandPalette = memo(function CommandPalette() { onOpenChange={setIsOpen} isOpen={isOpen} > - - - Search.. + + + + Search.. + {requestSwitchKeyCombination && {constructKeyCombinationDisplay(requestSwitchKeyCombination, false)} } From c0415c797b31dda0b02c2a4fbe33fc3c3e0593ff Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Mon, 13 Jan 2025 11:10:58 +0800 Subject: [PATCH 42/47] covert enum to union --- .../insomnia/src/ui/components/tabs/tab.tsx | 36 +++------ .../src/ui/components/tabs/tabList.tsx | 24 +++--- .../insomnia/src/ui/hooks/use-insomnia-tab.ts | 76 ++++++++++--------- 3 files changed, 63 insertions(+), 73 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index 8695a2b302d..08458d28073 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -7,21 +7,9 @@ import { Icon } from '../icon'; import { Tooltip } from '../tooltip'; import { TAB_CONTEXT_MENU_COMMAND } from './tabList'; -export 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 type TabType = 'request' | 'folder' | 'environment' | 'mockServer' | 'mockRoute' | 'document' | 'collection' | 'runner' | 'test' | 'testSuite'; export interface BaseTab { - type: TabEnum; + type: TabType; name: string; url: string; organizationId: string; @@ -49,23 +37,23 @@ const REQUEST_METHOD_STYLE_MAP: Record = { 'gRPC': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]', }; -const WORKSPACE_TAB_UI_MAP: Record = { - [TabEnum.Collection]: { +const WORKSPACE_TAB_UI_MAP: Partial> = { + request: { icon: 'bars', bgColor: 'bg-[--color-surprise]', textColor: 'text-[--color-font-surprise]', }, - [TabEnum.Env]: { + environment: { icon: 'code', bgColor: 'bg-[--color-font]', textColor: 'text-[--color-bg]', }, - [TabEnum.Mock]: { + mockServer: { icon: 'server', bgColor: 'bg-[--color-warning]', textColor: 'text-[--color-font-warning]', }, - [TabEnum.Document]: { + document: { icon: 'file', bgColor: 'bg-[--color-info]', textColor: 'text-[--color-font-info]', @@ -76,7 +64,7 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { const { closeTabById, currentOrgTabs } = useInsomniaTabContext(); - const renderTabIcon = (type: TabEnum) => { + const renderTabIcon = (type: TabType) => { if (WORKSPACE_TAB_UI_MAP[type]) { return ( @@ -85,20 +73,20 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { ); } - if (type === TabEnum.Request || type === TabEnum.MockRoute) { + if (type === 'request' || type === 'mockRoute') { return ( {tab.tag} ); } - if (type === TabEnum.Folder) { + if (type === 'folder') { return ; } - if (type === TabEnum.Runner) { + if (type === 'runner') { return ; }; - if (type === TabEnum.TESTSUITE) { + if (type === 'testSuite') { return ; } diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index aca0729a8dd..81c796b213b 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -14,7 +14,7 @@ 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'; -import { type BaseTab, InsomniaTab, TabEnum } from './tab'; +import { type BaseTab, InsomniaTab, type TabType } from './tab'; export interface OrganizationTabs { tabList: BaseTab[]; @@ -26,17 +26,17 @@ export const enum TAB_CONTEXT_MENU_COMMAND { 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', - [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 TAB_ROUTER_PATH: Record = { + collection: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug', + folder: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request-group/:requestGroupId', + request: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId', + environment: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/environment', + mockServer: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server', + runner: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/runner', + document: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/spec', + mockRoute: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server/mock-route/:mockRouteId', + test: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test', + testSuite: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/*', }; export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' }) => { diff --git a/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts b/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts index 7bade7f96c5..20313b348b5 100644 --- a/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts +++ b/packages/insomnia/src/ui/hooks/use-insomnia-tab.ts @@ -9,7 +9,7 @@ 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 { type BaseTab, type TabType } 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'; @@ -42,57 +42,57 @@ export const useInsomniaTab = ({ const location = useLocation(); const [searchParams] = useSearchParams(); - const generateTabUrl = useCallback((type: TabEnum) => { - if (type === TabEnum.Request) { + const generateTabUrl = useCallback((type: TabType) => { + if (type === 'request') { return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${activeRequest?._id}`; } - if (type === TabEnum.Folder) { + if (type === 'folder') { return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/${activeRequestGroup?._id}`; } - if (type === TabEnum.Collection) { + if (type === 'collection') { return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug?doNotSkipToActiveRequest=true`; } - if (type === TabEnum.Env) { + if (type === 'environment') { return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment`; } - if (type === TabEnum.Runner) { + if (type === 'runner') { return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner${location.search}`; } - if (type === TabEnum.Mock) { + if (type === 'mockServer') { return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server`; } - if (type === TabEnum.MockRoute) { + if (type === 'mockRoute') { return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${activeMockRoute?._id}`; } - if (type === TabEnum.Document) { + if (type === 'document') { return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/spec`; } - if (type === TabEnum.TEST) { + if (type === 'test') { return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test`; } - if (type === TabEnum.TESTSUITE) { + if (type === 'testSuite') { return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite?._id}`; } return ''; }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, location.search, organizationId, projectId, unitTestSuite?._id, workspaceId]); - const getTabType = (pathname: string) => { + const getTabType = (pathname: string): TabType | null => { for (const type in TAB_ROUTER_PATH) { const ifMatch = matchPath({ - path: TAB_ROUTER_PATH[type as TabEnum], + path: TAB_ROUTER_PATH[type as TabType], end: true, }, pathname); if (ifMatch) { - return type as TabEnum; + return type as TabType; } } @@ -107,76 +107,77 @@ export const useInsomniaTab = ({ return `runner_${workspaceId}`; }, [searchParams, workspaceId]); - const getCurrentTab = useCallback((type: TabEnum | null) => { + const getCurrentTab = useCallback((type: TabType | null) => { if (!type) { return undefined; } const currentOrgTabs = appTabsRef?.current?.[organizationId]; - if (type === TabEnum.Request) { + if (type === 'request') { return currentOrgTabs?.tabList.find(tab => tab.id === activeRequest?._id); } - if (type === TabEnum.Folder) { + if (type === 'folder') { return currentOrgTabs?.tabList.find(tab => tab.id === activeRequestGroup?._id); } - if (type === TabEnum.Runner) { + if (type === 'runner') { // collection runner tab id is prefixed with 'runner_' const runnerTabId = getRunnerTabId(); return currentOrgTabs?.tabList.find(tab => tab.id === runnerTabId); } - if (type === TabEnum.MockRoute) { + if (type === 'mockRoute') { return currentOrgTabs?.tabList.find(tab => tab.id === activeMockRoute?._id); } - if (type === TabEnum.TESTSUITE) { + if (type === 'testSuite') { return currentOrgTabs?.tabList.find(tab => tab.id === unitTestSuite?._id); } - if ([TabEnum.Collection, TabEnum.Document, TabEnum.Env, TabEnum.Mock, TabEnum.TEST].includes(type)) { + const collectionTabTypes: TabType[] = ['collection', 'document', 'environment', 'mockServer', 'test']; + if (collectionTabTypes.includes(type)) { return currentOrgTabs?.tabList.find(tab => tab.id === workspaceId); } return undefined; }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, appTabsRef, getRunnerTabId, organizationId, unitTestSuite?._id, workspaceId]); - const getTabId = useCallback((type: TabEnum | null): string => { + const getTabId = useCallback((type: TabType | null): string => { if (!type) { return ''; } - if (type === TabEnum.Request) { + if (type === 'request') { return activeRequest?._id || ''; } - if (type === TabEnum.Folder) { + if (type === 'folder') { return activeRequestGroup?._id || ''; } - if (type === TabEnum.Runner) { + if (type === 'runner') { const runnerTabId = getRunnerTabId(); return runnerTabId; } - if (type === TabEnum.MockRoute) { + if (type === 'mockRoute') { return activeMockRoute?._id || ''; } - if (type === TabEnum.TESTSUITE) { + if (type === 'testSuite') { return unitTestSuite?._id || ''; } - - if ([TabEnum.Collection, TabEnum.Document, TabEnum.Env, TabEnum.Mock, TabEnum.TEST].includes(type)) { + const collectionTabTypes: TabType[] = ['collection', 'document', 'environment', 'mockServer', 'test']; + if (collectionTabTypes.includes(type)) { return workspaceId; } return ''; }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, getRunnerTabId, unitTestSuite?._id, workspaceId]); - const packTabInfo = useCallback((type: TabEnum): BaseTab | undefined => { + const packTabInfo = useCallback((type: TabType): BaseTab | undefined => { if (!type) { return undefined; } - if (type === TabEnum.Request) { + if (type === 'request') { return { type, name: activeRequest?.name || 'New Request', @@ -192,7 +193,7 @@ export const useInsomniaTab = ({ }; } - if (type === TabEnum.Folder) { + if (type === 'folder') { return { type, name: activeRequestGroup?.name || 'My Folder', @@ -206,7 +207,8 @@ export const useInsomniaTab = ({ }; } - if ([TabEnum.Collection, TabEnum.Document, TabEnum.Env, TabEnum.Mock, TabEnum.TEST].includes(type)) { + const collectionTabTypes: TabType[] = ['collection', 'document', 'environment', 'mockServer', 'test']; + if (collectionTabTypes.includes(type)) { return { type, name: activeWorkspace.name, @@ -220,7 +222,7 @@ export const useInsomniaTab = ({ }; } - if (type === TabEnum.Runner) { + if (type === 'runner') { return { type, name: 'Runner', @@ -234,7 +236,7 @@ export const useInsomniaTab = ({ }; } - if (type === TabEnum.MockRoute) { + if (type === 'mockRoute') { return { type, name: activeMockRoute?.name || 'Untitled mock route', @@ -250,7 +252,7 @@ export const useInsomniaTab = ({ }; } - if (type === TabEnum.TESTSUITE) { + if (type === 'testSuite') { return { type, name: unitTestSuite?.name || 'Untitled test suite', From 554dfa451975494a3318ccfbe7f6272cb53a1ef9 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Mon, 13 Jan 2025 11:11:41 +0800 Subject: [PATCH 43/47] del log --- packages/insomnia/src/ui/components/tabs/tabList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 81c796b213b..0bdd0d47365 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -188,7 +188,6 @@ 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]; From 75c74da56d7800ff2873608ec4de8137de50af4d Mon Sep 17 00:00:00 2001 From: Curry Yang <163384738+CurryYangxx@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:38:51 +0800 Subject: [PATCH 44/47] feat(tabs): keep connection for tabs-[INS-4778] (#8266) * fix: runner not update * also delete folder runner tab when delete a folder * fix: keep websocket connection * feat: keep grpc&websocket connection * unify close connection after tab closed * del repeat func * close graphql subscription * resolve conflict * feat: close connections when active environment change * add desc for hooks * close connections when organization change --- .../dropdowns/response-history-dropdown.tsx | 2 +- .../src/ui/components/environment-picker.tsx | 3 +- packages/insomnia/src/ui/eventBus.ts | 4 +- .../src/ui/hooks/use-close-connection.ts | 71 +++++++++++++++++++ packages/insomnia/src/ui/routes/debug.tsx | 12 ++-- 5 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 packages/insomnia/src/ui/hooks/use-close-connection.ts diff --git a/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx index 3a1da85f251..7cfaf37f7f2 100644 --- a/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/response-history-dropdown.tsx @@ -60,7 +60,7 @@ export const ResponseHistoryDropdown = ({ const handleDeleteResponses = useCallback(async () => { if (isWebSocketResponse(activeResponse)) { - window.main.webSocket.closeAll(); + window.main.webSocket.close({ requestId }); } fetcher.submit({}, { action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/response/delete-all`, diff --git a/packages/insomnia/src/ui/components/environment-picker.tsx b/packages/insomnia/src/ui/components/environment-picker.tsx index 222aad93d89..ad010078412 100644 --- a/packages/insomnia/src/ui/components/environment-picker.tsx +++ b/packages/insomnia/src/ui/components/environment-picker.tsx @@ -5,6 +5,7 @@ import { useFetcher, useNavigate, useParams, useRouteLoaderData } from 'react-ro import { fuzzyMatch } from '../../common/misc'; import { isRemoteProject } from '../../models/project'; +import uiEventBus from '../eventBus'; import { useOrganizationPermissions } from '../hooks/use-organization-features'; import type { WorkspaceLoaderData } from '../routes/workspace'; import { Icon } from './icon'; @@ -239,7 +240,6 @@ export const EnvironmentPicker = ({ return; } const [environmentId] = keys.values(); - setActiveEnvironmentFetcher.submit( { environmentId, @@ -249,6 +249,7 @@ export const EnvironmentPicker = ({ action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`, } ); + uiEventBus.emit('CHANGE_ACTIVE_ENV', workspaceId); }} className="p-2 select-none text-sm overflow-y-auto focus:outline-none" > diff --git a/packages/insomnia/src/ui/eventBus.ts b/packages/insomnia/src/ui/eventBus.ts index 973c6819347..deb6104346a 100644 --- a/packages/insomnia/src/ui/eventBus.ts +++ b/packages/insomnia/src/ui/eventBus.ts @@ -1,10 +1,10 @@ type EventHandler = (...args: any[]) => void; -type UIEventType = 'CLOSE_TAB'; - +type UIEventType = 'CLOSE_TAB' | 'CHANGE_ACTIVE_ENV'; class EventBus { private events: Record = { CLOSE_TAB: [], + CHANGE_ACTIVE_ENV: [], }; // Subscribe to event diff --git a/packages/insomnia/src/ui/hooks/use-close-connection.ts b/packages/insomnia/src/ui/hooks/use-close-connection.ts new file mode 100644 index 00000000000..ce08e153b41 --- /dev/null +++ b/packages/insomnia/src/ui/hooks/use-close-connection.ts @@ -0,0 +1,71 @@ +import { useCallback, useEffect } from 'react'; + +import * as models from '../../models'; +import { isGrpcRequestId } from '../../models/grpc-request'; +import { isEventStreamRequest, isGraphqlSubscriptionRequest, isRequestId } from '../../models/request'; +import { isWebSocketRequestId } from '../../models/websocket-request'; +import { useInsomniaTabContext } from '../context/app/insomnia-tab-context'; +import uiEventBus from '../eventBus'; + +// this hook is use for control when to close connections(websocket & SSE & grpc stream & graphql subscription) +export const useCloseConnection = ({ organizationId }: { organizationId: string }) => { + + const closeConnectionById = async (id: string) => { + if (isGrpcRequestId(id)) { + window.main.grpc.cancel(id); + } else if (isWebSocketRequestId(id)) { + window.main.webSocket.close({ requestId: id }); + } else if (isRequestId(id)) { + const request = await models.request.getById(id); + if (request && isEventStreamRequest(request)) { + window.main.curl.close({ requestId: id }); + } else if (request && isGraphqlSubscriptionRequest(request)) { + window.main.webSocket.close({ requestId: id }); + } + } + }; + + // close websocket&grpc&SSE connections + const handleTabClose = useCallback((_: string, ids: 'all' | string[]) => { + if (ids === 'all') { + window.main.webSocket.closeAll(); + window.main.grpc.closeAll(); + window.main.curl.closeAll(); + return; + } + + ids.forEach(async id => { + await closeConnectionById(id); + }); + }, []); + + const { currentOrgTabs } = useInsomniaTabContext(); + + const handleActiveEnvironmentChange = useCallback((workspaceId: string) => { + const { tabList } = currentOrgTabs; + const tabs = tabList.filter(tab => tab.workspaceId === workspaceId); + tabs.forEach(async tab => { + const id = tab.id; + await closeConnectionById(id); + }); + }, [currentOrgTabs]); + + useEffect(() => { + uiEventBus.on('CLOSE_TAB', handleTabClose); + uiEventBus.on('CHANGE_ACTIVE_ENV', handleActiveEnvironmentChange); + + return () => { + uiEventBus.off('CLOSE_TAB', handleTabClose); + uiEventBus.off('CHANGE_ACTIVE_ENV', handleActiveEnvironmentChange); + }; + }, [handleTabClose, handleActiveEnvironmentChange]); + + // close all connections when organizationId change + useEffect(() => { + return () => { + window.main.webSocket.closeAll(); + window.main.grpc.closeAll(); + window.main.curl.closeAll(); + }; + }, [organizationId]); +}; diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index f29d70a797c..058d74783c0 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -105,6 +105,7 @@ 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 { useCloseConnection } from '../hooks/use-close-connection'; import { useExecutionState } from '../hooks/use-execution-state'; import { useInsomniaTab } from '../hooks/use-insomnia-tab'; import { useReadyState } from '../hooks/use-ready-state'; @@ -452,13 +453,10 @@ export const Debug: FC = () => { } }, }); - // Close all websocket connections when the active environment changes - useEffect(() => { - return () => { - window.main.webSocket.closeAll(); - window.main.grpc.closeAll(); - }; - }, [activeEnvironment?._id]); + + useCloseConnection({ + organizationId, + }); const isRealtimeRequest = activeRequest && From 8e3b7a8e203fdcd6644ffb1d050fac8498e5b8ce Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Tue, 14 Jan 2025 23:22:13 +0800 Subject: [PATCH 45/47] fix: tab icon --- 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 08458d28073..bb6779e2c2e 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -38,7 +38,7 @@ const REQUEST_METHOD_STYLE_MAP: Record = { }; const WORKSPACE_TAB_UI_MAP: Partial> = { - request: { + collection: { icon: 'bars', bgColor: 'bg-[--color-surprise]', textColor: 'text-[--color-font-surprise]', From faba8f3afaa6367d7c72a20916e83a1580ccfa22 Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Wed, 22 Jan 2025 10:08:07 +0800 Subject: [PATCH 46/47] uppercase --- packages/insomnia/src/ui/components/tabs/tabList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx index 0bdd0d47365..e0d545e7843 100644 --- a/packages/insomnia/src/ui/components/tabs/tabList.tsx +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -366,11 +366,11 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = '' {currentPage === 'debug' && ( - add request to current collection + Add request to current collection )} - add request to other collection + Add request to other collection From 8870e59dac7c957fee5cd6558ee51cf7b1b11c5c Mon Sep 17 00:00:00 2001 From: Curry Yang <163384738+CurryYangxx@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:37:07 +0800 Subject: [PATCH 47/47] test: multiple tab (#8304) * test: multiple tab * fix: dashboard test --- .../smoke/dashboard-interactions.test.ts | 10 +-- .../smoke/design-document-naming.test.ts | 2 +- .../tests/smoke/insomnia-tab.test.ts | 88 +++++++++++++++++++ .../components/dropdowns/method-dropdown.tsx | 2 +- .../add-request-to-collection-modal.tsx | 4 +- .../insomnia/src/ui/components/tabs/tab.tsx | 4 +- .../src/ui/components/tabs/tabList.tsx | 2 +- 7 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 packages/insomnia-smoke-test/tests/smoke/insomnia-tab.test.ts diff --git a/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts index 0c5be48efba..88b1ef053ec 100644 --- a/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts @@ -99,14 +99,14 @@ test.describe('Dashboard', async () => { await page.getByTestId('project').click(); // Rename document - await page.getByLabel('my-spec.yaml').getByRole('button').click(); + await page.getByLabel('Files').getByLabel('my-spec.yaml').getByRole('button').click(); await page.getByRole('menuitem', { name: 'Rename' }).click(); await page.locator('text=Rename DocumentName Rename >> input[type="text"]').fill('test123'); await page.click('#root button:has-text("Rename")'); await expect(page.locator('.app')).toContainText('test123'); // Duplicate document - await page.getByLabel('test123').getByRole('button').click(); + await page.getByLabel('Files').getByLabel('test123').getByRole('button').click(); await page.getByRole('menuitem', { name: 'Duplicate' }).click(); await page.locator('input[name="name"]').fill('test123-duplicate'); await page.click('[role="dialog"] button:has-text("Duplicate")'); @@ -114,7 +114,7 @@ test.describe('Dashboard', async () => { await page.getByTestId('project').click(); // Delete document - await page.getByLabel('test123-duplicate').getByRole('button').click(); + await page.getByLabel('Files').getByLabel('test123-duplicate').getByRole('button').click(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'Delete' }).click(); // @TODO: Re-enable - Requires mocking VCS operations @@ -141,7 +141,7 @@ test.describe('Dashboard', async () => { await expect(page.locator('.app')).toContainText('test123'); // Duplicate collection - await page.getByLabel('test123').getByRole('button').click(); + await page.getByLabel('Files').getByLabel('test123').getByRole('button').click(); await page.getByRole('menuitem', { name: 'Duplicate' }).click(); await page.locator('input[name="name"]').fill('test123-duplicate'); await page.click('[role="dialog"] button:has-text("Duplicate")'); @@ -149,7 +149,7 @@ test.describe('Dashboard', async () => { await page.getByTestId('project').click(); // Delete collection - await page.getByLabel('test123-duplicate').getByRole('button').click(); + await page.getByLabel('Files').getByLabel('test123-duplicate').getByRole('button').click(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'Delete' }).click(); // @TODO: Re-enable - Requires mocking VCS operations diff --git a/packages/insomnia-smoke-test/tests/smoke/design-document-naming.test.ts b/packages/insomnia-smoke-test/tests/smoke/design-document-naming.test.ts index 4d3782b3662..4441edf1da2 100644 --- a/packages/insomnia-smoke-test/tests/smoke/design-document-naming.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/design-document-naming.test.ts @@ -6,7 +6,7 @@ test.describe('design document operations', async () => { await page.getByPlaceholder('my-spec.yaml').fill('jurassic park'); await page.getByPlaceholder('my-spec.yaml').press('Enter'); await page.getByTestId('project').click(); - await page.getByLabel('jurassic park').click(); + await page.getByLabel('Files').getByLabel('jurassic park').click(); }); test('can delete a test suite with confirmation modal', async ({ page }) => { diff --git a/packages/insomnia-smoke-test/tests/smoke/insomnia-tab.test.ts b/packages/insomnia-smoke-test/tests/smoke/insomnia-tab.test.ts new file mode 100644 index 00000000000..9e6ff690ad9 --- /dev/null +++ b/packages/insomnia-smoke-test/tests/smoke/insomnia-tab.test.ts @@ -0,0 +1,88 @@ +import { expect } from '@playwright/test'; + +import { test } from '../../playwright/test'; + +const DEFAULT_REQUEST_NAME = 'New Request'; + +test.describe('multiple-tab feature test', () => { + test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); + + test.beforeEach(async ({ page }) => { + await page.getByLabel('Create in project').click(); + await page.getByLabel('Request collection', { exact: true }).click(); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + }); + + test('add tab & close tab', async ({ page }) => { + await page.getByLabel('Create in collection').click(); + await page.getByLabel('HTTP Request').click(); + const tab = await page.getByLabel('Insomnia Tabs').getByLabel(`tab-${DEFAULT_REQUEST_NAME}`, { exact: true }); + expect(tab).toBeVisible(); + expect(await tab.getAttribute('data-selected')).toBe('true'); + await tab.getByRole('button').click(); + await expect(tab).toBeHidden(); + }); + + test('active tab sync with the sidebar active request', async ({ page }) => { + await page.getByLabel('Create in collection').click(); + await page.getByLabel('HTTP Request').click(); + await page.getByTestId('New Request').dblclick(); + await page.getByRole('textbox', { name: 'GET New Request' }).fill('New Request A'); + await page.getByLabel('Create in collection').click(); + await page.getByLabel('HTTP Request').click(); + await page.getByTestId('New Request').dblclick(); + await page.getByRole('textbox', { name: 'GET New Request' }).fill('New Request B'); + await page.getByTestId('New Request A').click(); + await page.waitForTimeout(1000); + const tabA = await page.getByLabel('Insomnia Tabs').getByLabel('tab-New Request A', { exact: true }); + expect(await tabA.getAttribute('data-selected')).toBe('true'); + await page.getByTestId('New Request B').click(); + await page.waitForTimeout(1000); + const tabB = await page.getByLabel('Insomnia Tabs').getByLabel('tab-New Request B', { exact: true }); + expect(await tabB.getAttribute('data-selected')).toBe('true'); + }); + + test('close tab after delete a request', async ({ page }) => { + await page.getByLabel('Create in collection').click(); + await page.getByLabel('HTTP Request').click(); + const tab = await page.getByLabel('Insomnia Tabs').getByLabel(`tab-${DEFAULT_REQUEST_NAME}`, { exact: true }); + expect(tab).toBeVisible(); + await page.getByTestId('Dropdown-New-Request').click(); + await page.getByLabel('Delete').click(); + await page.getByRole('button', { name: 'Delete', exact: true }).click(); + await expect(tab).toBeHidden(); + }); + + test('change icon after change request method', async ({ page }) => { + await page.getByLabel('Create in collection').click(); + await page.getByLabel('HTTP Request').click(); + await page.waitForTimeout(1000); + expect(await page.getByLabel('Insomnia Tabs').getByLabel('Tab Tag').innerText()).toEqual('GET'); + await page.getByLabel('Request Method').click(); + await page.getByRole('button', { name: 'POST' }).click(); + await page.waitForTimeout(1000); + expect(await page.getByLabel('Insomnia Tabs').getByLabel('Tab Tag').innerText()).toEqual('POST'); + }); + + test('click + button to add a new request', async ({ page }) => { + await page.getByLabel('Tab Plus').click(); + await page.getByRole('menuitem', { name: 'add request to current' }).click(); + await page.getByTestId(DEFAULT_REQUEST_NAME).click(); + await page.getByTestId(DEFAULT_REQUEST_NAME).dblclick(); + await page.getByRole('textbox', { name: 'GET New Request' }).fill('New Request A'); + await page.getByTestId('project').click(); + await page.getByLabel('Create in project').click(); + await page.getByLabel('Request collection', { exact: true }).click(); + await page.getByPlaceholder('My Collection').fill('Test add tab collection'); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + await page.waitForTimeout(1000); + await page.getByLabel('Tab Plus').click(); + await page.getByRole('menuitem', { name: 'add request to other' }).click(); + await page.getByLabel('Select Workspace').selectOption({ label: 'My Collection' }); + await page.getByRole('dialog').getByRole('button', { name: 'Add' }).click(); + await page.waitForTimeout(1000); + expect(await page.getByTestId('workspace-context-dropdown').innerText()).toEqual('My Collection'); + await page.getByTestId(DEFAULT_REQUEST_NAME).click(); + }); + +}); diff --git a/packages/insomnia/src/ui/components/dropdowns/method-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/method-dropdown.tsx index 186386e0579..7ea5be2cb57 100644 --- a/packages/insomnia/src/ui/components/dropdowns/method-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/method-dropdown.tsx @@ -69,7 +69,7 @@ export const MethodDropdown = forwardRef(({ + {method}{' '} 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 a81e01a3f7c..54704a66eaf 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 @@ -106,9 +106,9 @@ export const AddRequestToCollectionModal: FC = ({ onHide } {strings.collection.plural}: - setSelectedWorkspaceId(e.target.value)}> + setSelectedWorkspaceId(e.target.value)}> {workspaceOptions.map(workspace => ( - + {workspace.name}{workspace._id === currentWorkspaceId && ' (current)'} ))} diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx index bb6779e2c2e..199af516a68 100644 --- a/packages/insomnia/src/ui/components/tabs/tab.tsx +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -75,7 +75,7 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { if (type === 'request' || type === 'mockRoute') { return ( - {tab.tag} + {tab.tag} ); } @@ -123,7 +123,7 @@ export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { return ( - +
+ Project is required +
+ Workspace is required +
+ {requestFetcher.data.error} +
@@ -1483,7 +1483,7 @@ const ProjectRoute: FC = () => {
This is an empty Organization. To get started create your first project.
The runner failed due to an unhandled error:
- {searchParams.get('error')} -
{searchParams.get('error')}
+ {searchParams.get('error')} +
+ {error} +
{error}