mirror of
https://github.com/Kong/insomnia.git
synced 2026-06-02 21:26:40 -04:00
fix: first request feedback
This commit is contained in:
@@ -109,18 +109,15 @@ export function SelectPopover<T extends SelectPopoverItem>({
|
||||
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 ? <div className="p-3 text-sm text-(--hl)">{emptyState}</div> : null)}
|
||||
@@ -134,6 +131,11 @@ export function SelectPopover<T extends SelectPopoverItem>({
|
||||
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,
|
||||
|
||||
@@ -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 = () => {
|
||||
<Fragment>
|
||||
<OrganizationTabList showActiveStatus={false} />
|
||||
<div className="px-4 pt-4">
|
||||
<FirstRequestCreation
|
||||
greetingName={greetingName}
|
||||
collectionItems={collectionItems}
|
||||
selectedCollectionId={selectedCollectionId}
|
||||
onSelectedCollectionChange={setSelectedCollectionId}
|
||||
onCreateCollection={() => {
|
||||
setNewWorkspaceModalState({ scope: 'collection', isOpen: true, redirect: false, source: 'home-page' });
|
||||
}}
|
||||
/>
|
||||
{activeSidebarTab === 'projects' && (
|
||||
<FirstRequestCreation
|
||||
greetingName={greetingName}
|
||||
collectionItems={collectionItems}
|
||||
selectedCollectionId={selectedCollectionId}
|
||||
onSelectedCollectionChange={setSelectedCollectionId}
|
||||
onCreateDesignDocument={createNewDocument}
|
||||
onCreateCollection={() => {
|
||||
setNewWorkspaceModalState({ scope: 'collection', isOpen: true, redirect: false, source: 'home-page' });
|
||||
}}
|
||||
onImportFrom={() => setImportModalType('file')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{activeProject ? (
|
||||
<div className="flex w-full flex-col overflow-hidden">
|
||||
|
||||
@@ -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<typeof clientLoader>('routes/organization.$organizationId.project.$projectId');
|
||||
}
|
||||
|
||||
export interface ProjectRouteContextValue {
|
||||
activeSidebarTab: ProjectNavigationSidebarTabId;
|
||||
setActiveSidebarTab: Dispatch<SetStateAction<ProjectNavigationSidebarTabId | undefined>>;
|
||||
}
|
||||
|
||||
export function useProjectRouteContext() {
|
||||
return useOutletContext<ProjectRouteContextValue>();
|
||||
}
|
||||
|
||||
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<ProjectNavigationSidebarTabId>(
|
||||
`${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<ProjectNavigationSidebarHandle>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const isExpanded = searchParams.get('isExpended') === 'true';
|
||||
if (navigationSidebarRef.current && isExpanded && activeProject) {
|
||||
navigationSidebarRef.current.expandProject(activeProject._id);
|
||||
}
|
||||
}, [searchParams, activeProject]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelGroup
|
||||
@@ -190,9 +219,12 @@ const Component = ({ loaderData }: Route.ComponentProps) => {
|
||||
>
|
||||
<div className="flex flex-1 flex-col divide-y divide-solid divide-(--hl-md) overflow-hidden">
|
||||
<ProjectNavigationSidebar
|
||||
activeTab={activeSidebarTab}
|
||||
storageRules={storageRules}
|
||||
konnectSyncEnabled={features.konnectSync.enabled}
|
||||
onCreateProject={() => setIsNewProjectModalOpen(true)}
|
||||
setActiveTab={setActiveSidebarTab}
|
||||
ref={navigationSidebarRef}
|
||||
/>
|
||||
{isScratchPad && <ScratchPadTutorialPanel />}
|
||||
{!isLearningFeatureDismissed && learningFeature?.active && (
|
||||
@@ -226,7 +258,12 @@ const Component = ({ loaderData }: Route.ComponentProps) => {
|
||||
/>
|
||||
<Panel id="pane-one" className="pane-one theme--pane flex flex-col">
|
||||
<GitFileIssuesProvider value={gitFileIssues}>
|
||||
<Outlet />
|
||||
<Outlet
|
||||
context={{
|
||||
activeSidebarTab,
|
||||
setActiveSidebarTab,
|
||||
}}
|
||||
/>
|
||||
</GitFileIssuesProvider>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<RecentProjectRequest[]>([]);
|
||||
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: <SvgIcon icon="graphql" />,
|
||||
onClick: handleCreateGithubLookupRequest,
|
||||
},
|
||||
{
|
||||
id: 'create-openapi-spec',
|
||||
label: 'Create OpenAPI spec',
|
||||
icon: <Icon icon="file" className="text-(--font-size-xl)" />,
|
||||
onClick: onCreateDesignDocument,
|
||||
},
|
||||
{
|
||||
id: 'import-files',
|
||||
label: 'Import files',
|
||||
icon: <Icon icon="file-import" className="text-(--font-size-xl)" />,
|
||||
onClick: onImportFrom,
|
||||
},
|
||||
];
|
||||
|
||||
const { settings } = useRootLoaderData()!;
|
||||
const keyComb = getPlatformKeyCombinations(settings.hotKeyRegistry.request_createHTTP)[0];
|
||||
const shortcutDisplay = constructKeyCombinationDisplay(keyComb, false);
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-sm bg-[radial-gradient(95.72%_95.72%_at_-0.32%_2.6%,var(--hl-md)_0%,var(--hl-xs)_100%),radial-gradient(100%_100.41%_at_100%_99.92%,var(--hl-md)_0%,var(--hl-xs)_100%)] p-px">
|
||||
@@ -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!`}
|
||||
</p>
|
||||
<div className="mt-8 w-[50%] min-w-100">
|
||||
<div className="flex aspect-540/127 flex-col overflow-hidden rounded-lg border border-[#3F3F46] bg-(--color-bg) shadow-[0_0_0_4px_#0044F433]">
|
||||
<div
|
||||
className={`flex aspect-540/127 flex-col overflow-hidden rounded-lg border border-(--hl-md) bg-(--color-bg) ${isRequestInputFocused ? 'shadow-[0_0_0_4px_#0044F433]' : ''}`}
|
||||
>
|
||||
<div className="flex-1 px-4 pt-3 pb-2">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
aria-label="Request endpoint or cURL input"
|
||||
className="text-md h-full w-full flex-1 resize-none font-mono"
|
||||
placeholder="Enter an endpoint URL or paste cURL, or ⌘N for a new blank request"
|
||||
placeholder={`Enter an endpoint URL or paste cURL, or ${shortcutDisplay} for a new blank request`}
|
||||
value={requestInput}
|
||||
onFocus={() => setIsRequestInputFocused(true)}
|
||||
onBlur={() => setIsRequestInputFocused(false)}
|
||||
onChange={event => {
|
||||
setCurlParseError(false);
|
||||
setRequestInput(event.target.value);
|
||||
@@ -383,7 +409,7 @@ export const FirstRequestCreation = ({
|
||||
New Collection
|
||||
</Button>
|
||||
}
|
||||
triggerClassName="h-8 rounded-md px-3 text-sm"
|
||||
triggerClassName="h-8 rounded-md px-3 text-sm data-[focus-visible=true]:!ring-0"
|
||||
popoverClassName="w-[240px]"
|
||||
dialogClassName="w-[240px]"
|
||||
renderTrigger={selectedItem => (
|
||||
|
||||
@@ -2,7 +2,18 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import type { StorageRules } from 'insomnia-api';
|
||||
import type { RequestGroup, Workspace } from 'insomnia-data';
|
||||
import { models, services } from 'insomnia-data';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
type Dispatch,
|
||||
type ForwardedRef,
|
||||
forwardRef,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Button, GridList, GridListItem, Input, SearchField, Tab, TabList, Tabs } from 'react-aria-components';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router';
|
||||
import * as reactUse from 'react-use';
|
||||
@@ -50,10 +61,18 @@ import { WorkspaceNode } from './workspace-node';
|
||||
interface ProjectNavigationSidebarProps {
|
||||
storageRules: StorageRules;
|
||||
activeNodeId?: string;
|
||||
activeTab: ProjectNavigationSidebarTabId;
|
||||
konnectSyncEnabled: boolean;
|
||||
onCreateProject: () => void;
|
||||
setActiveTab: Dispatch<SetStateAction<ProjectNavigationSidebarTabId | undefined>>;
|
||||
}
|
||||
|
||||
export interface ProjectNavigationSidebarHandle {
|
||||
expandProject: (projectId: string) => void;
|
||||
}
|
||||
|
||||
export type ProjectNavigationSidebarTabId = 'projects' | 'konnect';
|
||||
|
||||
const SidebarSearchField = ({
|
||||
value,
|
||||
isDisabled,
|
||||
@@ -136,11 +155,10 @@ const NewProjectButton = ({ onPress, isDisabled }: { onPress: () => void; isDisa
|
||||
</BasicButton>
|
||||
);
|
||||
|
||||
export const ProjectNavigationSidebar = ({
|
||||
storageRules,
|
||||
konnectSyncEnabled,
|
||||
onCreateProject,
|
||||
}: ProjectNavigationSidebarProps) => {
|
||||
const ProjectNavigationSidebarInner = (
|
||||
{ storageRules, konnectSyncEnabled, onCreateProject, activeTab, setActiveTab }: ProjectNavigationSidebarProps,
|
||||
ref: ForwardedRef<ProjectNavigationSidebarHandle>,
|
||||
) => {
|
||||
const navigate = useNavigate();
|
||||
const { organizationId, projectId: activeProjectId } = useParams() as {
|
||||
organizationId: string;
|
||||
@@ -177,15 +195,10 @@ export const ProjectNavigationSidebar = ({
|
||||
`${organizationId}:project-navigation-konnect-filter`,
|
||||
'',
|
||||
);
|
||||
const [storedTab, setActiveTab] = reactUse.useLocalStorage<'projects' | 'konnect'>(
|
||||
`${organizationId}:sidebar-tab`,
|
||||
'projects',
|
||||
);
|
||||
const [expandedProjectAndWorkspaceIds, setExpandedProjectAndWorkspaceIds] = reactUse.useLocalStorage<string[]>(
|
||||
`${organizationId}:nav-expanded-projects-and-workspaces`,
|
||||
[],
|
||||
);
|
||||
const activeTab = !konnectSyncEnabled ? 'projects' : (storedTab ?? 'projects');
|
||||
const isProjectTabActive = activeTab === 'projects';
|
||||
const { syncing, progress, startSync, cancelSync } = useKonnectSync();
|
||||
|
||||
@@ -723,6 +736,16 @@ export const ProjectNavigationSidebar = ({
|
||||
[expandedProjectAndWorkspaceIds, projectNavigationSidebarFilter, setExpandedProjectAndWorkspaceIds],
|
||||
);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
expandProject(projectId: string) {
|
||||
expandProjectOrWorkspaces([projectId]);
|
||||
},
|
||||
}),
|
||||
[expandProjectOrWorkspaces],
|
||||
);
|
||||
|
||||
const toggleRequestGroups = useCallback(
|
||||
async (requestGroupIds: string[], workspace: Workspace, collapsed?: boolean) => {
|
||||
if (requestGroupIds.length === 0) {
|
||||
@@ -884,7 +907,7 @@ export const ProjectNavigationSidebar = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden" data-testid="global-navigation-sidebar">
|
||||
<Tabs selectedKey={activeTab} onSelectionChange={key => setActiveTab(key as 'projects' | 'konnect')}>
|
||||
<Tabs selectedKey={activeTab} onSelectionChange={key => setActiveTab(key as ProjectNavigationSidebarTabId)}>
|
||||
<SideBarTabList
|
||||
konnectSyncEnabled={konnectSyncEnabled}
|
||||
isScratchPad={isScratchPad}
|
||||
@@ -1194,6 +1217,10 @@ export const ProjectNavigationSidebar = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const ProjectNavigationSidebar = forwardRef<ProjectNavigationSidebarHandle, ProjectNavigationSidebarProps>(
|
||||
ProjectNavigationSidebarInner,
|
||||
);
|
||||
|
||||
export const EmptyProjectNavigationSidebar = ({ onCreateProject }: { onCreateProject: () => void }) => {
|
||||
const { organizationId } = useParams() as { organizationId: string };
|
||||
const isScratchPad = models.organization.isScratchpadOrganizationId(organizationId);
|
||||
|
||||
Reference in New Issue
Block a user