diff --git a/.clang-format b/.clang-format index 3b0e7ff3094..c3ea599861d 100644 --- a/.clang-format +++ b/.clang-format @@ -1 +1 @@ -ColumnLimit: 100 +ColumnLimit: 120 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..b06419da73b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +build +coverage +node_modules +bin +# Ignore generated files in insomnia +packages/insomnia/src/**.js +packages/insomnia/src/**.js.map +# Ignore fixture files for tests since we need to keep them in a specific format +__fixtures__ +fixtures diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000000..f4c7f695766 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,17 @@ +{ + "trailingComma": "all", + "printWidth": 120, + "arrowParens": "avoid", + "singleQuote": true, + "quoteProps": "consistent", + "overrides": [ + { + "files": "packages/insomnia/**/*", + "options": { + "plugins": [ + "prettier-plugin-tailwindcss" + ] + } + } + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 54ac538b5d1..6a12d1233a1 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp - // List of extensions which should be recommended for users of this workspace. "recommendations": [ "yzhang.markdown-all-in-one", @@ -9,10 +8,10 @@ "dbaeumer.vscode-eslint", "adrieankhisbe.vscode-ndjson", "ms-playwright.playwright", - "bradlc.vscode-tailwindcss" + "bradlc.vscode-tailwindcss", + "esbenp.prettier-vscode", + "llvm-vs-code-extensions.vscode-clangd" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. - "unwantedRecommendations": [ - - ] + "unwantedRecommendations": [] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 95e8aaa7316..87e55d10a5e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,6 +19,12 @@ "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, "cSpell.words": [ "apikey", "Dismissable", @@ -35,12 +41,6 @@ "upsert", "xmark" ], - "[typescriptreact]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - }, - "[typescript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - }, "[cpp]": { "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd" }, diff --git a/package-lock.json b/package-lock.json index ff0f6533812..1fbb6f7be77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,8 @@ "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-simple-import-sort": "^12.1.1", + "prettier": "3.5.3", + "prettier-plugin-tailwindcss": "^0.6.11", "tslib": "2.0.1", "type-fest": "^4.15.0", "typescript": "5.6.2", @@ -15280,6 +15282,22 @@ "node": ">=10" } }, + "node_modules/json-schema-to-typescript/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -18659,15 +18677,98 @@ } }, "node_modules/prettier": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", - "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, "license": "MIT", "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz", + "integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } } }, "node_modules/pretty-format": { @@ -23383,7 +23484,6 @@ "node-forge": "^1.3.1", "oauth-1.0a": "^2.2.6", "papaparse": "^5.4.1", - "prettier": "2.4.1", "shell-quote": "^1.8.1", "swagger-ui-dist": "^5.17.14", "tough-cookie": "^4.1.3", diff --git a/package.json b/package.json index d1be91a0f3f..7f2209dd457 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,8 @@ "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-simple-import-sort": "^12.1.1", + "prettier": "3.5.3", + "prettier-plugin-tailwindcss": "^0.6.11", "tslib": "2.0.1", "type-fest": "^4.15.0", "typescript": "5.6.2", diff --git a/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts b/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts index 4a8a35cbfb2..d63d5871d6b 100644 --- a/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts @@ -41,12 +41,9 @@ test('can render schema and send GraphQL requests', async ({ app, page }) => { await page.getByText('GraphQL request with number').click(); await page.getByTestId('Dropdown-GraphQL-request-with-number').click(); - await page.getByText('Copy as cURL').click(); - const handle = await page.evaluateHandle(() => navigator.clipboard.readText()); - const clipboardContent = await handle.jsonValue(); - const exepctResult = JSON.stringify({ query: 'query($inputVar:Int){echoNum(intVar:$inputVar)}', variables: { inputVar: 3 } }); - // expect exported curl body to be JSON string - expect(clipboardContent.split('--data ')[1]?.replace(/[\n\s\']/g, '')).toContain(exepctResult); + await page.getByRole('menuitemradio', { name: 'Generate Code' }).click(); + await page.getByText('"query": "query($inputVar: Int) { echoNum(intVar: $inputVar)}",').click(); + await page.getByRole('button', { name: 'Done' }).click(); await page.getByRole('button', { name: 'Send' }).click(); diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 893f7e37ab3..b7dc03c9a94 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -77,7 +77,6 @@ "node-forge": "^1.3.1", "oauth-1.0a": "^2.2.6", "papaparse": "^5.4.1", - "prettier": "2.4.1", "shell-quote": "^1.8.1", "swagger-ui-dist": "^5.17.14", "tough-cookie": "^4.1.3", diff --git a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx index ec448642843..7ac553db26c 100644 --- a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx @@ -22,7 +22,6 @@ import type { Request } from '../../../../models/request'; import { fetchRequestData, responseTransform, sendCurlAndWriteTimeline, tryToInterpolateRequest, tryToTransformRequestWithPlugins } from '../../../../network/network'; import { invariant } from '../../../../utils/invariant'; import { jsonPrettify } from '../../../../utils/prettify/json'; -import { useRootLoaderData } from '../../../routes/root'; import { Dropdown, DropdownItem, DropdownSection, ItemContent } from '../../base/dropdown'; import { CodeEditor, type CodeEditorHandle } from '../../codemirror/code-editor'; import { GraphQLExplorer } from '../../graph-ql-explorer/graph-ql-explorer'; @@ -31,6 +30,7 @@ import { HelpTooltip } from '../../help-tooltip'; import { Icon } from '../../icon'; import { useDocBodyKeyboardShortcuts } from '../../keydown-binder'; import { TimeFromNow } from '../../time-from-now'; +import { prettifyGraphql } from './prettify-graphql.mjs'; // Type guard to ensure loc is non-nullable const hasLocation = (def: OperationDefinitionNode): def is OperationDefinitionNode & { loc: NonNullable } => { @@ -279,17 +279,10 @@ export const GraphQLEditor: FC = ({ isMounted = false; }; }, [automaticFetch, environmentId, includeInputValueDeprecation, request._id, request.url, workspaceId]); - const { - settings, - } = useRootLoaderData(); - const { editorIndentWithTabs, editorIndentSize } = settings; const beautifyRequestBody = async () => { const { body } = state; - const prettyQuery = await (await import('prettier')).format(body.query, { - parser: 'graphql', - useTabs: editorIndentWithTabs, - tabWidth: editorIndentSize, - }); + + const prettyQuery = prettifyGraphql(body.query); changeQuery(prettyQuery); // Update editor contents if (editorRef.current) { diff --git a/packages/insomnia/src/ui/components/editors/body/prettify-graphql.mjs b/packages/insomnia/src/ui/components/editors/body/prettify-graphql.mjs new file mode 100644 index 00000000000..a3c771a46a1 --- /dev/null +++ b/packages/insomnia/src/ui/components/editors/body/prettify-graphql.mjs @@ -0,0 +1,154 @@ +// taken from https://github.com/martin-888/graphql-prettier/blob/master/src/index.js +import { Kind, parse, print } from 'graphql'; + +const isSelectionsEmpty = (node) => + !node.selectionSet || + !node.selectionSet.selections || + node.selectionSet.selections.length === 0; + +const removeDuplicatedLeafNodes = (sourceNode) => { + if (isSelectionsEmpty(sourceNode)) { + return sourceNode; + } + + const newNode = { ...sourceNode }; + + newNode.selectionSet.selections = newNode.selectionSet.selections + .filter((selection, index, self) => + selection.kind !== Kind.FIELD || + selection.selectionSet || + index === self.findIndex(selection2 => + selection2.kind === Kind.FIELD && + selection.name.value === selection2.name.value)) + .map(removeDuplicatedLeafNodes); + + return newNode; +}; + +const mergeDuplicatedNotLeafNodes = (sourceNode) => { + if (isSelectionsEmpty(sourceNode)) { + return sourceNode; + } + + const newNode = { ...sourceNode }; + + if (sourceNode.selectionSet.selections.length <= 2) { + newNode.selectionSet.selections = newNode.selectionSet.selections + .map(mergeDuplicatedNotLeafNodes); + + return newNode; + } + + let nodesToMerge = []; + + const findIndex = (node1, node2) => + node1.kind === node2.kind && + node1.name.value === node2.name.value && + ((!node1.alias && !node2.alias) || (node1.alias.value === node2.alias.value)); + + newNode.selectionSet.selections = newNode.selectionSet.selections + .filter((selection, index, self) => { + if (selection.kind !== Kind.FIELD) { + return true; + } + + if (!selection.selectionSet) { + return true; + } + + if (index === self.findIndex(selection2 => findIndex(selection, selection2))) { + return true; + } + + nodesToMerge = [ + ...nodesToMerge, + selection, + ]; + + return false; + }); + + nodesToMerge.forEach(node => { + const index = newNode.selectionSet.selections + .findIndex(selection => findIndex(selection, node)); + + newNode.selectionSet.selections[index].selectionSet.selections = [ + ...newNode.selectionSet.selections[index].selectionSet.selections, + ...node.selectionSet.selections, + ]; + }); + + newNode.selectionSet.selections = newNode.selectionSet.selections + .map(mergeDuplicatedNotLeafNodes); + + return newNode; +}; + +const replaceFragments = (sourceNode, fragments) => { + if (isSelectionsEmpty(sourceNode)) { + return sourceNode; + } + + const newNode = { ...sourceNode }; + + let foundFragments = []; + + newNode.selectionSet.selections = newNode.selectionSet.selections + .filter(selection => { + if (selection.kind !== Kind.FRAGMENT_SPREAD) { + return true; + } + + if (!foundFragments.find(fragment => fragment.name.value === selection.name.value)) { + const fragment = fragments.find(fragment => fragment.name.value === selection.name.value); + + if (!fragment) { + throw Error(`Found usage of unknown fragment ${selection.name.value}`); + } + + foundFragments = [ + ...foundFragments, + fragment, + ]; + } + + return false; + }); + + newNode.selectionSet.selections = [ + ...newNode.selectionSet.selections, + ...foundFragments.reduce((acc, fragment) => [...fragment.selectionSet.selections, ...acc], []), + ]; + + newNode.selectionSet.selections = newNode.selectionSet.selections + .map(selection => replaceFragments(selection, fragments)); + + return newNode; +}; + +export const prettifyGraphql = (source, noDuplicates = true) => { + const document = parse(source); + + const fragments = document.definitions + .filter(node => node.kind === Kind.FRAGMENT_DEFINITION) + .map((node, _, self) => replaceFragments(node, self)); + + return document.definitions + .filter(node => node.kind === Kind.OPERATION_DEFINITION) + .map(operationNode => { + let newOperationNode = { ...operationNode }; + + newOperationNode.selectionSet.selections = newOperationNode.selectionSet.selections + .map(selection => replaceFragments(selection, fragments)); + + if (noDuplicates) { + newOperationNode.selectionSet.selections = newOperationNode.selectionSet.selections + .map(selection => removeDuplicatedLeafNodes(mergeDuplicatedNotLeafNodes(selection))); + } + + return newOperationNode; + }) + .map(print) + .join('\n'); +}; + diff --git a/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx b/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx index 64a05997593..921e69b4886 100644 --- a/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx +++ b/packages/insomnia/src/ui/hooks/use-runner-request-list.tsx @@ -19,14 +19,12 @@ export const useRunnerRequestList = (organizationId: string, targetFolderId: str return isRequest(item.doc); }) .map((item: Child) => { - const ancestorNames: string[] = []; - const ancestorIds: string[] = []; + const ancestors:{id:string;name:string}[] = [] if (item.ancestors) { item.ancestors.forEach(ancestorId => { const ancestor = entityMapRef.current.get(ancestorId); if (ancestor && isRequestGroup(ancestor?.doc)) { - ancestorNames.push(ancestor?.doc.name); - ancestorIds.push(ancestor?.doc._id); + ancestors.push({id:ancestor?.doc._id,name:ancestor?.doc.name}) } }); } @@ -36,8 +34,7 @@ export const useRunnerRequestList = (organizationId: string, targetFolderId: str return { id: item.doc._id, name: item.doc.name, - ancestorNames, - ancestorIds, + ancestors, method: requestDoc.method, url: item.doc.url, parentId: item.doc.parentId, @@ -45,7 +42,7 @@ export const useRunnerRequestList = (organizationId: string, targetFolderId: str }) .filter(item => { if (targetFolderId) { - return item.ancestorIds.includes(targetFolderId); + return item.ancestors.map(a=>a.id).includes(targetFolderId); } return true; }); diff --git a/packages/insomnia/src/ui/routes/runner.tsx b/packages/insomnia/src/ui/routes/runner.tsx index b9ef14800d7..d1af2cd3831 100644 --- a/packages/insomnia/src/ui/routes/runner.tsx +++ b/packages/insomnia/src/ui/routes/runner.tsx @@ -102,8 +102,7 @@ export const repositionInArray = (allItems: string[], itemsToMove: string[], tar export interface RequestRow { id: string; name: string; - ancestorNames: string[]; - ancestorIds: string[]; + ancestors: { id: string; name: string }[]; method: string; url: string; parentId: string; @@ -421,7 +420,7 @@ export const Runner: FC<{}> = () => { const selectedRequestIdsForCliCommand = targetFolderId !== null && targetFolderId !== '' ? reqList - .filter(item => item.ancestorIds.includes(targetFolderId)) + .filter(item => item.ancestors.map(a=>a.id).includes(targetFolderId)) .map(item => item.id) .filter(id => selectedKeys === 'all' || selectedKeys.has(id)) : reqList @@ -568,10 +567,9 @@ export const Runner: FC<{}> = () => { disabledKeys={disabledKeys} > {item => { - const parentFolders = item.ancestorNames.map((parentFolderName: string, i: number) => { - // eslint-disable-next-line react/no-array-index-key - return - + const parentFolders = item.ancestors.map(({id,name}) => { + return +