mirror of
https://github.com/Kong/insomnia.git
synced 2026-07-30 09:16:44 -04:00
fix: guard against infinite loops (#10281)
This commit is contained in:
@@ -1,9 +1,39 @@
|
||||
import { services } from 'insomnia-data';
|
||||
import { services, type Workspace } from 'insomnia-data';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { database as db } from '~/common/database';
|
||||
|
||||
import { checkAllProjectSyncStatus } from './project';
|
||||
import { checkAllProjectSyncStatus, getUnsyncedRemoteWorkspaces, type InsomniaFile } from './project';
|
||||
|
||||
const mkRemoteFile = (id: string): InsomniaFile => ({
|
||||
id,
|
||||
name: `${id}-name`,
|
||||
scope: 'unsynced',
|
||||
label: 'Unsynced',
|
||||
created: 0,
|
||||
lastModifiedTimestamp: 0,
|
||||
});
|
||||
|
||||
const mkWorkspace = (id: string): Workspace => ({ _id: id, scope: 'collection' }) as unknown as Workspace;
|
||||
|
||||
describe('getUnsyncedRemoteWorkspaces', () => {
|
||||
it('excludes a remote file once its workspace exists locally', () => {
|
||||
const remoteFiles = [mkRemoteFile('wrk_downloaded'), mkRemoteFile('wrk_pending')];
|
||||
const workspaces = [mkWorkspace('wrk_downloaded')];
|
||||
|
||||
const result = getUnsyncedRemoteWorkspaces(remoteFiles, workspaces);
|
||||
|
||||
expect(result.map(f => f.id)).toEqual(['wrk_pending']);
|
||||
});
|
||||
|
||||
it('de-duplicates remote files sharing an id', () => {
|
||||
const remoteFiles = [mkRemoteFile('wrk_dup'), mkRemoteFile('wrk_dup')];
|
||||
|
||||
const result = getUnsyncedRemoteWorkspaces(remoteFiles, []);
|
||||
|
||||
expect(result.map(f => f.id)).toEqual(['wrk_dup']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkAllProjectSyncStatus', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -209,8 +209,18 @@ export async function getAllLocalFiles({ projectId }: { projectId: string }) {
|
||||
return files;
|
||||
}
|
||||
|
||||
export const getUnsyncedRemoteWorkspaces = (remoteFiles: InsomniaFile[], workspaces: Workspace[]) =>
|
||||
remoteFiles.filter(remoteFile => !workspaces.find(w => w._id === remoteFile.id));
|
||||
export const getUnsyncedRemoteWorkspaces = (remoteFiles: InsomniaFile[], workspaces: Workspace[]) => {
|
||||
const seenIds = new Set<string>();
|
||||
const uniqueRemoteFiles = remoteFiles.filter(file => {
|
||||
if (seenIds.has(file.id)) {
|
||||
return false;
|
||||
}
|
||||
seenIds.add(file.id);
|
||||
return true;
|
||||
});
|
||||
|
||||
return uniqueRemoteFiles.filter(remoteFile => !workspaces.some(w => w._id === remoteFile.id));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all projects for an organization with their associated git repositories
|
||||
|
||||
@@ -18,6 +18,7 @@ import { fuzzyMatchAll } from 'insomnia-data/common';
|
||||
import { database } from '~/common/database';
|
||||
import { sortMethodMap } from '~/common/sorting';
|
||||
import type { Child } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
|
||||
import { dedupeCollectionItems } from '~/ui/utils/dedupe-collection-items';
|
||||
|
||||
export interface SlimRequestDoc extends BaseModel {
|
||||
type: 'Request' | 'GrpcRequest' | 'WebSocketRequest' | 'SocketIORequest' | 'RequestGroup';
|
||||
@@ -211,14 +212,7 @@ export function flattenCollectionChildren(
|
||||
const { isRequestGroup } = models.requestGroup;
|
||||
const collection: Child[] = [];
|
||||
|
||||
const seenIds = new Set<string>();
|
||||
const uniqueRequests = allRequests.filter(doc => {
|
||||
if (seenIds.has(doc._id)) {
|
||||
return false;
|
||||
}
|
||||
seenIds.add(doc._id);
|
||||
return true;
|
||||
});
|
||||
const uniqueRequests = dedupeCollectionItems(allRequests, doc => doc._id);
|
||||
|
||||
// map of parentId to its direct children requests and request groups
|
||||
const requestsByParentId = new Map<string, AllRequestDoc[]>();
|
||||
|
||||
@@ -57,6 +57,7 @@ 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 { dedupeCollectionItems } from '~/ui/utils/dedupe-collection-items';
|
||||
import { getAllRemoteBackendProjectsOfOrg } from '~/ui/utils/remote-projects';
|
||||
|
||||
import { Icon } from '../../icon';
|
||||
@@ -75,6 +76,9 @@ import { useProjectNavigationSidebarNavigation } from './use-project-navigation-
|
||||
import { useSidebarDragAndDrop } from './use-sidebar-drag-and-drop';
|
||||
import { WorkspaceNode } from './workspace-node';
|
||||
|
||||
const getSidebarGridListItemId = (item: FlatItem): string =>
|
||||
`${item.kind === 'pinnedRequest' ? 'pinned-request-' : ''}${item.doc._id}`;
|
||||
|
||||
interface ProjectNavigationSidebarProps {
|
||||
storageRules: StorageRules;
|
||||
activeNodeId?: string;
|
||||
@@ -354,10 +358,11 @@ const ProjectNavigationSidebarInner = (
|
||||
for (const file of files) {
|
||||
const projectId = remoteIdToProjectIdMap.get(file.teamProjectId);
|
||||
if (projectId) {
|
||||
if (!filesByProjectId.has(projectId)) {
|
||||
filesByProjectId.set(projectId, []);
|
||||
const projectFiles = filesByProjectId.get(projectId) ?? [];
|
||||
if (projectFiles.some(f => f.id === file.rootDocumentId)) {
|
||||
continue;
|
||||
}
|
||||
filesByProjectId.get(projectId)?.push({
|
||||
projectFiles.push({
|
||||
id: file.rootDocumentId,
|
||||
name: file.name,
|
||||
scope: 'unsynced',
|
||||
@@ -366,6 +371,7 @@ const ProjectNavigationSidebarInner = (
|
||||
created: 0,
|
||||
lastModifiedTimestamp: 0,
|
||||
});
|
||||
filesByProjectId.set(projectId, projectFiles);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -996,13 +1002,16 @@ const ProjectNavigationSidebarInner = (
|
||||
const [isShortcutCreateOpen, setIsShortcutCreateOpen] = useState(false);
|
||||
// The item that the shortcut create dropdown is targeting by keyboard up and down arrow keys
|
||||
const [shortcutTargetItemId, setShortcutTargetItemId] = useState<string | null>(null);
|
||||
const visibleFlatItems = useMemo(() => flatItems.filter(i => !i.hidden), [flatItems]);
|
||||
const visibleFlatItems = useMemo(
|
||||
() => dedupeCollectionItems(flatItems.filter(i => !i.hidden), getSidebarGridListItemId),
|
||||
[flatItems],
|
||||
);
|
||||
const virtualizer = useVirtualizer({
|
||||
getScrollElement: () => parentRef.current,
|
||||
count: visibleFlatItems.length,
|
||||
estimateSize: useCallback(() => 32, []),
|
||||
overscan: 30,
|
||||
getItemKey: index => visibleFlatItems[index].doc._id,
|
||||
getItemKey: index => getSidebarGridListItemId(visibleFlatItems[index]),
|
||||
});
|
||||
const sidebarDragAndDropHooks = useSidebarDragAndDrop({
|
||||
flatItems,
|
||||
@@ -1245,9 +1254,8 @@ const ProjectNavigationSidebarInner = (
|
||||
|
||||
return (
|
||||
<GridListItem
|
||||
// Prefix pinned-request to the key and id to ensure pinned items have a different key and id from non-pinned items with the same doc._id
|
||||
key={`${item.kind === 'pinnedRequest' ? 'pinned-request-' : ''}${virtualItem.key}`}
|
||||
id={`${item.kind === 'pinnedRequest' ? 'pinned-request-' : ''}${item.doc._id}`}
|
||||
key={getSidebarGridListItemId(item)}
|
||||
id={getSidebarGridListItemId(item)}
|
||||
textValue={item.doc.name || item.kind}
|
||||
onAuxClick={e => {
|
||||
if (e.button === 1 && item.kind === 'collectionChild') {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { dedupeCollectionItems } from './dedupe-collection-items';
|
||||
|
||||
describe('dedupeCollectionItems', () => {
|
||||
it('keeps the first occurrence of each id and preserves order', () => {
|
||||
const items = [{ id: 'a' }, { id: 'b' }, { id: 'a' }, { id: 'c' }];
|
||||
|
||||
const result = dedupeCollectionItems(items, item => item.id);
|
||||
|
||||
expect(result.map(i => i.id)).toEqual(['a', 'b', 'c']);
|
||||
expect(result[0]).toBe(items[0]);
|
||||
});
|
||||
|
||||
it('returns the same array reference when there are no duplicates', () => {
|
||||
const items = [{ id: 'a' }, { id: 'b' }];
|
||||
|
||||
expect(dedupeCollectionItems(items, item => item.id)).toBe(items);
|
||||
});
|
||||
});
|
||||
13
packages/insomnia/src/ui/utils/dedupe-collection-items.ts
Normal file
13
packages/insomnia/src/ui/utils/dedupe-collection-items.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function dedupeCollectionItems<T>(items: T[], getId: (item: T) => string): T[] {
|
||||
const seenIds = new Set<string>();
|
||||
const uniqueItems = items.filter(item => {
|
||||
const id = getId(item);
|
||||
if (seenIds.has(id)) {
|
||||
return false;
|
||||
}
|
||||
seenIds.add(id);
|
||||
return true;
|
||||
});
|
||||
|
||||
return uniqueItems.length === items.length ? items : uniqueItems;
|
||||
}
|
||||
Reference in New Issue
Block a user