mirror of
https://github.com/Kong/insomnia.git
synced 2026-08-04 03:42:20 -04:00
add package
remove workspaceCount from rootLoaderData move unrelevant loaders out add query context adapt tanstack query fix som issues add new ways to load data initial changes clean code
This commit is contained in:
27
package-lock.json
generated
27
package-lock.json
generated
@@ -9723,6 +9723,32 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.101.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz",
|
||||
"integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.101.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz",
|
||||
"integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.101.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
||||
@@ -29096,6 +29122,7 @@
|
||||
"@stoplight/spectral-ruleset-bundler": "1.7.0",
|
||||
"@stoplight/spectral-rulesets": "^1.22.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-query": "^5.101.0",
|
||||
"@tanstack/react-virtual": "3.13.12",
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
"acorn": "^8.16.0",
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"@stoplight/spectral-rulesets": "^1.22.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-virtual": "3.13.12",
|
||||
"@tanstack/react-query": "^5.101.0",
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
"acorn": "^8.16.0",
|
||||
"acorn-walk": "^8.3.5",
|
||||
|
||||
@@ -203,7 +203,6 @@ export const ErrorBoundary: FC<Route.ErrorBoundaryProps> = ({ error }) => {
|
||||
|
||||
export interface RootLoaderData {
|
||||
settings: Settings;
|
||||
workspaceCount: number;
|
||||
userSession: UserSession;
|
||||
}
|
||||
|
||||
@@ -213,13 +212,11 @@ export const useRootLoaderData = () => {
|
||||
|
||||
export async function clientLoader(_args: Route.ClientLoaderArgs) {
|
||||
const settings = await services.settings.get();
|
||||
const workspaceCount = await services.workspace.count();
|
||||
const userSession = await services.userSession.get();
|
||||
const cloudCredentials = await services.cloudCredential.all();
|
||||
|
||||
return {
|
||||
settings,
|
||||
workspaceCount,
|
||||
userSession,
|
||||
cloudCredentials,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IconName, IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import type { WorkspaceScope } from 'insomnia-data';
|
||||
import { models } from 'insomnia-data';
|
||||
import type { WorkspaceMeta, WorkspaceScope } from 'insomnia-data';
|
||||
import { database, models, services } from 'insomnia-data';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from '~/common/constants';
|
||||
import { scopeToBgColorMap, scopeToIconMap, scopeToTextColorMap } from '~/common/get-workspace-label';
|
||||
import { fuzzyMatchAll } from '~/common/misc';
|
||||
import { getAllLocalFiles, type InsomniaFile } from '~/common/project';
|
||||
import { type InsomniaFile } from '~/common/project';
|
||||
import { sortMethodMap } from '~/common/sorting';
|
||||
import { invariant } from '~/common/utils/invariant';
|
||||
import { useRootLoaderData } from '~/root';
|
||||
@@ -54,6 +54,8 @@ import { OrganizationTabList } from '~/ui/components/tabs/tab-list';
|
||||
import { TimeFromNow } from '~/ui/components/time-from-now';
|
||||
import { showResourceNotFoundToast } from '~/ui/components/toast-notification';
|
||||
import { useInsomniaEventStreamContext } from '~/ui/context/app/insomnia-event-stream-context';
|
||||
import { useProjects } from '~/ui/hooks/data/projects';
|
||||
import { useLocalFiles } from '~/ui/hooks/data/workspaces';
|
||||
import { useGitFileIssues } from '~/ui/hooks/use-git-file-issues';
|
||||
import { useTabNavigate } from '~/ui/hooks/use-insomnia-tab';
|
||||
import { useLoaderDeferData } from '~/ui/hooks/use-loader-defer-data';
|
||||
@@ -69,23 +71,29 @@ export async function clientLoader({ params }: Route.ClientLoaderArgs) {
|
||||
invariant(projectId, 'Project ID is required');
|
||||
invariant(organizationId, 'Organization ID is required');
|
||||
|
||||
const projectWorkspaces = await services.workspace.findByParentId(projectId);
|
||||
const workspaceMetas = await database.find<WorkspaceMeta>(models.workspaceMeta.type, {
|
||||
parentId: { $in: projectWorkspaces.map(workspace => workspace._id) },
|
||||
});
|
||||
const remoteFilesPromise = getAllRemoteFiles({ projectId, organizationId });
|
||||
const localFiles = await getAllLocalFiles({ projectId });
|
||||
|
||||
return {
|
||||
localFiles,
|
||||
remoteFilesPromise,
|
||||
workspaceMetas,
|
||||
};
|
||||
}
|
||||
|
||||
const Component = ({ loaderData }: Route.ComponentProps) => {
|
||||
const { localFiles, remoteFilesPromise } = loaderData;
|
||||
const { activeProject, activeProjectGitRepository, projects } = useProjectLoaderData()!;
|
||||
const { remoteFilesPromise } = loaderData;
|
||||
const { activeProject, activeProjectGitRepository } = useProjectLoaderData()!;
|
||||
const { activeSidebarTab } = useProjectRouteContext();
|
||||
const { organizationId, projectId } = useParams() as {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
};
|
||||
// Read the project list from the shared query cache (same source the sidebar uses)
|
||||
const { data: projects = [] } = useProjects(organizationId);
|
||||
const localFiles = useLocalFiles(projectId, loaderData.workspaceMetas);
|
||||
const [remoteFiles] = useLoaderDeferData<InsomniaFile[]>(remoteFilesPromise, projectId);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -20,7 +20,9 @@ import {
|
||||
type ProjectNavigationSidebarTabId,
|
||||
} from '~/ui/components/sidebar/project-navigation-sidebar/project-navigation-sidebar';
|
||||
import { SyncBar } from '~/ui/components/sidebar/sync-bar';
|
||||
import { dbQueryClient } from '~/ui/context/app/insomnia-query-client';
|
||||
import { useSidebarContext } from '~/ui/context/app/insomnia-sidebar-context';
|
||||
import { projectKeys } from '~/ui/hooks/data/projects';
|
||||
import { GitFileIssuesProvider, useProjectGitFileIssues } from '~/ui/hooks/use-git-file-issues';
|
||||
import { useLoaderDeferData } from '~/ui/hooks/use-loader-defer-data';
|
||||
import { useOrganizationPermissions } from '~/ui/hooks/use-organization-features';
|
||||
@@ -121,6 +123,8 @@ export async function clientLoader({ params }: Route.ClientLoaderArgs) {
|
||||
const organizationProjects = await getProjectsWithGitRepositories({ organizationId });
|
||||
|
||||
const projects = models.project.sortProjects(organizationProjects);
|
||||
// Seed the shared query cache so consumers (sidebar, project index page) read warm data on first render instead of refetching.
|
||||
dbQueryClient.setQueryData(projectKeys.details(organizationId), projects);
|
||||
|
||||
const learningFeaturePromise = getInsomniaLearningFeature(fallbackLearningFeature);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { SettingsModal, showSettingsModal } from '~/ui/components/modals/setting
|
||||
import { PresentUsers } from '~/ui/components/present-users';
|
||||
import { OrganizationSelect } from '~/ui/components/project/organization-select';
|
||||
import { InsomniaEventStreamProvider } from '~/ui/context/app/insomnia-event-stream-context';
|
||||
import { InsomniaTanstackQueryClientContext } from '~/ui/context/app/insomnia-query-client';
|
||||
import { SidebarContext } from '~/ui/context/app/insomnia-sidebar-context';
|
||||
import { InsomniaTabProvider } from '~/ui/context/app/insomnia-tab-context';
|
||||
import { RunnerProvider } from '~/ui/context/app/runner-context';
|
||||
@@ -275,162 +276,167 @@ const Component = ({ loaderData }: Route.ComponentProps) => {
|
||||
<InsomniaEventStreamProvider>
|
||||
<InsomniaTabProvider>
|
||||
<SidebarContext.Provider value={{ isSidebarCollapsed: isSidebarCollapsed ?? false, toggleSidebar }}>
|
||||
<div className="h-full w-full">
|
||||
<div
|
||||
className={`grid-template-app-layout relative grid h-full w-full divide-x divide-solid divide-(--hl-md) bg-(--color-bg)`}
|
||||
>
|
||||
<header className="grid grid-cols-3 items-center border-b border-solid border-(--hl-md) [grid-area:Header]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex w-12.5 shrink-0 justify-center py-2">
|
||||
<InsomniaLogo />
|
||||
</div>
|
||||
{!isScratchPad && (
|
||||
<OrganizationSelect
|
||||
organizationId={organizationId}
|
||||
organizations={organizations || []}
|
||||
onSelect={id => {
|
||||
window.main.trackAnalyticsEvent({ event: AnalyticsEvent.organizationSwitched });
|
||||
navigate(`/organization/${id}`);
|
||||
}}
|
||||
currentPlan={currentPlan}
|
||||
isScratchpadWorkspace={!!isScratchpadWorkspace}
|
||||
/>
|
||||
)}
|
||||
<InsomniaTanstackQueryClientContext>
|
||||
<div className="h-full w-full">
|
||||
<div
|
||||
className={`grid-template-app-layout relative grid h-full w-full divide-x divide-solid divide-(--hl-md) bg-(--color-bg)`}
|
||||
>
|
||||
<header className="grid grid-cols-3 items-center border-b border-solid border-(--hl-md) [grid-area:Header]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex w-12.5 shrink-0 justify-center py-2">
|
||||
<InsomniaLogo />
|
||||
</div>
|
||||
{!isScratchPad && (
|
||||
<OrganizationSelect
|
||||
organizationId={organizationId}
|
||||
organizations={organizations || []}
|
||||
onSelect={id => {
|
||||
window.main.trackAnalyticsEvent({ event: AnalyticsEvent.organizationSwitched });
|
||||
navigate(`/organization/${id}`);
|
||||
}}
|
||||
currentPlan={currentPlan}
|
||||
isScratchpadWorkspace={!!isScratchpadWorkspace}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!user ? <GitHubStarsButton /> : null}
|
||||
{!user ? <GitHubStarsButton /> : null}
|
||||
</div>
|
||||
<CommandPalette />
|
||||
<div className="flex min-w-min items-center justify-end gap-(--padding-sm) space-x-3 p-2">
|
||||
{user ? (
|
||||
<LoginUserActions
|
||||
organizationId={organizationId}
|
||||
isMinimal={false}
|
||||
user={user}
|
||||
currentPlan={currentPlan}
|
||||
/>
|
||||
) : (
|
||||
<ScratchPadAuthActions />
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div className="overflow-hidden border-b border-(--hl-md) [grid-area:Content]">
|
||||
<RunnerProvider>
|
||||
<Outlet />
|
||||
</RunnerProvider>
|
||||
</div>
|
||||
<CommandPalette />
|
||||
<div className="flex min-w-min items-center justify-end gap-(--padding-sm) space-x-3 p-2">
|
||||
{user ? (
|
||||
<LoginUserActions
|
||||
organizationId={organizationId}
|
||||
isMinimal={false}
|
||||
user={user}
|
||||
currentPlan={currentPlan}
|
||||
/>
|
||||
) : (
|
||||
<ScratchPadAuthActions />
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div className="overflow-hidden border-b border-(--hl-md) [grid-area:Content]">
|
||||
<RunnerProvider>
|
||||
<Outlet />
|
||||
</RunnerProvider>
|
||||
</div>
|
||||
<div className="relative flex items-center overflow-hidden [grid-area:Statusbar]" data-testid="statusbar">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="flex h-full shrink grow basis-1/3 items-center">
|
||||
<TooltipTrigger>
|
||||
<ToggleButton
|
||||
className="ml-3 grow-0 gap-2 px-2 py-1 text-xs text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-inset"
|
||||
onChange={value => {
|
||||
setIsSidebarCollapsed(!value);
|
||||
}}
|
||||
isSelected={!isSidebarCollapsed}
|
||||
>
|
||||
{({ isSelected }) => {
|
||||
return (
|
||||
<svg
|
||||
width={10}
|
||||
height={10}
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
{isSelected ? (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 1L1 2v12l1 1h12l1-1V2l-1-1H2zm12 13H7V2h7v12z"
|
||||
/>
|
||||
) : (
|
||||
<path d="M2 1L1 2v12l1 1h12l1-1V2l-1-1H2zm0 13V2h4v12H2zm5 0V2h7v12H7z" />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}}
|
||||
</ToggleButton>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
offset={8}
|
||||
className="flex max-h-[85vh] min-w-max items-center gap-2 overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-4 py-2 text-sm text-(--color-font) shadow-lg select-none focus:outline-hidden"
|
||||
>
|
||||
Toggle sidebar
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
offset={8}
|
||||
className="flex max-h-[85vh] min-w-max items-center gap-2 overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-4 py-2 text-sm text-(--color-font) shadow-lg select-none focus:outline-hidden"
|
||||
>
|
||||
Toggle header
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
data-testid="settings-button"
|
||||
className="flex h-full items-center justify-center gap-2 px-4 py-1 text-xs text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
|
||||
onPress={() => showSettingsModal()}
|
||||
>
|
||||
<Icon icon="gear" /> Preferences
|
||||
</Button>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
offset={8}
|
||||
className="flex max-h-[85vh] min-w-max items-center gap-2 overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-4 py-2 text-sm text-(--color-font) shadow-lg select-none focus:outline-hidden"
|
||||
>
|
||||
Preferences
|
||||
<Hotkey keyBindings={settings.hotKeyRegistry.preferences_showGeneral} />
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
{!isScratchpadWorkspace && hasUntrackedData && (
|
||||
<TooltipTrigger delay={500}>
|
||||
<Button
|
||||
className="flex h-full items-center justify-center gap-2 px-4 py-1 text-xs text-(--color-warning) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
|
||||
onPress={() => {
|
||||
window.main.trackAnalyticsEvent({
|
||||
event: AnalyticsEvent.statusbarOrphanedProjectsClicked,
|
||||
});
|
||||
showModal(SettingsModal, { tab: 'data' });
|
||||
<div
|
||||
className="relative flex items-center overflow-hidden [grid-area:Statusbar]"
|
||||
data-testid="statusbar"
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="flex h-full shrink grow basis-1/3 items-center">
|
||||
<TooltipTrigger>
|
||||
<ToggleButton
|
||||
className="ml-3 grow-0 gap-2 px-2 py-1 text-xs text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-inset"
|
||||
onChange={value => {
|
||||
setIsSidebarCollapsed(!value);
|
||||
}}
|
||||
isSelected={!isSidebarCollapsed}
|
||||
>
|
||||
<Icon icon="exclamation-circle" />
|
||||
{({ isSelected }) => {
|
||||
return (
|
||||
<svg
|
||||
width={10}
|
||||
height={10}
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
{isSelected ? (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 1L1 2v12l1 1h12l1-1V2l-1-1H2zm12 13H7V2h7v12z"
|
||||
/>
|
||||
) : (
|
||||
<path d="M2 1L1 2v12l1 1h12l1-1V2l-1-1H2zm0 13V2h4v12H2zm5 0V2h7v12H7z" />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}}
|
||||
</ToggleButton>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
offset={8}
|
||||
className="flex max-h-[85vh] min-w-max items-center gap-2 overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-4 py-2 text-sm text-(--color-font) shadow-lg select-none focus:outline-hidden"
|
||||
>
|
||||
Toggle sidebar
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
offset={8}
|
||||
className="flex max-h-[85vh] min-w-max items-center gap-2 overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-4 py-2 text-sm text-(--color-font) shadow-lg select-none focus:outline-hidden"
|
||||
>
|
||||
Toggle header
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
data-testid="settings-button"
|
||||
className="flex h-full items-center justify-center gap-2 px-4 py-1 text-xs text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
|
||||
onPress={() => showSettingsModal()}
|
||||
>
|
||||
<Icon icon="gear" /> Preferences
|
||||
</Button>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
offset={8}
|
||||
className="flex max-h-[85vh] min-w-max items-center gap-2 overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-4 py-2 text-sm text-(--color-font) shadow-lg select-none focus:outline-hidden"
|
||||
>
|
||||
We have detected orphaned projects on your computer, click here to view them.
|
||||
Preferences
|
||||
<Hotkey keyBindings={settings.hotKeyRegistry.preferences_showGeneral} />
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink grow basis-1/3 justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<NetworkAndSyncIndicator
|
||||
asyncTaskStatus={asyncTaskStatus}
|
||||
settings={settings}
|
||||
sync={syncOrgsAndProjects}
|
||||
/>
|
||||
{!isScratchpadWorkspace && hasUntrackedData && (
|
||||
<TooltipTrigger delay={500}>
|
||||
<Button
|
||||
className="flex h-full items-center justify-center gap-2 px-4 py-1 text-xs text-(--color-warning) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
|
||||
onPress={() => {
|
||||
window.main.trackAnalyticsEvent({
|
||||
event: AnalyticsEvent.statusbarOrphanedProjectsClicked,
|
||||
});
|
||||
showModal(SettingsModal, { tab: 'data' });
|
||||
}}
|
||||
>
|
||||
<Icon icon="exclamation-circle" />
|
||||
</Button>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
offset={8}
|
||||
className="flex max-h-[85vh] min-w-max items-center gap-2 overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-4 py-2 text-sm text-(--color-font) shadow-lg select-none focus:outline-hidden"
|
||||
>
|
||||
We have detected orphaned projects on your computer, click here to view them.
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink grow basis-1/3 justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<NetworkAndSyncIndicator
|
||||
asyncTaskStatus={asyncTaskStatus}
|
||||
settings={settings}
|
||||
sync={syncOrgsAndProjects}
|
||||
/>
|
||||
|
||||
<Link>
|
||||
<a
|
||||
className="flex items-center gap-1 px-(--padding-md) text-xs text-(--color-font) focus:underline focus:outline-hidden"
|
||||
href="https://konghq.com/"
|
||||
>
|
||||
Made with
|
||||
<Icon className="text-(--color-surprise-font)" icon="heart" /> by Kong
|
||||
</a>
|
||||
</Link>
|
||||
<Link>
|
||||
<a
|
||||
className="flex items-center gap-1 px-(--padding-md) text-xs text-(--color-font) focus:underline focus:outline-hidden"
|
||||
href="https://konghq.com/"
|
||||
>
|
||||
Made with
|
||||
<Icon className="text-(--color-surprise-font)" icon="heart" /> by Kong
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</InsomniaTanstackQueryClientContext>
|
||||
</SidebarContext.Provider>
|
||||
</InsomniaTabProvider>
|
||||
</InsomniaEventStreamProvider>
|
||||
|
||||
@@ -659,8 +659,14 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal, onModalChange }) =>
|
||||
|
||||
const workspaceData = useWorkspaceLoaderData();
|
||||
const activeWorkspaceName = workspaceData?.activeWorkspace.name;
|
||||
const { workspaceCount, userSession } = useRootLoaderData()!;
|
||||
const { userSession } = useRootLoaderData()!;
|
||||
const workspacesFetcher = useProjectListWorkspacesLoaderFetcher();
|
||||
const [workspaceCount, setWorkspaceCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
services.workspace.count().then(setWorkspaceCount);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isIdleAndUninitialized = workspacesFetcher.state === 'idle' && !workspacesFetcher.data;
|
||||
if (
|
||||
|
||||
@@ -2,12 +2,14 @@ import { services } from 'insomnia-data';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { database as db } from '~/common/database';
|
||||
|
||||
import {
|
||||
type AllRequestsAndMetaInWorkspace,
|
||||
getAllRequestsAndMetaByWorkspace,
|
||||
} from '~/ui/hooks/data/requests-and-metas';
|
||||
|
||||
import {
|
||||
filterCollection,
|
||||
flattenCollectionChildren,
|
||||
getAllRequestsAndMetaByWorkspace,
|
||||
getWorkspacesByProjectIds,
|
||||
} from './project-navigation-sidebar-utils';
|
||||
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
import type {
|
||||
GrpcRequest,
|
||||
GrpcRequestMeta,
|
||||
Request,
|
||||
RequestGroup,
|
||||
RequestGroupMeta,
|
||||
RequestMeta,
|
||||
SocketIORequest,
|
||||
SocketIORequestMeta,
|
||||
WebSocketRequest,
|
||||
WebSocketRequestMeta,
|
||||
Workspace,
|
||||
} from 'insomnia-data';
|
||||
import type { BaseModel } from 'insomnia-data';
|
||||
import { models } from 'insomnia-data';
|
||||
|
||||
import { database } from '~/common/database';
|
||||
import { fuzzyMatchAll } from '~/common/misc';
|
||||
import { sortMethodMap } from '~/common/sorting';
|
||||
import type { Child } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
|
||||
import type { CollectionChildDoc, CollectionWorkspaceChildren } from '~/ui/hooks/data/workspace-children';
|
||||
|
||||
export interface SlimRequestDoc extends BaseModel {
|
||||
type: 'Request' | 'GrpcRequest' | 'WebSocketRequest' | 'SocketIORequest' | 'RequestGroup';
|
||||
@@ -27,14 +14,6 @@ export interface SlimRequestDoc extends BaseModel {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
type AllRequestDoc = Request | GrpcRequest | WebSocketRequest | SocketIORequest | RequestGroup;
|
||||
|
||||
export interface AllRequestsAndMetaInWorkspace {
|
||||
allRequests: AllRequestDoc[];
|
||||
allRequestMetas: (RequestMeta | GrpcRequestMeta | WebSocketRequestMeta | SocketIORequestMeta)[];
|
||||
requestGroupMetas: RequestGroupMeta[];
|
||||
}
|
||||
|
||||
// TODO SLIM THE REQUEST DOCS TO ONLY WHAT WE NEED FOR THE SIDEBAR TO IMPROVE PERFORMANCE
|
||||
// const toSlimDoc = (r: AllRequestDoc): SlimRequestDoc => ({
|
||||
// _id: r._id,
|
||||
@@ -50,150 +29,21 @@ export interface AllRequestsAndMetaInWorkspace {
|
||||
// created: r.created,
|
||||
// });
|
||||
|
||||
export async function getWorkspacesByProjectIds(projectIds: string[]) {
|
||||
const workspaces = await database.find<Workspace>(models.workspace.type, {
|
||||
parentId: { $in: projectIds },
|
||||
});
|
||||
const workspacesByProjectId = new Map<string, Workspace[]>();
|
||||
projectIds.forEach(projectId => {
|
||||
workspacesByProjectId.set(projectId, workspaces.filter(w => w.parentId === projectId) || []);
|
||||
});
|
||||
return workspacesByProjectId;
|
||||
}
|
||||
|
||||
export async function getAllRequestsAndMetaByWorkspace(workspaceIds: string[]) {
|
||||
const allRequestsAndMetaByWorkspaceId = new Map<string, AllRequestsAndMetaInWorkspace>();
|
||||
let requestGroupIdQueue = [...workspaceIds];
|
||||
const allRequestGroups: RequestGroup[] = [];
|
||||
// Map to track which workspace each request group belongs to
|
||||
const requestGroupToWorkspaceId = new Map<string, string>();
|
||||
const requestToWorkspaceId = new Map<string, string>();
|
||||
const grpcRequestToWorkspaceId = new Map<string, string>();
|
||||
const wsRequestToWorkspaceId = new Map<string, string>();
|
||||
const socketIORequestToWorkspaceId = new Map<string, string>();
|
||||
// Initialize the map with workspace IDs
|
||||
workspaceIds.forEach(workspaceId => {
|
||||
requestGroupToWorkspaceId.set(workspaceId, workspaceId);
|
||||
requestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
grpcRequestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
wsRequestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
socketIORequestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
allRequestsAndMetaByWorkspaceId.set(workspaceId, { allRequests: [], allRequestMetas: [], requestGroupMetas: [] });
|
||||
});
|
||||
|
||||
while (requestGroupIdQueue.length) {
|
||||
const requestGroups = await database.find<RequestGroup>(models.requestGroup.type, {
|
||||
parentId: { $in: requestGroupIdQueue },
|
||||
});
|
||||
|
||||
if (requestGroups.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
requestGroups.forEach(requestGroup => {
|
||||
const workspaceId = requestGroupToWorkspaceId.get(requestGroup.parentId);
|
||||
if (workspaceId) {
|
||||
requestGroupToWorkspaceId.set(requestGroup._id, workspaceId);
|
||||
}
|
||||
});
|
||||
|
||||
allRequestGroups.push(...requestGroups);
|
||||
requestGroupIdQueue = requestGroups.map(rg => rg._id);
|
||||
}
|
||||
|
||||
const listOfParentIds = [...workspaceIds, ...allRequestGroups.map(requestGroup => requestGroup._id)];
|
||||
|
||||
const [reqs, grpcReqs, wsReqs, socketIOReqs] = await Promise.all([
|
||||
database.find(models.request.type, { parentId: { $in: listOfParentIds } }),
|
||||
database.find<GrpcRequest>(models.grpcRequest.type, { parentId: { $in: listOfParentIds } }),
|
||||
database.find(models.webSocketRequest.type, { parentId: { $in: listOfParentIds } }),
|
||||
database.find(models.socketIORequest.type, { parentId: { $in: listOfParentIds } }),
|
||||
]);
|
||||
|
||||
const allRequests = [...reqs, ...allRequestGroups, ...grpcReqs, ...wsReqs, ...socketIOReqs] as AllRequestDoc[];
|
||||
|
||||
const [requestMetas, grpcRequestMetas, requestGroupMetas, wsRequestMetas, socketIORequestMetas] = await Promise.all([
|
||||
database.find<RequestMeta>(models.requestMeta.type, { parentId: { $in: reqs.map(r => r._id) } }),
|
||||
database.find<GrpcRequestMeta>(models.grpcRequestMeta.type, {
|
||||
parentId: { $in: grpcReqs.map(r => r._id) },
|
||||
}),
|
||||
database.find<RequestGroupMeta>(models.requestGroupMeta.type, {
|
||||
parentId: { $in: allRequestGroups.map(requestGroup => requestGroup._id) },
|
||||
}),
|
||||
database.find<WebSocketRequestMeta>(models.webSocketRequestMeta.type, {
|
||||
parentId: { $in: wsReqs.map(r => r._id) },
|
||||
}),
|
||||
database.find<SocketIORequestMeta>(models.socketIORequestMeta.type, {
|
||||
parentId: { $in: socketIOReqs.map(r => r._id) },
|
||||
}),
|
||||
]);
|
||||
|
||||
const allRequestMetas = [...requestMetas, ...grpcRequestMetas, ...wsRequestMetas, ...socketIORequestMetas];
|
||||
// Associate requests with their workspace IDs and group request metas by workspace ID
|
||||
allRequests.forEach(request => {
|
||||
const { parentId, _id: requestId } = request;
|
||||
const workspaceId = requestGroupToWorkspaceId.get(parentId);
|
||||
if (workspaceId) {
|
||||
// Track which workspace this request belongs to
|
||||
if (models.grpcRequest.isGrpcRequest(request)) {
|
||||
grpcRequestToWorkspaceId.set(requestId, workspaceId);
|
||||
} else if (models.request.isRequest(request)) {
|
||||
requestToWorkspaceId.set(requestId, workspaceId);
|
||||
} else if (models.webSocketRequest.isWebSocketRequest(request)) {
|
||||
wsRequestToWorkspaceId.set(requestId, workspaceId);
|
||||
} else if (models.socketIORequest.isSocketIORequest(request)) {
|
||||
socketIORequestToWorkspaceId.set(requestId, workspaceId);
|
||||
}
|
||||
const workspaceData = allRequestsAndMetaByWorkspaceId.get(workspaceId);
|
||||
if (workspaceData) {
|
||||
workspaceData.allRequests.push(request);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Build map of requestGroupMetas by workspace ID
|
||||
requestGroupMetas.forEach(requestGroupMeta => {
|
||||
const workspaceId = requestGroupToWorkspaceId.get(requestGroupMeta.parentId);
|
||||
if (workspaceId) {
|
||||
const workspaceData = allRequestsAndMetaByWorkspaceId.get(workspaceId);
|
||||
if (workspaceData) {
|
||||
workspaceData.requestGroupMetas.push(requestGroupMeta);
|
||||
}
|
||||
}
|
||||
});
|
||||
allRequestMetas.forEach(requestMeta => {
|
||||
const requestOrGrpcRequestId = requestMeta.parentId;
|
||||
let workspaceId: string | undefined;
|
||||
if (models.request.isRequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = requestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
} else if (models.grpcRequest.isGrpcRequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = grpcRequestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
} else if (models.webSocketRequest.isWebSocketRequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = wsRequestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
} else if (models.socketIORequest.isSocketIORequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = socketIORequestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
}
|
||||
if (workspaceId) {
|
||||
const workspaceData = allRequestsAndMetaByWorkspaceId.get(workspaceId);
|
||||
if (workspaceData) {
|
||||
workspaceData.allRequestMetas.push(requestMeta);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return allRequestsAndMetaByWorkspaceId;
|
||||
}
|
||||
|
||||
export function flattenCollectionChildren(
|
||||
workspaceId: string,
|
||||
parentIsCollapsed: boolean,
|
||||
{ allRequests, allRequestMetas, requestGroupMetas }: AllRequestsAndMetaInWorkspace,
|
||||
collectionWorkspaceChildren: CollectionWorkspaceChildren,
|
||||
sortOrder: keyof typeof sortMethodMap = 'type-manual',
|
||||
): Child[] {
|
||||
const { isRequestGroup } = models.requestGroup;
|
||||
const allRequests = collectionWorkspaceChildren.children.requestsAndGroups;
|
||||
const { allRequestMetas, requestGroupMetas } = collectionWorkspaceChildren.childrenMetas;
|
||||
|
||||
const collection: Child[] = [];
|
||||
|
||||
// map of parentId to its direct children requests and request groups
|
||||
const requestsByParentId = new Map<string, AllRequestDoc[]>();
|
||||
const requestsByParentId = new Map<string, CollectionChildDoc[]>();
|
||||
|
||||
for (const req of allRequests) {
|
||||
const allRequestsByParentId = requestsByParentId.get(req.parentId);
|
||||
if (allRequestsByParentId) {
|
||||
@@ -204,7 +54,7 @@ export function flattenCollectionChildren(
|
||||
}
|
||||
const sortFunction = sortMethodMap[sortOrder];
|
||||
const rootRequests = (requestsByParentId.get(workspaceId) || []).sort(sortFunction);
|
||||
const stack: { doc: AllRequestDoc; level: number; parentIsCollapsed: boolean; ancestors: string[] }[] = [
|
||||
const stack: { doc: CollectionChildDoc; level: number; parentIsCollapsed: boolean; ancestors: string[] }[] = [
|
||||
...rootRequests,
|
||||
]
|
||||
.reverse()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import type { StorageRules } from 'insomnia-api';
|
||||
import type { RequestGroup, Workspace } from 'insomnia-data';
|
||||
@@ -36,7 +37,6 @@ import { getUnsyncedRemoteWorkspaces, type InsomniaFile } from '~/common/project
|
||||
import { sortMethodMap } from '~/common/sorting';
|
||||
import type { SyncResult } from '~/konnect/sync';
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { useProjectLoaderData } from '~/routes/organization.$organizationId.project.$projectId';
|
||||
import { AnalyticsEvent } from '~/ui/analytics';
|
||||
import type { WorkspaceSortOrder } from '~/ui/components/dropdowns/sidebar-project-dropdown';
|
||||
import { KongLogo } from '~/ui/components/kong-logo';
|
||||
@@ -47,24 +47,23 @@ import { EmptyNode } from '~/ui/components/sidebar/project-navigation-sidebar/em
|
||||
import { KonnectEnvOnboarding } from '~/ui/components/sidebar/project-navigation-sidebar/konnect-env-onboarding';
|
||||
import { KonnectSyncIntro } from '~/ui/components/sidebar/project-navigation-sidebar/konnect-sync-intro/konnect-sync-intro';
|
||||
import { UnsyncedWorkspaceNode } from '~/ui/components/sidebar/project-navigation-sidebar/unsynced-workspace-node';
|
||||
import { useInsomniaEventStreamContext } from '~/ui/context/app/insomnia-event-stream-context';
|
||||
import uiEventBus, { CLOUD_SYNC_FILE_CHANGE } from '~/ui/event-bus';
|
||||
import { useProjects } from '~/ui/hooks/data/projects';
|
||||
import {
|
||||
type CollectionWorkspaceChildren,
|
||||
useWorkspaceChildrenByWorkspaceIds,
|
||||
workspaceChildrenKeys,
|
||||
} from '~/ui/hooks/data/workspace-children';
|
||||
import { useWorkspacesByProjectIds } from '~/ui/hooks/data/workspaces';
|
||||
import { useTabNavigate } from '~/ui/hooks/use-insomnia-tab';
|
||||
import { useKonnectSync } from '~/ui/hooks/use-konnect-sync';
|
||||
import { useLoaderDeferData } from '~/ui/hooks/use-loader-defer-data';
|
||||
import { useOrganizationPermissions } from '~/ui/hooks/use-organization-features';
|
||||
import insomniaLogo from '~/ui/images/insomnia-logo.svg';
|
||||
import { isPrimaryClickModifier } from '~/ui/utils';
|
||||
import { getAllRemoteBackendProjectsOfOrg } from '~/ui/utils/remote-projects';
|
||||
|
||||
import { Icon } from '../../icon';
|
||||
import {
|
||||
type AllRequestsAndMetaInWorkspace,
|
||||
filterCollection,
|
||||
flattenCollectionChildren,
|
||||
getAllRequestsAndMetaByWorkspace,
|
||||
getWorkspacesByProjectIds,
|
||||
} from './project-navigation-sidebar-utils';
|
||||
import { filterCollection, flattenCollectionChildren } from './project-navigation-sidebar-utils';
|
||||
import { ProjectNode } from './project-node';
|
||||
import { PinnedHeaderNode, RequestNode } from './request-node';
|
||||
import type { FlatItem } from './types';
|
||||
@@ -202,19 +201,12 @@ const ProjectNavigationSidebarInner = (
|
||||
requestId?: string;
|
||||
requestGroupId?: string;
|
||||
};
|
||||
const { userSession, settings } = useRootLoaderData()!;
|
||||
const projectLoaderData = useProjectLoaderData()!;
|
||||
const { projects, projectsSyncStatusPromise } = projectLoaderData;
|
||||
const [checkAllProjectSyncStatus] = useLoaderDeferData<Record<string, boolean>>(
|
||||
projectsSyncStatusPromise,
|
||||
organizationId,
|
||||
);
|
||||
const { presence } = useInsomniaEventStreamContext();
|
||||
const { settings } = useRootLoaderData()!;
|
||||
|
||||
const [searchParams, _setSearchParams] = useSearchParams();
|
||||
const tabNavigate = useTabNavigate();
|
||||
|
||||
const [collectionSortOrders, setCollectionSortOrders] = useState<Record<string, SortOrder>>({});
|
||||
const [flatItems, setFlatItems] = useState<FlatItem[]>([]);
|
||||
const [projectWorkspaceSortOrder, setProjectWorkspaceSortOrder] = useState<Record<string, WorkspaceSortOrder>>({});
|
||||
const [unsyncedFilesByProjectId, setUnsyncedFilesByProjectId] = useState<Map<string, InsomniaFile[]>>(new Map());
|
||||
// Customized workspace sort orders by projectId
|
||||
@@ -241,6 +233,31 @@ const ProjectNavigationSidebarInner = (
|
||||
null,
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { data: projects = [] } = useProjects(organizationId);
|
||||
// Show konnect or none-konnect projects based on selected tab
|
||||
const activeProjects = useMemo(
|
||||
() => projects.filter(isProjectTabActive ? p => !p.konnectControlPlaneId : p => p.konnectControlPlaneId != null),
|
||||
[isProjectTabActive, projects],
|
||||
);
|
||||
const projectIds = useMemo(() => activeProjects.map(p => p._id), [activeProjects]);
|
||||
const workspacesByProjectId = useWorkspacesByProjectIds(projectIds);
|
||||
// Get the list of collection workspace ids that should be cached based on the current filter and expanded projects/workspaces.
|
||||
const collectionWorkspaceIds = useMemo(() => {
|
||||
const ids: string[] = [];
|
||||
projectIds.forEach(projectId => {
|
||||
(workspacesByProjectId.get(projectId) || []).forEach(workspace => {
|
||||
if (
|
||||
workspace.scope === 'collection' &&
|
||||
(!!projectNavigationSidebarFilter || (expandedProjectAndWorkspaceIds || []).includes(workspace._id))
|
||||
) {
|
||||
ids.push(workspace._id);
|
||||
}
|
||||
});
|
||||
});
|
||||
return ids;
|
||||
}, [projectIds, workspacesByProjectId, projectNavigationSidebarFilter, expandedProjectAndWorkspaceIds]);
|
||||
const collectionByWorkspaceId = useWorkspaceChildrenByWorkspaceIds(collectionWorkspaceIds, 'collection');
|
||||
const nonKonnectProjects = projects.filter(p => !p.konnectControlPlaneId);
|
||||
const konnectProjects = projects.filter(p => p.konnectControlPlaneId != null);
|
||||
const [filterInputValue, setFilterInputValue] = useState(projectNavigationSidebarFilter || '');
|
||||
@@ -269,46 +286,17 @@ const ProjectNavigationSidebarInner = (
|
||||
reactUse.useDebounce(() => setProjectNavigationSidebarFilter(filterInputValue), 300, [filterInputValue]);
|
||||
reactUse.useDebounce(() => setKonnectFilter(konnectFilterInputValue), 300, [konnectFilterInputValue]);
|
||||
const activeFilter = ((isProjectTabActive ? projectNavigationSidebarFilter : konnectFilter) || '').trim();
|
||||
// ref to cache queried workspaces by project id
|
||||
const cachedWorkspacesRef = useRef<Map<string, Workspace[]>>(new Map());
|
||||
// ref to cache queried collection children (request & requestGroups) data and meta by workspace id
|
||||
const cachedCollectionChildrenAndMetaRef = useRef<Map<string, AllRequestsAndMetaInWorkspace>>(new Map());
|
||||
// ref to track whether we are currently fetching unsynced files for cloud sync projects to avoid duplicate requests
|
||||
|
||||
const isFetchingUnsyncedFilesRef = useRef(false);
|
||||
|
||||
const syncKonnectProjectsAndNotifyRef = useRef<() => Promise<void>>(async () => {});
|
||||
|
||||
const isScratchPad = activeProjectId === models.project.SCRATCHPAD_PROJECT_ID;
|
||||
|
||||
const projectsWithPresence = useMemo(
|
||||
() =>
|
||||
projects
|
||||
.filter(isProjectTabActive ? p => !p.konnectControlPlaneId : p => p.konnectControlPlaneId != null)
|
||||
.map(project => {
|
||||
const projectPresence = presence
|
||||
.filter(p => p.project === project.remoteId)
|
||||
.filter(p => p.acct !== userSession.accountId)
|
||||
.map(user => {
|
||||
return {
|
||||
key: user.acct,
|
||||
alt: user.firstName || user.lastName ? `${user.firstName} ${user.lastName}` : user.acct,
|
||||
src: user.avatar,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...project,
|
||||
presence: projectPresence,
|
||||
hasUncommittedOrUnpushedChanges:
|
||||
checkAllProjectSyncStatus?.[project._id] ||
|
||||
project.gitRepository?.hasUncommittedChanges ||
|
||||
project.gitRepository?.hasUnpushedChanges,
|
||||
};
|
||||
}),
|
||||
[projects, isProjectTabActive, presence, checkAllProjectSyncStatus, userSession.accountId],
|
||||
);
|
||||
|
||||
const cloudSyncProjects = useMemo(() => projects.filter(p => models.project.isRemoteProject(p)), [projects]);
|
||||
// Generate a stable string key to trigger getOrFetchUnsyncedFiles when the list of cloud sync projects changes.
|
||||
const cloudSyncProjects = useMemo(
|
||||
() => activeProjects.filter(p => models.project.isRemoteProject(p)),
|
||||
[activeProjects],
|
||||
); // Generate a stable string key to trigger getOrFetchUnsyncedFiles when the list of cloud sync projects changes.
|
||||
const cloudSyncProjectIdsKey = useMemo(
|
||||
() =>
|
||||
cloudSyncProjects
|
||||
@@ -474,7 +462,7 @@ const ProjectNavigationSidebarInner = (
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [projectNavigationSidebarFilter, konnectFilter]);
|
||||
}, [projectNavigationSidebarFilter, konnectFilter, activeProjectId]);
|
||||
|
||||
useEffect(() => {
|
||||
getAllRemoteFilesByProjectId();
|
||||
@@ -486,57 +474,20 @@ const ProjectNavigationSidebarInner = (
|
||||
return uiEventBus.on(CLOUD_SYNC_FILE_CHANGE, updateUnsyncedFiles);
|
||||
}, [getAllRemoteFilesByProjectId, organizationId]);
|
||||
|
||||
useEffect(() => {
|
||||
// clear caches on any router data change to avoid showing stale data
|
||||
cachedWorkspacesRef.current.clear();
|
||||
cachedCollectionChildrenAndMetaRef.current.clear();
|
||||
}, [projectLoaderData]);
|
||||
// Note: the local-db → query-cache sync (invalidate/patch on db.changes) is registered once at the
|
||||
// provider level in `subscribeQueryClientToDbChanges`, so it stays active even when this sidebar is
|
||||
// unmounted. Do not re-register it here.
|
||||
|
||||
useEffect(() => {
|
||||
const tryToGetWorkspacesFromCache = async (projectIds: string[]) => {
|
||||
const uncachedProjectIds = projectIds.filter(id => !cachedWorkspacesRef.current.has(id));
|
||||
if (uncachedProjectIds.length > 0) {
|
||||
const workspacesByProjectId = await getWorkspacesByProjectIds(uncachedProjectIds);
|
||||
for (const [projectId, workspaces] of workspacesByProjectId.entries()) {
|
||||
cachedWorkspacesRef.current.set(projectId, workspaces);
|
||||
}
|
||||
}
|
||||
return cachedWorkspacesRef.current;
|
||||
};
|
||||
const tryToGetCollectionChildrenAndMetaFromCache = async (workspaceIds: string[]) => {
|
||||
const uncachedWorkspaceIds = workspaceIds.filter(id => !cachedCollectionChildrenAndMetaRef.current.has(id));
|
||||
if (uncachedWorkspaceIds.length > 0) {
|
||||
const collectionChildAndMetaByWorkspaceId = await getAllRequestsAndMetaByWorkspace(uncachedWorkspaceIds);
|
||||
for (const [workspaceId, collectionChildrenAndMeta] of collectionChildAndMetaByWorkspaceId.entries()) {
|
||||
cachedCollectionChildrenAndMetaRef.current.set(workspaceId, collectionChildrenAndMeta);
|
||||
}
|
||||
}
|
||||
return cachedCollectionChildrenAndMetaRef.current;
|
||||
};
|
||||
|
||||
const buildWorkspaceAndCollectionData = async () => {
|
||||
const flatItems = useMemo<FlatItem[]>(() => {
|
||||
const buildWorkspaceAndCollectionData = (): FlatItem[] => {
|
||||
const items: FlatItem[] = [];
|
||||
// Array of project and collection workspace ids that should get data from db
|
||||
const activeFilterLower = activeFilter.toLowerCase();
|
||||
|
||||
const projectIds = projectsWithPresence.map(p => p._id);
|
||||
const collectionWorkspaceIds: string[] = [];
|
||||
const workspacesByProject = await tryToGetWorkspacesFromCache(projectIds);
|
||||
projectIds.forEach(projectId => {
|
||||
const workspaces = workspacesByProject.get(projectId) || [];
|
||||
workspaces.forEach(wk => {
|
||||
if (
|
||||
wk.scope === 'collection' &&
|
||||
// Fetch collection children and meta if 1) the workspace is expanded or 2) there is an active filter
|
||||
(!!activeFilter || (expandedProjectAndWorkspaceIds || []).includes(wk._id))
|
||||
) {
|
||||
collectionWorkspaceIds.push(wk._id);
|
||||
}
|
||||
});
|
||||
});
|
||||
const collectionChildrenAndMetaByWorkspaceId =
|
||||
await tryToGetCollectionChildrenAndMetaFromCache(collectionWorkspaceIds);
|
||||
for (const project of projectsWithPresence) {
|
||||
const workspacesByProject = workspacesByProjectId;
|
||||
const collectionChildrenAndMetaByWorkspaceId = collectionByWorkspaceId;
|
||||
|
||||
for (const project of activeProjects) {
|
||||
const projectId = project._id;
|
||||
const isProjectCollapsed = !(expandedProjectAndWorkspaceIds ?? []).includes(projectId);
|
||||
items.push({
|
||||
@@ -620,7 +571,9 @@ const ProjectNavigationSidebarInner = (
|
||||
hidden: isProjectCollapsed,
|
||||
});
|
||||
|
||||
const allRequestsAndMetaInWorkspace = collectionChildrenAndMetaByWorkspaceId.get(workspaceId);
|
||||
const allRequestsAndMetaInWorkspace = collectionChildrenAndMetaByWorkspaceId.get(
|
||||
workspaceId,
|
||||
) as CollectionWorkspaceChildren;
|
||||
// build collection children if it's a collection workspace and parent workspace and project are not collapsed or there is an active filter
|
||||
const shouldHideCollectionChildren = isWorkspaceCollapsed || isProjectCollapsed;
|
||||
let collectionChildren =
|
||||
@@ -747,19 +700,20 @@ const ProjectNavigationSidebarInner = (
|
||||
});
|
||||
}
|
||||
|
||||
setFlatItems(items);
|
||||
return items;
|
||||
};
|
||||
buildWorkspaceAndCollectionData();
|
||||
return buildWorkspaceAndCollectionData();
|
||||
}, [
|
||||
activeFilter,
|
||||
activeProjects,
|
||||
collectionByWorkspaceId,
|
||||
collectionSortOrders,
|
||||
projectWorkspaceSortOrder,
|
||||
expandedProjectAndWorkspaceIds,
|
||||
isProjectTabActive,
|
||||
localWorkspaceOrders,
|
||||
organizationId,
|
||||
projectsWithPresence,
|
||||
projectWorkspaceSortOrder,
|
||||
unsyncedFilesByProjectId,
|
||||
workspacesByProjectId,
|
||||
]);
|
||||
|
||||
const handleLocalWorkspaceReorder = useCallback(
|
||||
@@ -771,7 +725,7 @@ const ProjectNavigationSidebarInner = (
|
||||
dropPosition: 'before' | 'after',
|
||||
) => {
|
||||
const isMoveToDifferentProject = sourceProjectId !== targetProjectId;
|
||||
const workspaces = cachedWorkspacesRef.current.get(targetProjectId) || [];
|
||||
const workspaces = workspacesByProjectId.get(targetProjectId) || [];
|
||||
const currentWorkspaceSortOrder = projectWorkspaceSortOrder[targetProjectId] || 'type-manual';
|
||||
// Get the base order of workspace before re-order
|
||||
const baseOrder =
|
||||
@@ -801,7 +755,7 @@ const ProjectNavigationSidebarInner = (
|
||||
}
|
||||
setLocalWorkspaceOrders({ ...localWorkspaceOrders, [targetProjectId]: reordered });
|
||||
},
|
||||
[projectWorkspaceSortOrder, setLocalWorkspaceOrders, localWorkspaceOrders],
|
||||
[workspacesByProjectId, projectWorkspaceSortOrder, localWorkspaceOrders, setLocalWorkspaceOrders],
|
||||
);
|
||||
|
||||
const toggleProjectOrWorkspace = useCallback(
|
||||
@@ -866,98 +820,35 @@ const ProjectNavigationSidebarInner = (
|
||||
};
|
||||
});
|
||||
|
||||
const collapsedByRequestGroupId = new Map(
|
||||
nextStates.map(({ requestGroupId, collapsed }) => [requestGroupId, collapsed]),
|
||||
);
|
||||
|
||||
// Optimistically update the cached collection so the tree re-renders immediately.
|
||||
queryClient.setQueryData<CollectionWorkspaceChildren>(workspaceChildrenKeys.details(workspace._id), previous => {
|
||||
if (!previous) {
|
||||
return previous;
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
childrenMetas: {
|
||||
...previous.childrenMetas,
|
||||
requestGroupMetas: previous.childrenMetas.requestGroupMetas.map(requestGroupMeta =>
|
||||
collapsedByRequestGroupId.has(requestGroupMeta.parentId)
|
||||
? { ...requestGroupMeta, collapsed: collapsedByRequestGroupId.get(requestGroupMeta.parentId)! }
|
||||
: requestGroupMeta,
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
// Persist the change to database
|
||||
await Promise.all(
|
||||
nextStates.map(({ requestGroupId, collapsed }) =>
|
||||
services.requestGroupMeta.updateOrCreateForParentId(requestGroupId, { collapsed }),
|
||||
),
|
||||
);
|
||||
// Update the collapsed state in the cache.
|
||||
cachedCollectionChildrenAndMetaRef.current.forEach(workspaceData => {
|
||||
workspaceData.requestGroupMetas.forEach(requestGroupMeta => {
|
||||
const nextState = nextStates.find(({ requestGroupId }) => requestGroupId === requestGroupMeta.parentId);
|
||||
if (nextState) {
|
||||
requestGroupMeta.collapsed = nextState.collapsed;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setFlatItems(previousFlatItems =>
|
||||
nextStates.reduce((nextFlatItems, { requestGroupId, collapsed }) => {
|
||||
const toggledChildren: { id: string; parentIsCollapsed: boolean }[] = [];
|
||||
|
||||
return nextFlatItems.map(item => {
|
||||
if (item.kind === 'collectionChild') {
|
||||
const { children, doc } = item;
|
||||
// Find toggled request group and update collapsed state
|
||||
if (doc._id === requestGroupId) {
|
||||
// Add all children of the toggled request group to the array to update their hidden state
|
||||
toggledChildren.push(
|
||||
...(children?.map(child => ({ id: child.doc._id, parentIsCollapsed: collapsed })) ?? []),
|
||||
);
|
||||
|
||||
return {
|
||||
...item,
|
||||
collapsed,
|
||||
hidden: false,
|
||||
};
|
||||
}
|
||||
|
||||
const matchedToggledChild = toggledChildren.find(tc => tc.id === item.doc._id);
|
||||
if (matchedToggledChild) {
|
||||
const { parentIsCollapsed } = matchedToggledChild;
|
||||
if (models.requestGroup.isRequestGroupId(doc._id)) {
|
||||
// Add children of the toggled child request group to the array to update their hidden state
|
||||
const isToggledRequestGroupCollapsed =
|
||||
parentIsCollapsed ||
|
||||
cachedCollectionChildrenAndMetaRef.current
|
||||
.get(workspace._id)
|
||||
?.requestGroupMetas.find(rgm => rgm.parentId === doc._id)?.collapsed ||
|
||||
false;
|
||||
toggledChildren.push(
|
||||
...(item.children?.map(child => ({
|
||||
id: child.doc._id,
|
||||
parentIsCollapsed: isToggledRequestGroupCollapsed,
|
||||
})) ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
hidden: parentIsCollapsed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (item.kind === 'emptyFolder') {
|
||||
const parentFolder = item.requestGroup;
|
||||
const parentFolderId = parentFolder?._id;
|
||||
const matchedToggledChild = toggledChildren.find(tc => tc.id === parentFolderId);
|
||||
// Update the emptyFolder node hidden state based on its parent request group collapsed state.
|
||||
if (parentFolderId === requestGroupId) {
|
||||
return {
|
||||
...item,
|
||||
hidden: collapsed,
|
||||
};
|
||||
} else if (matchedToggledChild) {
|
||||
return {
|
||||
...item,
|
||||
hidden:
|
||||
matchedToggledChild.parentIsCollapsed ||
|
||||
// If the parent folder is toggled to be expanded, the empty folder node should still be hidden if its parent request group is collapsed.
|
||||
cachedCollectionChildrenAndMetaRef.current
|
||||
.get(workspace._id)
|
||||
?.requestGroupMetas.find(rgm => rgm.parentId === matchedToggledChild.id)?.collapsed ||
|
||||
false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}, previousFlatItems),
|
||||
);
|
||||
},
|
||||
[activeFilter],
|
||||
[activeFilter, queryClient],
|
||||
);
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -1121,7 +1012,11 @@ const ProjectNavigationSidebarInner = (
|
||||
if (routeInfo?.resourceId === docId) {
|
||||
toggleProjectOrWorkspace(docId);
|
||||
} else {
|
||||
!isScratchPad && window.main.trackAnalyticsEvent({ event: AnalyticsEvent.projectSwitched, properties: { project_id: docId } });
|
||||
!isScratchPad &&
|
||||
window.main.trackAnalyticsEvent({
|
||||
event: AnalyticsEvent.projectSwitched,
|
||||
properties: { project_id: docId },
|
||||
});
|
||||
!isScratchPad && navigate(`/organization/${organizationId}/project/${docId}`);
|
||||
}
|
||||
} else if (item.kind === 'workspace') {
|
||||
|
||||
@@ -3,14 +3,17 @@ import { models } from 'insomnia-data';
|
||||
import { useState } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { useProjectLoaderData } from '~/routes/organization.$organizationId.project.$projectId';
|
||||
import { ProjectDropdown, type WorkspaceSortOrder } from '~/ui/components/dropdowns/sidebar-project-dropdown';
|
||||
import { useInsomniaEventStreamContext } from '~/ui/context/app/insomnia-event-stream-context';
|
||||
import { useLoaderDeferData } from '~/ui/hooks/use-loader-defer-data';
|
||||
|
||||
import { AvatarGroup } from '../../avatar';
|
||||
import { Icon } from '../../icon';
|
||||
import { KonnectProjectIcon } from './konnect-project-icon/konnect-project-icon';
|
||||
import { ACTIVE_BORDER_CLASS, ICON_CLASS, ROW_CLASS, TOGGLE_BTN_CLASS } from './project-navigation-sidebar-utils';
|
||||
import { type ProjectFlatItem } from './types';
|
||||
|
||||
interface ProjectNodeProps {
|
||||
item: ProjectFlatItem;
|
||||
storageRules: StorageRules;
|
||||
@@ -21,8 +24,30 @@ interface ProjectNodeProps {
|
||||
|
||||
export const ProjectNode = ({ item, storageRules, onToggle, sortOrder, onSortOrderChange }: ProjectNodeProps) => {
|
||||
const { doc, collapsed, organizationId } = item;
|
||||
const { name: projectName, presence, _id: projectId } = doc;
|
||||
const { userSession } = useRootLoaderData()!;
|
||||
const { presence } = useInsomniaEventStreamContext();
|
||||
const { name: projectName, _id: projectId } = doc;
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
||||
const { projectsSyncStatusPromise } = useProjectLoaderData()!;
|
||||
const [checkAllProjectSyncStatus] = useLoaderDeferData<Record<string, boolean>>(
|
||||
projectsSyncStatusPromise,
|
||||
organizationId,
|
||||
);
|
||||
const hasUncommittedOrUnpushedChanges =
|
||||
checkAllProjectSyncStatus?.[projectId] ||
|
||||
doc.gitRepository?.hasUncommittedChanges ||
|
||||
doc.gitRepository?.hasUnpushedChanges;
|
||||
|
||||
const projectPresence = presence
|
||||
.filter(p => p.project === doc.remoteId)
|
||||
.filter(p => p.acct !== userSession.accountId)
|
||||
.map(user => {
|
||||
return {
|
||||
key: user.acct,
|
||||
alt: user.firstName || user.lastName ? `${user.firstName} ${user.lastName}` : user.acct,
|
||||
src: user.avatar,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -59,11 +84,14 @@ export const ProjectNode = ({ item, storageRules, onToggle, sortOrder, onSortOrd
|
||||
)}
|
||||
<span className="min-w-0 flex-1 truncate text-base text-[rgb(var(--color-font-rgb),0.8)]">{projectName}</span>
|
||||
</div>
|
||||
{presence.length > 0 && <AvatarGroup size="small" maxAvatars={3} items={presence} />}
|
||||
{projectPresence.length > 0 && <AvatarGroup size="small" maxAvatars={3} items={projectPresence} />}{' '}
|
||||
{projectId !== models.project.SCRATCHPAD_PROJECT_ID && (
|
||||
<ProjectDropdown
|
||||
organizationId={organizationId}
|
||||
project={doc}
|
||||
project={{
|
||||
...doc,
|
||||
hasUncommittedOrUnpushedChanges,
|
||||
}}
|
||||
storageRules={storageRules}
|
||||
sortOrder={sortOrder}
|
||||
onSortOrderChange={onSortOrderChange}
|
||||
|
||||
@@ -3,15 +3,9 @@ import type { BaseModel, GitRepository, Project, RequestGroup, Workspace, Worksp
|
||||
import type { InsomniaFile } from '~/common/project';
|
||||
import type { Child } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
|
||||
|
||||
export type ProjectWithPresence = Project & {
|
||||
export type ProjectWithGitRepository = Project & {
|
||||
gitRepository?: GitRepository;
|
||||
presence: {
|
||||
key: string;
|
||||
alt: string;
|
||||
src: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export interface WorkspaceSummary {
|
||||
workspace: Workspace;
|
||||
meta: WorkspaceMeta;
|
||||
@@ -28,14 +22,14 @@ interface BaseFlatItem<T extends BaseModel> {
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export interface ProjectFlatItem extends BaseFlatItem<ProjectWithPresence> {
|
||||
export interface ProjectFlatItem extends BaseFlatItem<ProjectWithGitRepository> {
|
||||
kind: 'project';
|
||||
}
|
||||
|
||||
export interface WorkspaceFlatItem extends BaseFlatItem<Workspace> {
|
||||
kind: 'workspace';
|
||||
// parent project
|
||||
project: ProjectWithPresence;
|
||||
project: ProjectWithGitRepository;
|
||||
}
|
||||
|
||||
// Unsynced workspace in cloud sync project
|
||||
@@ -50,7 +44,7 @@ export type UnsyncedWorkspaceFlatItem = Omit<BaseFlatItem<any>, 'doc'> &
|
||||
export interface CollectionChildFlatItem extends BaseFlatItem<Child['doc']> {
|
||||
kind: 'collectionChild';
|
||||
// parent project
|
||||
project: ProjectWithPresence;
|
||||
project: ProjectWithGitRepository;
|
||||
// parent workspace
|
||||
workspace: Workspace;
|
||||
// nested children for request group
|
||||
@@ -77,7 +71,7 @@ export interface EmptyNodeFlatItem {
|
||||
hidden: boolean;
|
||||
organizationId: string;
|
||||
doc: { _id: string; name: string };
|
||||
project: ProjectWithPresence;
|
||||
project: ProjectWithGitRepository;
|
||||
workspace?: Workspace;
|
||||
requestGroup?: RequestGroup;
|
||||
level?: number;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { type PropsWithChildren, useEffect } from 'react';
|
||||
|
||||
import { subscribeQueryClientToDbChanges } from '~/ui/hooks/data/db-changes-sync';
|
||||
|
||||
// Single shared QueryClient instance for all database queries.
|
||||
// This client treats as the cache of the local NeDB database and will listen to db.change events
|
||||
export const dbQueryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: Infinity,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const InsomniaTanstackQueryClientContext = ({ children }: PropsWithChildren) => {
|
||||
useEffect(() => subscribeQueryClientToDbChanges(dbQueryClient), []);
|
||||
return <QueryClientProvider client={dbQueryClient}>{children}</QueryClientProvider>;
|
||||
};
|
||||
114
packages/insomnia/src/ui/hooks/data/db-changes-sync.ts
Normal file
114
packages/insomnia/src/ui/hooks/data/db-changes-sync.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { QueryClient } from '@tanstack/react-query';
|
||||
import { models } from 'insomnia-data';
|
||||
|
||||
import type { ChangeBufferEvent } from '~/common/database';
|
||||
|
||||
import { projectKeys } from './projects';
|
||||
import {
|
||||
type CollectionWorkspaceChildren,
|
||||
findWorkspaceIdsForChangedCollectionChildDoc,
|
||||
updateCollectionChildrenWithUpdatedDoc,
|
||||
workspaceChildrenKeys,
|
||||
} from './workspace-children';
|
||||
import { workspaceKeys } from './workspaces';
|
||||
|
||||
const COLLECTION_CHILDREN_DOC_TYPES = [
|
||||
// collection scope
|
||||
models.request.type,
|
||||
models.grpcRequest.type,
|
||||
models.webSocketRequest.type,
|
||||
models.socketIORequest.type,
|
||||
models.requestGroup.type,
|
||||
models.requestMeta.type,
|
||||
models.grpcRequestMeta.type,
|
||||
models.webSocketRequestMeta.type,
|
||||
models.socketIORequestMeta.type,
|
||||
models.requestGroupMeta.type,
|
||||
];
|
||||
|
||||
export const MONITOR_DOC_TYPES = [
|
||||
models.project.type,
|
||||
models.workspace.type,
|
||||
...COLLECTION_CHILDREN_DOC_TYPES,
|
||||
// mock-server scope
|
||||
models.mockServer.type,
|
||||
// design scope
|
||||
models.apiSpec.type,
|
||||
// mcp scope
|
||||
models.mcpRequest.type,
|
||||
// environment scope
|
||||
models.environment.type,
|
||||
];
|
||||
|
||||
// Bridge local NeDB change events to the TanStack cache: patch in place for plain field updates,
|
||||
export const subscribeQueryClientToDbChanges = (queryClient: QueryClient): (() => void) => {
|
||||
const unsubscribe = window.main.on('db.changes', (_, changes: ChangeBufferEvent[]) => {
|
||||
const organizationIdsToRevalidate: string[] = [];
|
||||
const projectIdsToRevalidate: string[] = [];
|
||||
const workspaceIdsToRevalidate: string[] = [];
|
||||
|
||||
for (const [event, doc] of changes) {
|
||||
if (!MONITOR_DOC_TYPES.includes(doc.type)) {
|
||||
continue;
|
||||
}
|
||||
if (doc.type === models.project.type) {
|
||||
// project changed: patch the owning organization’s cached project list in place when possible.
|
||||
organizationIdsToRevalidate.push(doc.parentId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (doc.type === models.workspace.type) {
|
||||
const projectId = doc.parentId;
|
||||
// workspace changed: refresh the owning project’s cached workspace list + meta list
|
||||
projectIdsToRevalidate.push(projectId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (COLLECTION_CHILDREN_DOC_TYPES.includes(doc.type)) {
|
||||
// request/requestGroup/meta changed: patch the owning workspace's cached collection in place when possible.
|
||||
for (const workspaceId of findWorkspaceIdsForChangedCollectionChildDoc(queryClient, doc)) {
|
||||
// Avoid full db refresh when it is an update event and we just need to update a single doc in the cache.
|
||||
if (event === 'update') {
|
||||
let updated = false;
|
||||
queryClient.setQueryData<CollectionWorkspaceChildren>(
|
||||
workspaceChildrenKeys.details(workspaceId),
|
||||
previous => {
|
||||
if (!previous) {
|
||||
return previous;
|
||||
}
|
||||
const updatedData = updateCollectionChildrenWithUpdatedDoc(previous, doc);
|
||||
if (updatedData) {
|
||||
updated = true;
|
||||
return updatedData;
|
||||
}
|
||||
return previous;
|
||||
},
|
||||
);
|
||||
if (updated) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Add, remove or changed doc can be updated (like due to move), refresh the entire workspaceChildrenKeys in the workspace
|
||||
workspaceIdsToRevalidate.push(workspaceId);
|
||||
}
|
||||
} else {
|
||||
// non-collection child doc changed (apiSpec/mockServer/environment/mcpRequest): refresh the workspace's
|
||||
// children cache. The project dashboard reads apiSpec/mockServer from this same cache, so it updates too.
|
||||
if (models.workspace.isWorkspaceId(doc.parentId)) {
|
||||
workspaceIdsToRevalidate.push(doc.parentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
organizationIdsToRevalidate.forEach(organizationId =>
|
||||
queryClient.invalidateQueries({ queryKey: projectKeys.details(organizationId) }),
|
||||
);
|
||||
projectIdsToRevalidate.forEach(projectId =>
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.details(projectId) }),
|
||||
);
|
||||
workspaceIdsToRevalidate.forEach(workspaceId =>
|
||||
queryClient.invalidateQueries({ queryKey: workspaceChildrenKeys.details(workspaceId) }),
|
||||
);
|
||||
});
|
||||
return unsubscribe;
|
||||
};
|
||||
23
packages/insomnia/src/ui/hooks/data/projects.ts
Normal file
23
packages/insomnia/src/ui/hooks/data/projects.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
|
||||
import type { GitRepository, Project } from 'insomnia-data';
|
||||
import { models } from 'insomnia-data';
|
||||
|
||||
import { getProjectsWithGitRepositories } from '~/common/project';
|
||||
|
||||
export type ProjectWithGitRepository = Project & { gitRepository?: GitRepository };
|
||||
|
||||
export const projectKeys = {
|
||||
all: ['projects'],
|
||||
details: (organizationId: string) => [...projectKeys.all, organizationId],
|
||||
detail: (projectId: string, organizationId: string) => [...projectKeys.details(organizationId), projectId],
|
||||
};
|
||||
|
||||
export const getProjects = async (organizationId: string): Promise<ProjectWithGitRepository[]> =>
|
||||
models.project.sortProjects(await getProjectsWithGitRepositories({ organizationId }));
|
||||
|
||||
export const useProjects = (organizationId: string): UseQueryResult<ProjectWithGitRepository[]> => {
|
||||
return useQuery({
|
||||
queryKey: projectKeys.details(organizationId),
|
||||
queryFn: () => getProjects(organizationId),
|
||||
});
|
||||
};
|
||||
289
packages/insomnia/src/ui/hooks/data/requests-and-metas.ts
Normal file
289
packages/insomnia/src/ui/hooks/data/requests-and-metas.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import type { QueryClient } from '@tanstack/react-query';
|
||||
import { useQueries, useQuery, type UseQueryResult } from '@tanstack/react-query';
|
||||
import type {
|
||||
BaseModel,
|
||||
GrpcRequest,
|
||||
GrpcRequestMeta,
|
||||
Request,
|
||||
RequestGroup,
|
||||
RequestGroupMeta,
|
||||
RequestMeta,
|
||||
SocketIORequest,
|
||||
SocketIORequestMeta,
|
||||
WebSocketRequest,
|
||||
WebSocketRequestMeta,
|
||||
} from 'insomnia-data';
|
||||
import { models } from 'insomnia-data';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { database } from '~/common/database';
|
||||
|
||||
type AllRequestDoc = Request | GrpcRequest | WebSocketRequest | SocketIORequest | RequestGroup;
|
||||
|
||||
export type { AllRequestDoc };
|
||||
|
||||
export interface AllRequestsAndMetaInWorkspace {
|
||||
allRequests: AllRequestDoc[];
|
||||
allRequestMetas: (RequestMeta | GrpcRequestMeta | WebSocketRequestMeta | SocketIORequestMeta)[];
|
||||
requestGroupMetas: RequestGroupMeta[];
|
||||
}
|
||||
|
||||
const EMPTY_REQUESTS_AND_META: AllRequestsAndMetaInWorkspace = {
|
||||
allRequests: [],
|
||||
allRequestMetas: [],
|
||||
requestGroupMetas: [],
|
||||
};
|
||||
|
||||
export const requestsAndMetaKeys = {
|
||||
all: ['requestsAndMetas'],
|
||||
detail: (workspaceId: string) => [...requestsAndMetaKeys.all, workspaceId],
|
||||
};
|
||||
|
||||
// ALL request doc types
|
||||
const REQUEST_DOC_TYPES: string[] = [
|
||||
models.request.type,
|
||||
models.grpcRequest.type,
|
||||
models.webSocketRequest.type,
|
||||
models.socketIORequest.type,
|
||||
models.requestGroup.type,
|
||||
];
|
||||
// All request meta doc types
|
||||
const REQUEST_META_DOC_TYPES: string[] = [
|
||||
models.requestMeta.type,
|
||||
models.grpcRequestMeta.type,
|
||||
models.webSocketRequestMeta.type,
|
||||
models.socketIORequestMeta.type,
|
||||
];
|
||||
|
||||
// Given a changed request/group/meta doc, find the cached workspace id(s) whose collection contains it.
|
||||
export const findWorkspaceIdsForChangedDoc = (queryClient: QueryClient, doc: BaseModel): string[] => {
|
||||
const workspaceIds: string[] = [];
|
||||
const cachedCollections = queryClient.getQueriesData<AllRequestsAndMetaInWorkspace>({
|
||||
queryKey: requestsAndMetaKeys.all,
|
||||
});
|
||||
for (const [queryKey, data] of cachedCollections) {
|
||||
const workspaceId = queryKey[1] as string;
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
doc.parentId === workspaceId ||
|
||||
data.allRequests.some(request => request._id === doc._id || request._id === doc.parentId)
|
||||
) {
|
||||
workspaceIds.push(workspaceId);
|
||||
}
|
||||
}
|
||||
return workspaceIds;
|
||||
};
|
||||
|
||||
// Apply an `update` change for a single doc to a cached workspace tree without re-querying NeDB.
|
||||
export function updateWorkspaceDataWithUpdatedDoc(
|
||||
data: AllRequestsAndMetaInWorkspace,
|
||||
doc: BaseModel,
|
||||
): AllRequestsAndMetaInWorkspace | null {
|
||||
const replaceById = <T extends BaseModel>(list: T[]): T[] | null => {
|
||||
const index = list.findIndex(item => item._id === doc._id);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
// The item has been moved to another parent, consider failure to update
|
||||
if (list[index].parentId !== doc.parentId) {
|
||||
return null;
|
||||
}
|
||||
const next = list.slice();
|
||||
next[index] = doc as unknown as T;
|
||||
return next;
|
||||
};
|
||||
|
||||
if (REQUEST_DOC_TYPES.includes(doc.type)) {
|
||||
const allRequests = replaceById(data.allRequests);
|
||||
return allRequests ? { ...data, allRequests } : null;
|
||||
}
|
||||
if (REQUEST_META_DOC_TYPES.includes(doc.type)) {
|
||||
const allRequestMetas = replaceById(data.allRequestMetas);
|
||||
return allRequestMetas ? { ...data, allRequestMetas } : null;
|
||||
}
|
||||
if (doc.type === models.requestGroupMeta.type) {
|
||||
const requestGroupMetas = replaceById(data.requestGroupMetas);
|
||||
return requestGroupMetas ? { ...data, requestGroupMetas } : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Batched read: walk the request-group tree for the given workspaces and group every
|
||||
// request, request and request-group meta under its owning workspace id.
|
||||
export async function getAllRequestsAndMetaByWorkspace(
|
||||
workspaceIds: string[],
|
||||
): Promise<Map<string, AllRequestsAndMetaInWorkspace>> {
|
||||
const allRequestsAndMetaByWorkspaceId = new Map<string, AllRequestsAndMetaInWorkspace>();
|
||||
let requestGroupIdQueue = [...workspaceIds];
|
||||
const allRequestGroups: RequestGroup[] = [];
|
||||
// Map to track which workspace each request group belongs to
|
||||
const requestGroupToWorkspaceId = new Map<string, string>();
|
||||
const requestToWorkspaceId = new Map<string, string>();
|
||||
const grpcRequestToWorkspaceId = new Map<string, string>();
|
||||
const wsRequestToWorkspaceId = new Map<string, string>();
|
||||
const socketIORequestToWorkspaceId = new Map<string, string>();
|
||||
// Initialize the map with workspace IDs
|
||||
workspaceIds.forEach(workspaceId => {
|
||||
requestGroupToWorkspaceId.set(workspaceId, workspaceId);
|
||||
requestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
grpcRequestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
wsRequestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
socketIORequestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
allRequestsAndMetaByWorkspaceId.set(workspaceId, { allRequests: [], allRequestMetas: [], requestGroupMetas: [] });
|
||||
});
|
||||
|
||||
while (requestGroupIdQueue.length) {
|
||||
const requestGroups = await database.find<RequestGroup>(models.requestGroup.type, {
|
||||
parentId: { $in: requestGroupIdQueue },
|
||||
});
|
||||
|
||||
if (requestGroups.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
requestGroups.forEach(requestGroup => {
|
||||
const workspaceId = requestGroupToWorkspaceId.get(requestGroup.parentId);
|
||||
if (workspaceId) {
|
||||
requestGroupToWorkspaceId.set(requestGroup._id, workspaceId);
|
||||
}
|
||||
});
|
||||
|
||||
allRequestGroups.push(...requestGroups);
|
||||
requestGroupIdQueue = requestGroups.map(rg => rg._id);
|
||||
}
|
||||
|
||||
const listOfParentIds = [...workspaceIds, ...allRequestGroups.map(requestGroup => requestGroup._id)];
|
||||
|
||||
const [reqs, grpcReqs, wsReqs, socketIOReqs] = await Promise.all([
|
||||
database.find(models.request.type, { parentId: { $in: listOfParentIds } }),
|
||||
database.find<GrpcRequest>(models.grpcRequest.type, { parentId: { $in: listOfParentIds } }),
|
||||
database.find(models.webSocketRequest.type, { parentId: { $in: listOfParentIds } }),
|
||||
database.find(models.socketIORequest.type, { parentId: { $in: listOfParentIds } }),
|
||||
]);
|
||||
|
||||
const allRequests = [...reqs, ...allRequestGroups, ...grpcReqs, ...wsReqs, ...socketIOReqs] as AllRequestDoc[];
|
||||
|
||||
const [requestMetas, grpcRequestMetas, requestGroupMetas, wsRequestMetas, socketIORequestMetas] = await Promise.all([
|
||||
database.find<RequestMeta>(models.requestMeta.type, { parentId: { $in: reqs.map(r => r._id) } }),
|
||||
database.find<GrpcRequestMeta>(models.grpcRequestMeta.type, {
|
||||
parentId: { $in: grpcReqs.map(r => r._id) },
|
||||
}),
|
||||
database.find<RequestGroupMeta>(models.requestGroupMeta.type, {
|
||||
parentId: { $in: allRequestGroups.map(requestGroup => requestGroup._id) },
|
||||
}),
|
||||
database.find<WebSocketRequestMeta>(models.webSocketRequestMeta.type, {
|
||||
parentId: { $in: wsReqs.map(r => r._id) },
|
||||
}),
|
||||
database.find<SocketIORequestMeta>(models.socketIORequestMeta.type, {
|
||||
parentId: { $in: socketIOReqs.map(r => r._id) },
|
||||
}),
|
||||
]);
|
||||
|
||||
const allRequestMetas = [...requestMetas, ...grpcRequestMetas, ...wsRequestMetas, ...socketIORequestMetas];
|
||||
// Associate requests with their workspace IDs and group request metas by workspace ID
|
||||
allRequests.forEach(request => {
|
||||
const { parentId, _id: requestId } = request;
|
||||
const workspaceId = requestGroupToWorkspaceId.get(parentId);
|
||||
if (workspaceId) {
|
||||
// Track which workspace this request belongs to
|
||||
if (models.grpcRequest.isGrpcRequest(request)) {
|
||||
grpcRequestToWorkspaceId.set(requestId, workspaceId);
|
||||
} else if (models.request.isRequest(request)) {
|
||||
requestToWorkspaceId.set(requestId, workspaceId);
|
||||
} else if (models.webSocketRequest.isWebSocketRequest(request)) {
|
||||
wsRequestToWorkspaceId.set(requestId, workspaceId);
|
||||
} else if (models.socketIORequest.isSocketIORequest(request)) {
|
||||
socketIORequestToWorkspaceId.set(requestId, workspaceId);
|
||||
}
|
||||
const workspaceData = allRequestsAndMetaByWorkspaceId.get(workspaceId);
|
||||
if (workspaceData) {
|
||||
workspaceData.allRequests.push(request);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Build map of requestGroupMetas by workspace ID
|
||||
requestGroupMetas.forEach(requestGroupMeta => {
|
||||
const workspaceId = requestGroupToWorkspaceId.get(requestGroupMeta.parentId);
|
||||
if (workspaceId) {
|
||||
const workspaceData = allRequestsAndMetaByWorkspaceId.get(workspaceId);
|
||||
if (workspaceData) {
|
||||
workspaceData.requestGroupMetas.push(requestGroupMeta);
|
||||
}
|
||||
}
|
||||
});
|
||||
allRequestMetas.forEach(requestMeta => {
|
||||
const requestOrGrpcRequestId = requestMeta.parentId;
|
||||
let workspaceId: string | undefined;
|
||||
if (models.request.isRequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = requestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
} else if (models.grpcRequest.isGrpcRequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = grpcRequestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
} else if (models.webSocketRequest.isWebSocketRequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = wsRequestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
} else if (models.socketIORequest.isSocketIORequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = socketIORequestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
}
|
||||
if (workspaceId) {
|
||||
const workspaceData = allRequestsAndMetaByWorkspaceId.get(workspaceId);
|
||||
if (workspaceData) {
|
||||
workspaceData.allRequestMetas.push(requestMeta);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return allRequestsAndMetaByWorkspaceId;
|
||||
}
|
||||
|
||||
export const getRequestsAndMetaByWorkspaceId = async (workspaceId: string): Promise<AllRequestsAndMetaInWorkspace> => {
|
||||
const result = await getAllRequestsAndMetaByWorkspace([workspaceId]);
|
||||
return result.get(workspaceId) ?? EMPTY_REQUESTS_AND_META;
|
||||
};
|
||||
|
||||
let pendingWorkspaceIds: string[] = [];
|
||||
let pendingBatch: Promise<Map<string, AllRequestsAndMetaInWorkspace>> | null = null;
|
||||
|
||||
// Combine all per-workspace reads requested within the same microtask into a single batched DB sweep
|
||||
const loadRequestsAndMetaBatched = (workspaceId: string): Promise<AllRequestsAndMetaInWorkspace> => {
|
||||
pendingWorkspaceIds.push(workspaceId);
|
||||
if (!pendingBatch) {
|
||||
pendingBatch = Promise.resolve().then(() => {
|
||||
const ids = [...pendingWorkspaceIds];
|
||||
console.log(`Batching requests/meta load for workspaces: ${ids.join(', ')}`);
|
||||
pendingWorkspaceIds = [];
|
||||
pendingBatch = null;
|
||||
return getAllRequestsAndMetaByWorkspace(ids);
|
||||
});
|
||||
}
|
||||
return pendingBatch.then(map => map.get(workspaceId) ?? EMPTY_REQUESTS_AND_META);
|
||||
};
|
||||
|
||||
// A single workspace's collection children (requests/requestGroups) and their metas.
|
||||
export const useRequestsAndMeta = (workspaceId: string): UseQueryResult<AllRequestsAndMetaInWorkspace> => {
|
||||
return useQuery({
|
||||
queryKey: requestsAndMetaKeys.detail(workspaceId),
|
||||
queryFn: () => loadRequestsAndMetaBatched(workspaceId),
|
||||
});
|
||||
};
|
||||
|
||||
export const useRequestsAndMetaByWorkspaceIds = (workspaceIds: string[]) =>
|
||||
useQueries({
|
||||
queries: workspaceIds.map(workspaceId => ({
|
||||
queryKey: requestsAndMetaKeys.detail(workspaceId),
|
||||
queryFn: () => loadRequestsAndMetaBatched(workspaceId),
|
||||
})),
|
||||
combine: useCallback(
|
||||
(results: UseQueryResult<AllRequestsAndMetaInWorkspace>[]) => {
|
||||
const dataByWorkspaceId = new Map<string, AllRequestsAndMetaInWorkspace>();
|
||||
workspaceIds.forEach((workspaceId, index) => {
|
||||
const data = results[index]?.data;
|
||||
if (data) {
|
||||
dataByWorkspaceId.set(workspaceId, data);
|
||||
}
|
||||
});
|
||||
return dataByWorkspaceId;
|
||||
},
|
||||
[workspaceIds],
|
||||
),
|
||||
});
|
||||
491
packages/insomnia/src/ui/hooks/data/workspace-children.ts
Normal file
491
packages/insomnia/src/ui/hooks/data/workspace-children.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
import type { QueryClient } from '@tanstack/react-query';
|
||||
import { useQueries, useQuery, type UseQueryResult } from '@tanstack/react-query';
|
||||
import type {
|
||||
ApiSpec,
|
||||
BaseModel,
|
||||
Environment,
|
||||
GrpcRequest,
|
||||
GrpcRequestMeta,
|
||||
McpRequest,
|
||||
MockServer,
|
||||
Request,
|
||||
RequestGroup,
|
||||
RequestGroupMeta,
|
||||
RequestMeta,
|
||||
SocketIORequest,
|
||||
SocketIORequestMeta,
|
||||
WebSocketRequest,
|
||||
WebSocketRequestMeta,
|
||||
Workspace,
|
||||
WorkspaceScope,
|
||||
} from 'insomnia-data';
|
||||
import { models } from 'insomnia-data';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { database } from '~/common/database';
|
||||
|
||||
type CollectionChildDoc = Request | GrpcRequest | WebSocketRequest | SocketIORequest | RequestGroup;
|
||||
export type { CollectionChildDoc };
|
||||
|
||||
type CollectionRequestMeta = RequestMeta | GrpcRequestMeta | WebSocketRequestMeta | SocketIORequestMeta;
|
||||
|
||||
interface CommonWorkspaceChildren<TChildren, TChildrenMeta extends object = {}> {
|
||||
children: TChildren;
|
||||
childrenMetas: TChildrenMeta;
|
||||
}
|
||||
export interface CollectionWorkspaceChildren
|
||||
extends CommonWorkspaceChildren<
|
||||
{ requestsAndGroups: CollectionChildDoc[] },
|
||||
{
|
||||
allRequestMetas: CollectionRequestMeta[];
|
||||
requestGroupMetas: RequestGroupMeta[];
|
||||
}
|
||||
> {}
|
||||
|
||||
export interface MockServerWorkspaceChildren extends CommonWorkspaceChildren<{ mockServer?: MockServer }> {}
|
||||
|
||||
export interface DesignWorkspaceChildren extends CommonWorkspaceChildren<{ apiSpec?: ApiSpec }> {}
|
||||
|
||||
export interface EnvironmentWorkspaceChildren extends CommonWorkspaceChildren<{ baseEnvironment?: Environment }> {}
|
||||
|
||||
export interface McpWorkspaceChildren extends CommonWorkspaceChildren<{ mcpRequest?: McpRequest }> {}
|
||||
|
||||
export type WorkspaceChildren =
|
||||
| CollectionWorkspaceChildren
|
||||
| MockServerWorkspaceChildren
|
||||
| DesignWorkspaceChildren
|
||||
| EnvironmentWorkspaceChildren
|
||||
| McpWorkspaceChildren;
|
||||
|
||||
const emptyWorkspaceChildren: WorkspaceChildren = {
|
||||
children: {},
|
||||
childrenMetas: {},
|
||||
};
|
||||
|
||||
export const workspaceChildrenKeys = {
|
||||
all: ['workspaceChildrenAndMetas'],
|
||||
details: (workspaceId: string) => [...workspaceChildrenKeys.all, workspaceId],
|
||||
};
|
||||
|
||||
// Walk the given collection and group every request, request and request-group meta under it.
|
||||
async function getAllCollectionChildrenAndMetasByWorkspaceIds(
|
||||
workspaceIds: string[],
|
||||
): Promise<Map<string, CollectionWorkspaceChildren>> {
|
||||
const allRequestsAndMetaByWorkspaceId = new Map<string, CollectionWorkspaceChildren>();
|
||||
if (workspaceIds.length === 0) {
|
||||
return allRequestsAndMetaByWorkspaceId;
|
||||
}
|
||||
let requestGroupIdQueue = [...workspaceIds];
|
||||
const allRequestGroups: RequestGroup[] = [];
|
||||
// Map to track which workspace each request group belongs to
|
||||
const requestGroupToWorkspaceId = new Map<string, string>();
|
||||
const requestToWorkspaceId = new Map<string, string>();
|
||||
const grpcRequestToWorkspaceId = new Map<string, string>();
|
||||
const wsRequestToWorkspaceId = new Map<string, string>();
|
||||
const socketIORequestToWorkspaceId = new Map<string, string>();
|
||||
// Initialize the map with workspace IDs
|
||||
workspaceIds.forEach(workspaceId => {
|
||||
requestGroupToWorkspaceId.set(workspaceId, workspaceId);
|
||||
requestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
grpcRequestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
wsRequestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
socketIORequestToWorkspaceId.set(workspaceId, workspaceId);
|
||||
allRequestsAndMetaByWorkspaceId.set(workspaceId, {
|
||||
children: {
|
||||
requestsAndGroups: [],
|
||||
},
|
||||
childrenMetas: { allRequestMetas: [], requestGroupMetas: [] },
|
||||
});
|
||||
});
|
||||
|
||||
while (requestGroupIdQueue.length) {
|
||||
const requestGroups = await database.find<RequestGroup>(models.requestGroup.type, {
|
||||
parentId: { $in: requestGroupIdQueue },
|
||||
});
|
||||
|
||||
if (requestGroups.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
requestGroups.forEach(requestGroup => {
|
||||
const workspaceId = requestGroupToWorkspaceId.get(requestGroup.parentId);
|
||||
if (workspaceId) {
|
||||
requestGroupToWorkspaceId.set(requestGroup._id, workspaceId);
|
||||
}
|
||||
});
|
||||
|
||||
allRequestGroups.push(...requestGroups);
|
||||
requestGroupIdQueue = requestGroups.map(rg => rg._id);
|
||||
}
|
||||
|
||||
const listOfParentIds = [...workspaceIds, ...allRequestGroups.map(requestGroup => requestGroup._id)];
|
||||
|
||||
const [reqs, grpcReqs, wsReqs, socketIOReqs] = await Promise.all([
|
||||
database.find(models.request.type, { parentId: { $in: listOfParentIds } }),
|
||||
database.find<GrpcRequest>(models.grpcRequest.type, { parentId: { $in: listOfParentIds } }),
|
||||
database.find(models.webSocketRequest.type, { parentId: { $in: listOfParentIds } }),
|
||||
database.find(models.socketIORequest.type, { parentId: { $in: listOfParentIds } }),
|
||||
]);
|
||||
|
||||
const allRequests = [...reqs, ...allRequestGroups, ...grpcReqs, ...wsReqs, ...socketIOReqs] as CollectionChildDoc[];
|
||||
|
||||
const [requestMetas, grpcRequestMetas, requestGroupMetas, wsRequestMetas, socketIORequestMetas] = await Promise.all([
|
||||
database.find<RequestMeta>(models.requestMeta.type, { parentId: { $in: reqs.map(r => r._id) } }),
|
||||
database.find<GrpcRequestMeta>(models.grpcRequestMeta.type, {
|
||||
parentId: { $in: grpcReqs.map(r => r._id) },
|
||||
}),
|
||||
database.find<RequestGroupMeta>(models.requestGroupMeta.type, {
|
||||
parentId: { $in: allRequestGroups.map(requestGroup => requestGroup._id) },
|
||||
}),
|
||||
database.find<WebSocketRequestMeta>(models.webSocketRequestMeta.type, {
|
||||
parentId: { $in: wsReqs.map(r => r._id) },
|
||||
}),
|
||||
database.find<SocketIORequestMeta>(models.socketIORequestMeta.type, {
|
||||
parentId: { $in: socketIOReqs.map(r => r._id) },
|
||||
}),
|
||||
]);
|
||||
|
||||
const allRequestMetas = [...requestMetas, ...grpcRequestMetas, ...wsRequestMetas, ...socketIORequestMetas];
|
||||
// Associate requests with their workspace IDs and group request metas by workspace ID
|
||||
allRequests.forEach(request => {
|
||||
const { parentId, _id: requestId } = request;
|
||||
const workspaceId = requestGroupToWorkspaceId.get(parentId);
|
||||
if (workspaceId) {
|
||||
// Track which workspace this request belongs to
|
||||
if (models.grpcRequest.isGrpcRequest(request)) {
|
||||
grpcRequestToWorkspaceId.set(requestId, workspaceId);
|
||||
} else if (models.request.isRequest(request)) {
|
||||
requestToWorkspaceId.set(requestId, workspaceId);
|
||||
} else if (models.webSocketRequest.isWebSocketRequest(request)) {
|
||||
wsRequestToWorkspaceId.set(requestId, workspaceId);
|
||||
} else if (models.socketIORequest.isSocketIORequest(request)) {
|
||||
socketIORequestToWorkspaceId.set(requestId, workspaceId);
|
||||
}
|
||||
const workspaceData = allRequestsAndMetaByWorkspaceId.get(workspaceId);
|
||||
if (workspaceData) {
|
||||
workspaceData.children.requestsAndGroups.push(request);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Build map of requestGroupMetas by workspace ID
|
||||
requestGroupMetas.forEach(requestGroupMeta => {
|
||||
const workspaceId = requestGroupToWorkspaceId.get(requestGroupMeta.parentId);
|
||||
if (workspaceId) {
|
||||
const workspaceData = allRequestsAndMetaByWorkspaceId.get(workspaceId);
|
||||
if (workspaceData) {
|
||||
workspaceData.childrenMetas.requestGroupMetas.push(requestGroupMeta);
|
||||
}
|
||||
}
|
||||
});
|
||||
allRequestMetas.forEach(requestMeta => {
|
||||
const requestOrGrpcRequestId = requestMeta.parentId;
|
||||
let workspaceId: string | undefined;
|
||||
if (models.request.isRequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = requestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
} else if (models.grpcRequest.isGrpcRequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = grpcRequestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
} else if (models.webSocketRequest.isWebSocketRequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = wsRequestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
} else if (models.socketIORequest.isSocketIORequestId(requestOrGrpcRequestId)) {
|
||||
workspaceId = socketIORequestToWorkspaceId.get(requestOrGrpcRequestId);
|
||||
}
|
||||
if (workspaceId) {
|
||||
const workspaceData = allRequestsAndMetaByWorkspaceId.get(workspaceId);
|
||||
if (workspaceData) {
|
||||
workspaceData.childrenMetas.allRequestMetas.push(requestMeta);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return allRequestsAndMetaByWorkspaceId;
|
||||
}
|
||||
|
||||
const getAllMockServerChildrenByWorkspaceIds = async (workspaceIds: string[]) => {
|
||||
const map = new Map<string, MockServerWorkspaceChildren>();
|
||||
if (workspaceIds.length > 0) {
|
||||
const mockServers = await database.find<MockServer>(models.mockServer.type, {
|
||||
parentId: { $in: workspaceIds },
|
||||
});
|
||||
workspaceIds.forEach(workspaceId => {
|
||||
map.set(workspaceId, {
|
||||
children: {
|
||||
mockServer: mockServers.find(mockServer => mockServer.parentId === workspaceId),
|
||||
},
|
||||
childrenMetas: {},
|
||||
});
|
||||
});
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
const getAllDesignChildrenByWorkspaceIds = async (workspaceIds: string[]) => {
|
||||
const map = new Map<string, DesignWorkspaceChildren>();
|
||||
if (workspaceIds.length > 0) {
|
||||
const apiSpecs = await database.find<ApiSpec>(models.apiSpec.type, {
|
||||
parentId: { $in: workspaceIds },
|
||||
});
|
||||
workspaceIds.forEach(workspaceId => {
|
||||
map.set(workspaceId, {
|
||||
children: {
|
||||
apiSpec: apiSpecs.find(apiSpec => apiSpec.parentId === workspaceId),
|
||||
},
|
||||
childrenMetas: {},
|
||||
});
|
||||
});
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
const getAllEnvironmentChildrenByWorkspaceIds = async (workspaceIds: string[]) => {
|
||||
const map = new Map<string, EnvironmentWorkspaceChildren>();
|
||||
if (workspaceIds.length > 0) {
|
||||
const environments = await database.find<Environment>(models.environment.type, {
|
||||
parentId: { $in: workspaceIds },
|
||||
});
|
||||
workspaceIds.forEach(workspaceId => {
|
||||
map.set(workspaceId, {
|
||||
children: {
|
||||
baseEnvironment: environments.find(environment => environment.parentId === workspaceId),
|
||||
},
|
||||
childrenMetas: {},
|
||||
});
|
||||
});
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
const getAllMcpChildrenByWorkspaceIds = async (workspaceIds: string[]) => {
|
||||
const map = new Map<string, McpWorkspaceChildren>();
|
||||
if (workspaceIds.length > 0) {
|
||||
const mcpRequests = await database.find<McpRequest>(models.mcpRequest.type, {
|
||||
parentId: { $in: workspaceIds },
|
||||
});
|
||||
workspaceIds.forEach(workspaceId => {
|
||||
map.set(workspaceId, {
|
||||
children: {
|
||||
mcpRequest: mcpRequests.find(mcpRequest => mcpRequest.parentId === workspaceId),
|
||||
},
|
||||
childrenMetas: {},
|
||||
});
|
||||
});
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
export const findWorkspaceIdsForChangedCollectionChildDoc = (queryClient: QueryClient, doc: BaseModel): string[] => {
|
||||
const workspaceIds: string[] = [];
|
||||
const cachedWorkspaces = queryClient.getQueriesData<WorkspaceChildren>({
|
||||
queryKey: workspaceChildrenKeys.all,
|
||||
});
|
||||
for (const [queryKey, data] of cachedWorkspaces) {
|
||||
const workspaceId = queryKey[1] as string;
|
||||
if (!data || !data.children || !('requestsAndGroups' in data.children)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
doc.parentId === workspaceId ||
|
||||
data.children.requestsAndGroups.some(r => r._id === doc._id || r._id === doc.parentId)
|
||||
) {
|
||||
workspaceIds.push(workspaceId);
|
||||
}
|
||||
}
|
||||
return workspaceIds;
|
||||
};
|
||||
|
||||
export function updateCollectionChildrenWithUpdatedDoc(
|
||||
collectionChildren: CollectionWorkspaceChildren,
|
||||
doc: BaseModel,
|
||||
): CollectionWorkspaceChildren | null {
|
||||
// ALL request doc types
|
||||
const REQUEST_DOC_TYPES: string[] = [
|
||||
models.request.type,
|
||||
models.grpcRequest.type,
|
||||
models.webSocketRequest.type,
|
||||
models.socketIORequest.type,
|
||||
models.requestGroup.type,
|
||||
];
|
||||
// All request meta doc types
|
||||
const REQUEST_META_DOC_TYPES: string[] = [
|
||||
models.requestMeta.type,
|
||||
models.grpcRequestMeta.type,
|
||||
models.webSocketRequestMeta.type,
|
||||
models.socketIORequestMeta.type,
|
||||
];
|
||||
|
||||
const replaceById = <T extends BaseModel>(list: T[]): T[] | null => {
|
||||
const index = list.findIndex(item => item._id === doc._id);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
// The item has been moved to another parent, consider failure to update
|
||||
if (list[index].parentId !== doc.parentId) {
|
||||
return null;
|
||||
}
|
||||
const next = list.slice();
|
||||
next[index] = doc as unknown as T;
|
||||
return next;
|
||||
};
|
||||
|
||||
if (REQUEST_DOC_TYPES.includes(doc.type)) {
|
||||
const requestsAndGroups = replaceById(collectionChildren.children.requestsAndGroups);
|
||||
return requestsAndGroups
|
||||
? {
|
||||
...collectionChildren,
|
||||
children: {
|
||||
requestsAndGroups,
|
||||
},
|
||||
}
|
||||
: null;
|
||||
}
|
||||
if (REQUEST_META_DOC_TYPES.includes(doc.type)) {
|
||||
const allRequestMetas = replaceById(collectionChildren.childrenMetas.allRequestMetas);
|
||||
return allRequestMetas
|
||||
? { ...collectionChildren, childrenMetas: { ...collectionChildren.childrenMetas, allRequestMetas } }
|
||||
: null;
|
||||
}
|
||||
if (doc.type === models.requestGroupMeta.type) {
|
||||
const requestGroupMetas = replaceById(collectionChildren.childrenMetas.requestGroupMetas);
|
||||
return requestGroupMetas
|
||||
? { ...collectionChildren, childrenMetas: { ...collectionChildren.childrenMetas, requestGroupMetas } }
|
||||
: null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const getWorkspaceChildren = async (
|
||||
workspaceIds: string[],
|
||||
scope?: WorkspaceScope,
|
||||
): Promise<Map<string, WorkspaceChildren>> => {
|
||||
if (scope) {
|
||||
switch (scope) {
|
||||
case 'design': {
|
||||
const designChildrenByWorkspaceIds = await getAllDesignChildrenByWorkspaceIds(workspaceIds);
|
||||
return designChildrenByWorkspaceIds;
|
||||
}
|
||||
case 'collection': {
|
||||
const collectionChildrenByWorkspaceIds = await getAllCollectionChildrenAndMetasByWorkspaceIds(workspaceIds);
|
||||
return collectionChildrenByWorkspaceIds;
|
||||
}
|
||||
case 'mock-server': {
|
||||
const mockServerChildrenByWorkspaceIds = await getAllMockServerChildrenByWorkspaceIds(workspaceIds);
|
||||
return mockServerChildrenByWorkspaceIds;
|
||||
}
|
||||
case 'environment': {
|
||||
const environmentChildrenByWorkspaceIds = await getAllEnvironmentChildrenByWorkspaceIds(workspaceIds);
|
||||
return environmentChildrenByWorkspaceIds;
|
||||
}
|
||||
case 'mcp': {
|
||||
const mcpChildrenByWorkspaceIds = await getAllMcpChildrenByWorkspaceIds(workspaceIds);
|
||||
return mcpChildrenByWorkspaceIds;
|
||||
}
|
||||
default: {
|
||||
console.warn(`Unsupported workspace scope: ${scope}`);
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
}
|
||||
const workspaces = await database.find<Workspace>(models.workspace.type, {
|
||||
_id: { $in: workspaceIds },
|
||||
});
|
||||
if (workspaces.length > 0) {
|
||||
const workspaceIdToScopeMap = new Map(workspaces.map(workspace => [workspace._id, workspace.scope]));
|
||||
const collectionWorkspaceIds = workspaces.filter(w => w.scope === 'collection').map(w => w._id);
|
||||
const mockServerWorkspaceIds = workspaces.filter(w => w.scope === 'mock-server').map(w => w._id);
|
||||
const designWorkspaceIds = workspaces.filter(w => w.scope === 'design').map(w => w._id);
|
||||
const environmentWorkspaceIds = workspaces.filter(w => w.scope === 'environment').map(w => w._id);
|
||||
const mcpWorkspaceIds = workspaces.filter(w => w.scope === 'mcp').map(w => w._id);
|
||||
const collectionChildrenPromise = getAllCollectionChildrenAndMetasByWorkspaceIds(collectionWorkspaceIds);
|
||||
const mockServerChildrenPromise = getAllMockServerChildrenByWorkspaceIds(mockServerWorkspaceIds);
|
||||
const designChildrenPromise = getAllDesignChildrenByWorkspaceIds(designWorkspaceIds);
|
||||
const environmentChildrenPromise = getAllEnvironmentChildrenByWorkspaceIds(environmentWorkspaceIds);
|
||||
const mcpChildrenPromise = getAllMcpChildrenByWorkspaceIds(mcpWorkspaceIds);
|
||||
const [
|
||||
collectionChildrenByWorkspaceIds,
|
||||
mockServerChildrenByWorkspaceIds,
|
||||
designChildrenByWorkspaceIds,
|
||||
environmentChildrenByWorkspaceIds,
|
||||
mcpChildrenByWorkspaceIds,
|
||||
] = await Promise.all([
|
||||
collectionChildrenPromise,
|
||||
mockServerChildrenPromise,
|
||||
designChildrenPromise,
|
||||
environmentChildrenPromise,
|
||||
mcpChildrenPromise,
|
||||
]);
|
||||
const map = new Map<string, WorkspaceChildren>();
|
||||
workspaceIds.forEach(workspaceId => {
|
||||
const scope = workspaceIdToScopeMap.get(workspaceId);
|
||||
if (scope === 'collection') {
|
||||
map.set(workspaceId, collectionChildrenByWorkspaceIds.get(workspaceId) ?? emptyWorkspaceChildren);
|
||||
} else if (scope === 'mock-server') {
|
||||
map.set(workspaceId, mockServerChildrenByWorkspaceIds.get(workspaceId) ?? emptyWorkspaceChildren);
|
||||
} else if (scope === 'design') {
|
||||
map.set(workspaceId, designChildrenByWorkspaceIds.get(workspaceId) ?? emptyWorkspaceChildren);
|
||||
} else if (scope === 'environment') {
|
||||
map.set(workspaceId, environmentChildrenByWorkspaceIds.get(workspaceId) ?? emptyWorkspaceChildren);
|
||||
} else if (scope === 'mcp') {
|
||||
map.set(workspaceId, mcpChildrenByWorkspaceIds.get(workspaceId) ?? emptyWorkspaceChildren);
|
||||
} else {
|
||||
console.warn(`Unsupported workspace scope: ${scope} for workspace ${workspaceId}`);
|
||||
map.set(workspaceId, emptyWorkspaceChildren);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
return new Map();
|
||||
};
|
||||
|
||||
// Pending batches keyed by scope. Reads must only batch together when they share a scope: the batch runs a
|
||||
// single `getWorkspaceChildren(ids, scope)` sweep, so mixing scopes (e.g. the sidebar's 'collection' with
|
||||
// the project index's 'design'/'mock-server') would apply one caller's scope to every workspace in the batch.
|
||||
const SCOPELESS_BATCH_KEY = '__all__';
|
||||
const pendingBatches = new Map<string, { ids: string[]; batch: Promise<Map<string, WorkspaceChildren>> }>();
|
||||
|
||||
//Combine all per-workspace reads of the same scope requested within the same microtask into a single batched DB sweep
|
||||
const loadWorkspaceChildrenBatched = (workspaceId: string, scope?: WorkspaceScope): Promise<WorkspaceChildren> => {
|
||||
const scopeKey = scope ?? SCOPELESS_BATCH_KEY;
|
||||
let pending = pendingBatches.get(scopeKey);
|
||||
if (!pending) {
|
||||
const ids: string[] = [];
|
||||
const batch = Promise.resolve().then(() => {
|
||||
pendingBatches.delete(scopeKey);
|
||||
console.log(`Batching requests/meta load for workspaces (${scopeKey}): ${ids.join(', ')}`);
|
||||
return getWorkspaceChildren(ids, scope);
|
||||
});
|
||||
pending = { ids, batch };
|
||||
pendingBatches.set(scopeKey, pending);
|
||||
}
|
||||
pending.ids.push(workspaceId);
|
||||
return pending.batch.then(map => map.get(workspaceId) ?? emptyWorkspaceChildren);
|
||||
};
|
||||
|
||||
export const useWorkspaceChildren = (
|
||||
workspaceId: string,
|
||||
scope?: WorkspaceScope,
|
||||
): UseQueryResult<WorkspaceChildren> => {
|
||||
return useQuery({
|
||||
queryKey: workspaceChildrenKeys.details(workspaceId),
|
||||
queryFn: () => loadWorkspaceChildrenBatched(workspaceId, scope),
|
||||
});
|
||||
};
|
||||
|
||||
export const useWorkspaceChildrenByWorkspaceIds = (workspaceIds: string[], scope?: WorkspaceScope) =>
|
||||
useQueries({
|
||||
queries: workspaceIds.map(workspaceId => ({
|
||||
queryKey: workspaceChildrenKeys.details(workspaceId),
|
||||
queryFn: () => loadWorkspaceChildrenBatched(workspaceId, scope),
|
||||
})),
|
||||
combine: useCallback(
|
||||
(results: UseQueryResult<WorkspaceChildren>[]) => {
|
||||
const dataByWorkspaceId = new Map<string, WorkspaceChildren>();
|
||||
workspaceIds.forEach((workspaceId, index) => {
|
||||
const data = results[index]?.data;
|
||||
if (data) {
|
||||
dataByWorkspaceId.set(workspaceId, data);
|
||||
}
|
||||
});
|
||||
return dataByWorkspaceId;
|
||||
},
|
||||
[workspaceIds],
|
||||
),
|
||||
});
|
||||
125
packages/insomnia/src/ui/hooks/data/workspaces.ts
Normal file
125
packages/insomnia/src/ui/hooks/data/workspaces.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useQueries, useQuery, type UseQueryResult } from '@tanstack/react-query';
|
||||
import type { Workspace, WorkspaceMeta } from 'insomnia-data';
|
||||
import { models, services } from 'insomnia-data';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { parseApiSpec, type ParsedApiSpec } from '~/common/api-specs';
|
||||
import { scopeToLabelMap } from '~/common/get-workspace-label';
|
||||
import { isNotNullOrUndefined } from '~/common/misc';
|
||||
import { type InsomniaFile } from '~/common/project';
|
||||
import { descendingNumberSort } from '~/common/sorting';
|
||||
|
||||
import {
|
||||
type DesignWorkspaceChildren,
|
||||
type MockServerWorkspaceChildren,
|
||||
useWorkspaceChildrenByWorkspaceIds,
|
||||
} from './workspace-children';
|
||||
|
||||
export const workspaceKeys = {
|
||||
all: ['workspaces'],
|
||||
// all workspace details under a project
|
||||
details: (projectId: string) => [...workspaceKeys.all, 'details', projectId],
|
||||
// a single workspace detail, scoped under its parent project
|
||||
detail: (workspaceId: string, projectId: string) => [...workspaceKeys.details(projectId), workspaceId],
|
||||
};
|
||||
|
||||
export const getWorkspacesByProjectId = (projectId: string): Promise<Workspace[]> =>
|
||||
services.workspace.findByParentId(projectId);
|
||||
|
||||
export const getWorkspaceById = async (workspaceId: string): Promise<Workspace | undefined> =>
|
||||
await services.workspace.getById(workspaceId);
|
||||
|
||||
// All workspace details under a project.
|
||||
export const useWorkspaces = (projectId: string): UseQueryResult<Workspace[]> =>
|
||||
useQuery({
|
||||
queryKey: workspaceKeys.details(projectId),
|
||||
queryFn: () => getWorkspacesByProjectId(projectId),
|
||||
});
|
||||
export const useWorkspace = (workspaceId: string, projectId: string): UseQueryResult<Workspace | undefined> =>
|
||||
useQuery({
|
||||
queryKey: workspaceKeys.detail(projectId, workspaceId),
|
||||
queryFn: () => getWorkspaceById(workspaceId),
|
||||
});
|
||||
export const useWorkspacesByProjectIds = (projectIds: string[]) =>
|
||||
useQueries({
|
||||
queries: projectIds.map(projectId => ({
|
||||
queryKey: workspaceKeys.details(projectId),
|
||||
queryFn: async () => await getWorkspacesByProjectId(projectId),
|
||||
})),
|
||||
combine: useCallback(
|
||||
(results: UseQueryResult<Workspace[]>[]) => {
|
||||
const map = new Map<string, Workspace[]>();
|
||||
projectIds.forEach((projectId, index) => map.set(projectId, results[index]?.data ?? []));
|
||||
return map;
|
||||
},
|
||||
[projectIds],
|
||||
),
|
||||
});
|
||||
|
||||
export const useLocalFiles = (projectId: string, workspaceMetas: WorkspaceMeta[]): InsomniaFile[] => {
|
||||
const workspacesByProjectId = useWorkspacesByProjectIds([projectId]);
|
||||
const workspaces = workspacesByProjectId.get(projectId) ?? [];
|
||||
const designWorkspaceIds = workspaces
|
||||
.filter(workspace => workspace.scope === 'design')
|
||||
.map(workspace => workspace._id);
|
||||
const mockServerWorkspaceIds = workspaces
|
||||
.filter(workspace => workspace.scope === 'mock-server')
|
||||
.map(workspace => workspace._id);
|
||||
|
||||
const designChildrenByWorkspaceId = useWorkspaceChildrenByWorkspaceIds(designWorkspaceIds, 'design');
|
||||
const mockServerChildrenByWorkspaceId = useWorkspaceChildrenByWorkspaceIds(mockServerWorkspaceIds, 'mock-server');
|
||||
|
||||
return workspaces.map(workspace => {
|
||||
const apiSpec = (designChildrenByWorkspaceId.get(workspace._id) as DesignWorkspaceChildren)?.children.apiSpec;
|
||||
const mockServer = (mockServerChildrenByWorkspaceId.get(workspace._id) as MockServerWorkspaceChildren)?.children
|
||||
.mockServer;
|
||||
|
||||
let spec: ParsedApiSpec['contents'] = null;
|
||||
let specFormat: ParsedApiSpec['format'] = null;
|
||||
let specFormatVersion: ParsedApiSpec['formatVersion'] = null;
|
||||
if (apiSpec) {
|
||||
try {
|
||||
const result = parseApiSpec(apiSpec.contents);
|
||||
spec = result.contents;
|
||||
specFormat = result.format;
|
||||
specFormatVersion = result.formatVersion;
|
||||
} catch {
|
||||
// Assume there is no spec
|
||||
// TODO: Check for parse errors if it's an invalid spec
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceMeta = workspaceMetas?.find(wm => wm.parentId === workspace._id);
|
||||
|
||||
// WorkspaceMeta is a good proxy for last modified time
|
||||
const workspaceModified = workspaceMeta?.modified || workspace.modified;
|
||||
|
||||
const modifiedLocally = models.workspace.isDesign(workspace) ? apiSpec?.modified || 0 : workspaceModified;
|
||||
|
||||
// Span spec, workspace and sync related timestamps for card last modified label and sort order
|
||||
const lastModifiedFrom = [workspace?.modified, workspaceMeta?.modified, modifiedLocally];
|
||||
|
||||
const lastModifiedTimestamp = lastModifiedFrom.filter(isNotNullOrUndefined).sort(descendingNumberSort)[0];
|
||||
|
||||
const specVersion = spec?.info?.version ? String(spec?.info?.version) : '';
|
||||
|
||||
return {
|
||||
id: workspace._id,
|
||||
name: workspace.name,
|
||||
scope: workspace.scope,
|
||||
label: scopeToLabelMap[workspace.scope],
|
||||
created: workspace.created,
|
||||
lastModifiedTimestamp: lastModifiedTimestamp,
|
||||
branch: '',
|
||||
lastCommit: '',
|
||||
version: specVersion ? `${specVersion?.startsWith('v') ? '' : 'v'}${specVersion}` : '',
|
||||
oasFormat: specFormat ? `${specFormat === 'openapi' ? 'OpenAPI' : 'Swagger'} ${specFormatVersion || ''}` : '',
|
||||
mockServer,
|
||||
apiSpec,
|
||||
workspace,
|
||||
hasUncommittedChanges: workspaceMeta?.hasUncommittedChanges,
|
||||
hasUnpushedChanges: workspaceMeta?.hasUnpushedChanges,
|
||||
gitFilePath: workspaceMeta?.gitFilePath,
|
||||
};
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user