diff --git a/packages/insomnia/src/basic-components/select-popover.tsx b/packages/insomnia/src/basic-components/select-popover.tsx index 64a74cdd90..2fa8ff65b3 100644 --- a/packages/insomnia/src/basic-components/select-popover.tsx +++ b/packages/insomnia/src/basic-components/select-popover.tsx @@ -109,18 +109,15 @@ export function SelectPopover({ items={[...items]} selectedKeys={selectedKey === null || selectedKey === undefined ? [] : [selectedKey]} selectionMode="single" + disallowEmptySelection onSelectionChange={keys => { if (keys === 'all' || !keys) { return; } - const [nextKey] = keys.values(); + const [key] = keys.values(); - if (nextKey === undefined) { - return; - } - - onSelectionChange(nextKey); + onSelectionChange(key); setOpen(false); }} renderEmptyState={() => (emptyState ?
{emptyState}
: null)} @@ -134,6 +131,11 @@ export function SelectPopover({ id={item.id} textValue={item.textValue ?? item.label} isDisabled={item.isDisabled} + onPress={() => { + if (String(item.id) === String(selectedKey)) { + setOpen(false); + } + }} className={twMerge( 'flex min-h-8 w-full items-center gap-2 rounded-sm px-2 text-(--color-font) transition-colors hover:bg-(--hl-sm) focus:bg-(--hl-xs) focus:outline-hidden disabled:cursor-not-allowed aria-selected:font-bold', itemClassName, diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx index 4df76aae11..a1d68ff0b8 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx @@ -34,7 +34,7 @@ import { sortMethodMap } from '~/common/sorting'; import { useRootLoaderData } from '~/root'; import { useOrganizationLoaderData } from '~/routes/organization'; import { useInsomniaSyncPullRemoteFileActionFetcher } from '~/routes/organization.$organizationId.insomnia-sync.pull-remote-file'; -import { useProjectLoaderData } from '~/routes/organization.$organizationId.project.$projectId'; +import { useProjectLoaderData, useProjectRouteContext } from '~/routes/organization.$organizationId.project.$projectId'; import { useWorkspaceNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.new'; import { useStorageRulesLoaderFetcher } from '~/routes/organization.$organizationId.storage-rules'; import { AnalyticsEvent, trackOnceDaily } from '~/ui/analytics'; @@ -79,6 +79,7 @@ export interface ProjectLoaderData { const Component = () => { const { localFiles, activeProject, activeProjectGitRepository, projects, remoteFilesPromise } = useProjectLoaderData()!; + const { activeSidebarTab } = useProjectRouteContext(); const { organizationId, projectId } = useParams() as { organizationId: string; projectId: string; @@ -336,15 +337,19 @@ const Component = () => {
- { - setNewWorkspaceModalState({ scope: 'collection', isOpen: true, redirect: false, source: 'home-page' }); - }} - /> + {activeSidebarTab === 'projects' && ( + { + setNewWorkspaceModalState({ scope: 'collection', isOpen: true, redirect: false, source: 'home-page' }); + }} + onImportFrom={() => setImportModalType('file')} + /> + )}
{activeProject ? (
diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx index 25ca878a16..fc65008fff 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx @@ -1,9 +1,9 @@ import { getLearningFeature } from 'insomnia-api'; import { models, services } from 'insomnia-data'; -import { useEffect, useRef, useState } from 'react'; +import { type Dispatch, type SetStateAction, useEffect, useRef, useState } from 'react'; import { Button, Heading } from 'react-aria-components'; import { type ImperativePanelHandle, Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; -import { href, Outlet, redirect, useParams, useRouteLoaderData } from 'react-router'; +import { href, Outlet, redirect, useOutletContext, useParams, useRouteLoaderData, useSearchParams } from 'react-router'; import * as reactUse from 'react-use'; import { logout } from '~/account/session'; @@ -18,7 +18,11 @@ import { import { useStorageRulesLoaderFetcher } from '~/routes/organization.$organizationId.storage-rules'; import { ProjectModal } from '~/ui/components/modals/project-modal'; import { ScratchPadTutorialPanel } from '~/ui/components/panes/scratchpad-tutorial-pane'; -import { ProjectNavigationSidebar } from '~/ui/components/sidebar/project-navigation-sidebar/project-navigation-sidebar'; +import { + ProjectNavigationSidebar, + type ProjectNavigationSidebarHandle, + type ProjectNavigationSidebarTabId, +} from '~/ui/components/sidebar/project-navigation-sidebar/project-navigation-sidebar'; import { SyncBar } from '~/ui/components/sidebar/sync-bar'; import { useSidebarContext } from '~/ui/context/app/insomnia-sidebar-context'; import { GitFileIssuesProvider, useProjectGitFileIssues } from '~/ui/hooks/use-git-file-issues'; @@ -131,11 +135,22 @@ export function useProjectLoaderData() { return useRouteLoaderData('routes/organization.$organizationId.project.$projectId'); } +export interface ProjectRouteContextValue { + activeSidebarTab: ProjectNavigationSidebarTabId; + setActiveSidebarTab: Dispatch>; +} + +export function useProjectRouteContext() { + return useOutletContext(); +} + const Component = ({ loaderData }: Route.ComponentProps) => { const { organizationId } = useParams() as { organizationId: string; projectId: string; }; + + const [searchParams] = useSearchParams(); const { activeProject, learningFeaturePromise } = loaderData; const storageRuleFetcher = useStorageRulesLoaderFetcher({ key: `storage-rule:${organizationId}` }); @@ -160,6 +175,11 @@ const Component = ({ loaderData }: Route.ComponentProps) => { }, [isSidebarCollapsed]); const { features } = useOrganizationPermissions(); + const [storedSidebarTab, setActiveSidebarTab] = reactUse.useLocalStorage( + `${organizationId}:sidebar-tab`, + 'projects', + ); + const activeSidebarTab = !features.konnectSync.enabled ? 'projects' : (storedSidebarTab ?? 'projects'); const isScratchPad = models.project.isScratchpadProject(activeProject); const gitRepositoryId = @@ -171,6 +191,15 @@ const Component = ({ loaderData }: Route.ComponentProps) => { gitRepositoryId, }); + const navigationSidebarRef = useRef(null); + + useEffect(() => { + const isExpanded = searchParams.get('isExpended') === 'true'; + if (navigationSidebarRef.current && isExpanded && activeProject) { + navigationSidebarRef.current.expandProject(activeProject._id); + } + }, [searchParams, activeProject]); + return ( <> { >
setIsNewProjectModalOpen(true)} + setActiveTab={setActiveSidebarTab} + ref={navigationSidebarRef} /> {isScratchPad && } {!isLearningFeatureDismissed && learningFeature?.active && ( @@ -226,7 +258,12 @@ const Component = ({ loaderData }: Route.ComponentProps) => { /> - + diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx index a225e7fa31..7e6bde1dd7 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx @@ -114,6 +114,7 @@ export async function clientAction({ params, request }: Route.ClientActionArgs) await services.request.create({ parentId: parentId || workspaceId, url: req.url, + name: req.name || 'New Request', method: req.method, headers: req.headers, body: req.body as RequestBody, diff --git a/packages/insomnia/src/routes/organization.$organizationId.project._index.tsx b/packages/insomnia/src/routes/organization.$organizationId.project._index.tsx index c77c1bbdb4..01ae4eb3cb 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project._index.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project._index.tsx @@ -83,7 +83,7 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { parentId: project._id, }); - return redirect(`/organization/${organizationId}/project/${project._id}`); + return redirect(`/organization/${organizationId}/project/${project._id}?isExpended=true`); } catch (error) { console.warn('[project] Failed to auto-create initial local project', error); } diff --git a/packages/insomnia/src/ui/components/first-request-creation.tsx b/packages/insomnia/src/ui/components/first-request-creation.tsx index 0dbe126968..f0378c8ac4 100644 --- a/packages/insomnia/src/ui/components/first-request-creation.tsx +++ b/packages/insomnia/src/ui/components/first-request-creation.tsx @@ -1,11 +1,13 @@ import type { IconProp } from '@fortawesome/fontawesome-svg-core'; import type { Request } from 'insomnia-data'; +import { constructKeyCombinationDisplay, getPlatformKeyCombinations } from 'insomnia-data/common'; import { type KeyboardEvent as ReactKeyboardEvent, useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import { Button } from '~/basic-components/button'; import { SelectPopover } from '~/basic-components/select-popover'; import { getProjectRecentRequests, type RecentProjectRequest } from '~/common/project'; +import { useRootLoaderData } from '~/root'; import { useRequestNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new'; import { useWorkspaceNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.new'; import { createKeybindingsHandler, useKeyboardShortcuts } from '~/ui/components/keydown-binder'; @@ -72,6 +74,8 @@ interface FirstRequestCreationProps { selectedCollectionId: string | null; onSelectedCollectionChange: (collectionId: string | null) => void; onCreateCollection: () => void; + onCreateDesignDocument: () => void; + onImportFrom: () => void; } export const FirstRequestCreation = ({ @@ -80,6 +84,8 @@ export const FirstRequestCreation = ({ selectedCollectionId, onSelectedCollectionChange, onCreateCollection, + onCreateDesignDocument, + onImportFrom, }: FirstRequestCreationProps) => { const navigate = useNavigate(); const { organizationId, projectId } = useParams() as { @@ -94,6 +100,7 @@ export const FirstRequestCreation = ({ const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [requestInput, setRequestInput] = useState(''); const [recentRequests, setRecentRequests] = useState([]); + const [isRequestInputFocused, setIsRequestInputFocused] = useState(false); const [curlParseError, setCurlParseError] = useState(false); const [selectOpen, setSelectOpen] = useState(false); const trimmedInput = requestInput.trim(); @@ -324,8 +331,23 @@ export const FirstRequestCreation = ({ icon: , onClick: handleCreateGithubLookupRequest, }, + { + id: 'create-openapi-spec', + label: 'Create OpenAPI spec', + icon: , + onClick: onCreateDesignDocument, + }, + { + id: 'import-files', + label: 'Import files', + icon: , + onClick: onImportFrom, + }, ]; + const { settings } = useRootLoaderData()!; + const keyComb = getPlatformKeyCombinations(settings.hotKeyRegistry.request_createHTTP)[0]; + const shortcutDisplay = constructKeyCombinationDisplay(keyComb, false); return ( <>
@@ -339,15 +361,19 @@ export const FirstRequestCreation = ({ : `We have a sneaking suspicion that you came here to send a request, so let’s get started!`}

-
+