diff --git a/.github/ISSUE_TEMPLATE/BugReport.yaml b/.github/ISSUE_TEMPLATE/BugReport.yaml index 4b6da7871..f525bb7e6 100644 --- a/.github/ISSUE_TEMPLATE/BugReport.yaml +++ b/.github/ISSUE_TEMPLATE/BugReport.yaml @@ -6,26 +6,58 @@ body: attributes: value: | Thanks for taking the time to fill out this bug report! + + Before submitting, please make sure you've searched existing issues: + 👉 [Search existing issues](https://github.com/usebruno/bruno/issues?q=is%3Aissue) + - type: checkboxes attributes: label: 'I have checked the following:' options: - - label: I use the newest version of bruno. - required: true - - label: I've searched existing issues and found nothing related to my issue. + - label: "I have searched existing issues and found nothing related to my issue." required: true + + - type: checkboxes + attributes: + label: 'This bug is:' + options: + - label: making Bruno unusable for me + required: false + - label: slowing me down but I'm able to continue working + required: false + - label: annoying + required: false + + - type: input + attributes: + label: Bruno version + description: Please specify the version of Bruno you are using in which the issue occurs. + placeholder: 1.38.1 + validations: + required: true + + - type: input + attributes: + label: Operating System + description: Information about the operating system the issue occurs on. + placeholder: Windows 11 26100.3037 / macOS 15.1 (24B83) / Linux 6.13.1 + validations: + required: true + - type: textarea attributes: label: Describe the bug - description: A clear and concise description of the bug. + description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce. validations: required: true + - type: textarea attributes: label: .bru file to reproduce the bug - description: Attach your .bru file here that can reqroduce the problem. + description: Attach your .bru file here that can reproduce the problem. validations: required: false + - type: textarea attributes: label: Screenshots/Live demo link diff --git a/.github/ISSUE_TEMPLATE/FeatureRequest.yaml b/.github/ISSUE_TEMPLATE/FeatureRequest.yaml index 3a3997beb..161e56e9c 100644 --- a/.github/ISSUE_TEMPLATE/FeatureRequest.yaml +++ b/.github/ISSUE_TEMPLATE/FeatureRequest.yaml @@ -8,13 +8,23 @@ body: options: - label: I've searched existing issues and found nothing related to my issue. required: true + - type: checkboxes + attributes: + label: 'This feature' + options: + - label: blocks me from using Bruno + required: false + - label: would improve my quality of life in Bruno + required: false + - label: is something I've never seen an API client do before + required: false - type: markdown attributes: value: | Suggest an idea for this project. - type: textarea attributes: - label: Describe the feature you want to add + label: Describe the feature you want to add, and how it would change your usage of Bruno description: A clear and concise description of the feature you want to be added. validations: required: true @@ -23,4 +33,4 @@ body: label: Mockups or Images of the feature description: Add some images to support your feature. validations: - required: true + required: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c029b0224..d218fc65a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,14 +5,13 @@ on: pull_request: branches: [main] -permissions: - contents: read - jobs: unit-test: name: Unit Tests timeout-minutes: 60 runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -55,6 +54,7 @@ jobs: permissions: checks: write pull-requests: write + contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 diff --git a/package-lock.json b/package-lock.json index f53c786a9..8d63dedff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -787,7 +786,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -818,7 +816,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -836,7 +833,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/@babel/generator": { @@ -1116,7 +1112,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -7083,7 +7078,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { @@ -7096,7 +7090,6 @@ "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/linkify-it": "*", @@ -7107,7 +7100,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -7199,6 +7191,13 @@ "@types/estree": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/verror": { "version": "1.10.10", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", @@ -8586,7 +8585,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, "license": "ISC" }, "node_modules/boolean": { @@ -9417,6 +9415,229 @@ "node": "*" } }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/cheerio-select/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/cheerio/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -10115,7 +10336,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -10583,7 +10803,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -11256,6 +11475,15 @@ "domelementtype": "1" } }, + "node_modules/dompurify": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", @@ -11718,13 +11946,25 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { "iconv-lite": "^0.6.2" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -12739,7 +12979,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -17377,7 +17616,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -17782,6 +18020,106 @@ "dev": true, "license": "MIT" }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -21170,7 +21508,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, "license": "ISC" }, "node_modules/scheduler": { @@ -23213,7 +23550,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -23229,6 +23566,15 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "license": "MIT" }, + "node_modules/undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -23757,6 +24103,27 @@ "node": ">=10.13.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -23898,6 +24265,28 @@ "node": ">= 16" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -24019,7 +24408,7 @@ }, "packages/bruno-app": { "name": "@usebruno/app", - "version": "0.3.0", + "version": "1.39.0", "dependencies": { "@babel/preset-env": "^7.26.0", "@fontsource/inter": "^5.0.15", @@ -24034,6 +24423,7 @@ "codemirror": "5.65.2", "codemirror-graphql": "2.1.1", "cookie": "0.7.1", + "dompurify": "^3.2.4", "escape-html": "^1.0.3", "file": "^0.2.2", "file-dialog": "^0.0.8", @@ -24043,7 +24433,7 @@ "graphiql": "3.7.1", "graphql": "^16.6.0", "graphql-request": "^3.7.0", - "httpsnippet": "^3.0.6", + "httpsnippet": "^3.0.9", "i18next": "24.1.2", "idb": "^7.0.0", "immer": "^9.0.15", @@ -24078,6 +24468,7 @@ "react-redux": "^7.2.9", "react-tooltip": "^5.5.2", "sass": "^1.46.0", + "semver": "^7.7.1", "strip-json-comments": "^5.0.1", "styled-components": "^5.3.3", "system": "^2.0.1", @@ -24135,6 +24526,17 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "packages/bruno-app/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "packages/bruno-cli": { "name": "@usebruno/cli", "version": "1.16.0", @@ -24365,6 +24767,7 @@ "btoa": "^1.2.1", "chai": "^4.3.7", "chai-string": "^1.5.0", + "cheerio": "^1.0.0", "crypto-js": "^4.1.1", "json-query": "^2.2.2", "lodash": "^4.17.21", @@ -24374,7 +24777,8 @@ "node-vault": "^0.10.2", "path": "^0.12.7", "quickjs-emscripten": "^0.29.2", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "xml2js": "^0.6.2" }, "devDependencies": { "@rollup/plugin-commonjs": "^23.0.2", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 7203bb816..f3e6fe292 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -1,6 +1,6 @@ { "name": "@usebruno/app", - "version": "0.3.0", + "version": "1.39.0", "private": true, "scripts": { "dev": "rsbuild dev", @@ -24,6 +24,7 @@ "codemirror": "5.65.2", "codemirror-graphql": "2.1.1", "cookie": "0.7.1", + "dompurify": "^3.2.4", "escape-html": "^1.0.3", "file": "^0.2.2", "file-dialog": "^0.0.8", @@ -68,6 +69,7 @@ "react-redux": "^7.2.9", "react-tooltip": "^5.5.2", "sass": "^1.46.0", + "semver": "^7.7.1", "strip-json-comments": "^5.0.1", "styled-components": "^5.3.3", "system": "^2.0.1", diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index 2f9ca9cdd..ed198086d 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -83,7 +83,7 @@ if (!SERVER_RENDERED) { 'bru.runner', 'bru.runner.setNextRequest(requestName)', 'bru.runner.skipRequest()', - 'bru.runner.stopExecution()', + 'bru.runner.stopExecution()' ]; CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => { const cursor = editor.getCursor(); @@ -174,11 +174,21 @@ export default class CodeEditor extends React.Component { } }, 'Cmd-F': (cm) => { + if (this._isSearchOpen()) { + // replace the older search component with the new one + const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top'); + search && search.remove(); + } cm.execCommand('findPersistent'); this._bindSearchHandler(); this._appendSearchResultsCount(); }, 'Ctrl-F': (cm) => { + if (this._isSearchOpen()) { + // replace the older search component with the new one + const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top'); + search && search.remove(); + } cm.execCommand('findPersistent'); this._bindSearchHandler(); this._appendSearchResultsCount(); @@ -365,6 +375,10 @@ export default class CodeEditor extends React.Component { } }; + _isSearchOpen = () => { + return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top'); + }; + /** * Bind handler to search input to count number of search results */ diff --git a/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js index afe08bcba..4c3130e3d 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js @@ -1,11 +1,7 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` - div.CodeMirror { - .CodeMirror-scroll { - padding-bottom: 0px; - } - } + .editing-mode { cursor: pointer; } diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js index 86bf2308f..a83850e91 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js @@ -1,10 +1,14 @@ -import React from 'react'; +import React from "react"; import { getTotalRequestCountInCollection } from 'utils/collections/'; -import { IconFolder, IconFileOff, IconWorld, IconApi } from '@tabler/icons'; +import { IconFolder, IconWorld, IconApi, IconClock } from '@tabler/icons'; +import { areItemsLoading, getItemsLoadStats } from "utils/collections/index"; const Info = ({ collection }) => { const totalRequestsInCollection = getTotalRequestCountInCollection(collection); + const isCollectionLoading = areItemsLoading(collection); + const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection); + return (
@@ -42,8 +46,10 @@ const Info = ({ collection }) => {
Requests
-
- {totalRequestsInCollection} request{totalRequestsInCollection !== 1 ? 's' : ''} in collection +
+ { + isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection` + }
@@ -53,4 +59,4 @@ const Info = ({ collection }) => { ); }; -export default Info; +export default Info; \ No newline at end of file diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js index c15b36cd8..4c7406580 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js @@ -2,8 +2,15 @@ import React from 'react'; import { flattenItems } from "utils/collections"; import { IconAlertTriangle } from '@tabler/icons'; import StyledWrapper from "./StyledWrapper"; +import { useDispatch, useSelector } from 'react-redux'; +import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index'; +import { getDefaultRequestPaneTab } from 'utils/collections/index'; +import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; +import { hideHomePage } from 'providers/ReduxStore/slices/app'; const RequestsNotLoaded = ({ collection }) => { + const dispatch = useDispatch(); + const tabs = useSelector((state) => state.tabs.tabs); const flattenedItems = flattenItems(collection.items); const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading); @@ -11,6 +18,29 @@ const RequestsNotLoaded = ({ collection }) => { return null; } + const handleRequestClick = (item) => e => { + e.preventDefault(); + if (isItemARequest(item)) { + dispatch(hideHomePage()); + if (itemIsOpenedInTabs(item, tabs)) { + dispatch( + focusTab({ + uid: item.uid + }) + ); + return; + } + dispatch( + addTab({ + uid: item.uid, + collectionUid: collection.uid, + requestPaneTab: getDefaultRequestPaneTab(item) + }) + ); + return; + } + } + return (
@@ -31,7 +61,7 @@ const RequestsNotLoaded = ({ collection }) => { {flattenedItems?.map((item, index) => ( item?.partial && !item?.loading ? ( - + {item?.pathname?.split(`${collection?.pathname}/`)?.[1]} diff --git a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js index 105a92642..bb48cbdc0 100644 --- a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js @@ -104,18 +104,15 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
) : ( diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js index 187a91a68..07dcf1419 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js @@ -154,7 +154,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
-
{getTabPanel(focusedTab.requestPaneTab)}
+
{getTabPanel(focusedTab.requestPaneTab)}
); }; diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js index 91fea0134..eaac6f204 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js @@ -49,7 +49,7 @@ const GraphQLVariables = ({ variables, item, collection }) => { const onSave = () => dispatch(saveRequest(item.uid, collection.uid)); return ( - + <>
Path -
Path variables are automatically added whenever the :name @@ -186,9 +185,7 @@ const QueryParams = ({ item, collection }) => { https://example.com/v1/users/:id
- `} - infotipId="path-param-InfoTip" - /> +
diff --git a/packages/bruno-app/src/components/RequestPane/Tests/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Tests/StyledWrapper.js deleted file mode 100644 index 9f7583222..000000000 --- a/packages/bruno-app/src/components/RequestPane/Tests/StyledWrapper.js +++ /dev/null @@ -1,10 +0,0 @@ -import styled from 'styled-components'; - -const StyledWrapper = styled.div` - div.CodeMirror { - /* todo: find a better way */ - height: calc(100vh - 220px); - } -`; - -export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Tests/index.js b/packages/bruno-app/src/components/RequestPane/Tests/index.js index c781d34d5..d0d19c283 100644 --- a/packages/bruno-app/src/components/RequestPane/Tests/index.js +++ b/packages/bruno-app/src/components/RequestPane/Tests/index.js @@ -5,7 +5,6 @@ import CodeEditor from 'components/CodeEditor'; import { updateRequestTests } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { useTheme } from 'providers/Theme'; -import StyledWrapper from './StyledWrapper'; const Tests = ({ item, collection }) => { const dispatch = useDispatch(); @@ -28,19 +27,17 @@ const Tests = ({ item, collection }) => { const onSave = () => dispatch(saveRequest(item.uid, collection.uid)); return ( - - - + ); }; diff --git a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js index c073135d3..cd3f83797 100644 --- a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js @@ -98,7 +98,7 @@ const VarsTable = ({ item, collection, vars, varType }) => { ) : (
Expr - +
), accessor: 'value', width: '46%' }, { name: '', accessor: '', width: '14%' } diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js index 1a951b624..7908dfc09 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js @@ -1,4 +1,4 @@ -import { IconLoader2, IconFile } from '@tabler/icons'; +import { IconLoader2, IconFile, IconAlertTriangle } from '@tabler/icons'; import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions'; import { useDispatch } from 'react-redux'; import StyledWrapper from './StyledWrapper'; @@ -15,65 +15,59 @@ const RequestNotLoaded = ({ collection, item }) => { return
-
+
File Info
-
+
Name:
{item?.name}
- +
Path:
{item?.pathname}
- +
Size:
{item?.size?.toFixed?.(2)} MB
{!item?.error && ( - <> -
-
- Due to its large size, this request wasn't loaded automatically. +
+
+ + The request wasn't loaded due to its large size. Please try again with the following options:
-
-
- - - May cause the app to freeze temporarily while it runs. - -
-
+
- - Runs in background. - -
+

(Runs in background)

- +
+ +

(May cause the app to freeze temporarily while it runs)

+
+
)} {item?.loading && ( <> -
+
Loading... diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js index 8ca76b15e..447523fdb 100644 --- a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js +++ b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js @@ -35,7 +35,7 @@ const CollectionToolBar = ({ collection }) => { const viewCollectionSettings = () => { dispatch( addTab({ - uid: uuid(), + uid: collection.uid, collectionUid: collection.uid, type: 'collection-settings' }) diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index 4381c86b1..e8bf153a4 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -11,14 +11,14 @@ import ResponsePane from './ResponsePane'; import StyledWrapper from './StyledWrapper'; import { areItemsLoading } from 'utils/collections'; -const getRelativePath = (fullPath, pathname) => { +const getDisplayName = (fullPath, pathname, name) => { // convert to unix style path fullPath = slash(fullPath); pathname = slash(pathname); let relativePath = path.relative(fullPath, pathname); - const { dir, name } = path.parse(relativePath); - return path.join(dir, name); + const { dir } = path.parse(relativePath); + return [dir, name].filter(i => i).join('/'); }; export default function RunnerResults({ collection }) { @@ -58,7 +58,7 @@ export default function RunnerResults({ collection }) { type: info.type, filename: info.filename, pathname: info.pathname, - relativePath: getRelativePath(collection.pathname, info.pathname) + displayName: getDisplayName(collection.pathname, info.pathname, info.name) }; if (newItem.status !== 'error' && newItem.status !== 'skipped') { if (newItem.testResults) { @@ -186,7 +186,7 @@ export default function RunnerResults({ collection }) { - {item.relativePath} + {item.displayName} {item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? ( @@ -266,7 +266,7 @@ export default function RunnerResults({ collection }) {
- {selectedItem.relativePath} + {selectedItem.displayName} {selectedItem.testStatus === 'pass' ? ( @@ -275,7 +275,6 @@ export default function RunnerResults({ collection }) { )}
- {/*
{selectedItem.relativePath}
*/}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js index 41d3e5ff2..1b048e731 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js @@ -127,7 +127,7 @@ const CloneCollection = ({ onClose, collection }) => { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 3e426eb7f..a08fac3b7 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -32,6 +32,7 @@ const CollectionItem = ({ item, collection, searchText }) => { const activeTabUid = useSelector((state) => state.tabs.activeTabUid); const isSidebarDragging = useSelector((state) => state.app.isDragging); const dispatch = useDispatch(); + const collectionItemRef = useRef(null); const [renameItemModalOpen, setRenameItemModalOpen] = useState(false); const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false); @@ -45,28 +46,31 @@ const CollectionItem = ({ item, collection, searchText }) => { const itemIsCollapsed = hasSearchText ? false : item.collapsed; const [{ isDragging }, drag] = useDrag({ - type: `COLLECTION_ITEM_${collection.uid}`, + type: `collection-item-${collection.uid}`, item: item, collect: (monitor) => ({ isDragging: monitor.isDragging() - }) + }), + options: { + dropEffect: "move" + } }); const [{ isOver }, drop] = useDrop({ - accept: `COLLECTION_ITEM_${collection.uid}`, + accept: `collection-item-${collection.uid}`, drop: (draggedItem) => { - if (draggedItem.uid !== item.uid) { - dispatch(moveItem(collection.uid, draggedItem.uid, item.uid)); - } + dispatch(moveItem(collection.uid, draggedItem.uid, item.uid)); }, canDrop: (draggedItem) => { return draggedItem.uid !== item.uid; }, collect: (monitor) => ({ - isOver: monitor.isOver() - }) + isOver: monitor.isOver(), + }), }); + drag(drop(collectionItemRef)); + const dropdownTippyRef = useRef(); const MenuIcon = forwardRef((props, ref) => { return ( @@ -255,7 +259,7 @@ const CollectionItem = ({ item, collection, searchText }) => { {generateCodeItemModalOpen && ( setGenerateCodeItemModalOpen(false)} /> )} -
drag(drop(node))}> +
{indents && indents.length ? indents.map((i) => { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js index b8e0d21fd..5c06cc42a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js @@ -12,6 +12,17 @@ const Wrapper = styled.div` transform: rotateZ(90deg); } + &.item-hovered { + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + .collection-actions { + .dropdown { + div[aria-expanded='false'] { + visibility: visible; + } + } + } + } + .collection-actions { .dropdown { div[aria-expanded='true'] { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 3fe00c686..f21f25ac3 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -2,11 +2,11 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react'; import classnames from 'classnames'; import { uuid } from 'utils/common'; import filter from 'lodash/filter'; -import { useDrop } from 'react-dnd'; +import { useDrop, useDrag } from 'react-dnd'; import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; import { collapseCollection } from 'providers/ReduxStore/slices/collections'; -import { mountCollection, moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { mountCollection, moveItemToRootOfCollection, moveCollectionAndPersist } from 'providers/ReduxStore/slices/collections/actions'; import { useDispatch, useSelector } from 'react-redux'; import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import NewRequest from 'components/Sidebar/NewRequest'; @@ -33,6 +33,7 @@ const Collection = ({ collection, searchText }) => { const tabs = useSelector((state) => state.tabs.tabs); const dispatch = useDispatch(); const isLoading = areItemsLoading(collection); + const collectionRef = useRef(null); const menuDropdownTippyRef = useRef(); const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref); @@ -117,33 +118,58 @@ const Collection = ({ collection, searchText }) => { const viewCollectionSettings = () => { dispatch( addTab({ - uid: uuid(), + uid: collection.uid, collectionUid: collection.uid, type: 'collection-settings' }) ); }; + const isCollectionItem = (itemType) => { + return itemType.startsWith('collection-item'); + }; + + const [{ isDragging }, drag] = useDrag({ + type: "collection", + item: collection, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + options: { + dropEffect: "move" + } + }); + const [{ isOver }, drop] = useDrop({ - accept: `COLLECTION_ITEM_${collection.uid}`, - drop: (draggedItem) => { - dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid)); + accept: ["collection", `collection-item-${collection.uid}`], + drop: (draggedItem, monitor) => { + const itemType = monitor.getItemType(); + if (isCollectionItem(itemType)) { + dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid)) + } else { + dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection})); + } }, canDrop: (draggedItem) => { - // todo need to make sure that draggedItem belongs to the collection - return true; + return draggedItem.uid !== collection.uid; }, collect: (monitor) => ({ - isOver: monitor.isOver() - }) + isOver: monitor.isOver(), + }), }); + drag(drop(collectionRef)); + if (searchText && searchText.length) { if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) { return null; } } + const collectionRowClassName = classnames('flex py-1 collection-name items-center', { + 'item-hovered': isOver + }); + // we need to sort request items by seq property const sortRequestItems = (items = []) => { return items.sort((a, b) => a.seq - b.seq); @@ -173,7 +199,9 @@ const Collection = ({ collection, searchText }) => { {showCloneCollectionModalOpen && ( setShowCloneCollectionModalOpen(false)} /> )} -
+
{ {collections && collections.length ? collections.map((c) => { return ( - - - + ); }) : null} diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js index 6f05207d2..17f31f1f8 100644 --- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js @@ -120,7 +120,7 @@ const CreateCollection = ({ onClose }) => { diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index 50e19c22e..9f476e3c8 100644 --- a/packages/bruno-app/src/components/Sidebar/index.js +++ b/packages/bruno-app/src/components/Sidebar/index.js @@ -5,6 +5,7 @@ import Preferences from 'components/Preferences'; import Cookies from 'components/Cookies'; import ToolHint from 'components/ToolHint'; import GoldenEdition from './GoldenEdition'; +import { useApp } from 'providers/App'; import { useState, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; @@ -20,7 +21,7 @@ const Sidebar = () => { const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth); const preferencesOpen = useSelector((state) => state.app.showPreferences); const [goldenEditionOpen, setGoldenEditionOpen] = useState(false); - + const { version } = useApp(); const [asideWidth, setAsideWidth] = useState(leftSidebarWidth); const [cookiesOpen, setCookiesOpen] = useState(false); @@ -184,7 +185,7 @@ const Sidebar = () => { Star */}
-
v1.36.1
+
v{version}
diff --git a/packages/bruno-app/src/components/ToolHint/index.js b/packages/bruno-app/src/components/ToolHint/index.js index b8799dd69..3d559625e 100644 --- a/packages/bruno-app/src/components/ToolHint/index.js +++ b/packages/bruno-app/src/components/ToolHint/index.js @@ -34,7 +34,7 @@ const ToolHint = ({ - + + + ); } diff --git a/packages/bruno-app/src/providers/App/index.js b/packages/bruno-app/src/providers/App/index.js index 7664ae03e..b06d1d3a8 100644 --- a/packages/bruno-app/src/providers/App/index.js +++ b/packages/bruno-app/src/providers/App/index.js @@ -6,11 +6,12 @@ import ConfirmAppClose from './ConfirmAppClose'; import useIpcEvents from './useIpcEvents'; import useTelemetry from './useTelemetry'; import StyledWrapper from './StyledWrapper'; +import { version } from '../../../package.json'; export const AppContext = React.createContext(); export const AppProvider = (props) => { - useTelemetry(); + useTelemetry({ version }); useIpcEvents(); const dispatch = useDispatch(); @@ -37,7 +38,7 @@ export const AppProvider = (props) => { }, []); return ( - + {props.children} @@ -46,4 +47,12 @@ export const AppProvider = (props) => { ); }; +export const useApp = () => { + const context = React.useContext(AppContext); + if (!context) { + throw new Error('useApp must be used within an AppProvider'); + } + return context; +}; + export default AppProvider; diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index 6b64e1279..712a6efb7 100644 --- a/packages/bruno-app/src/providers/App/useTelemetry.js +++ b/packages/bruno-app/src/providers/App/useTelemetry.js @@ -42,7 +42,7 @@ const getAnonymousTrackingId = () => { return id; }; -const trackStart = () => { +const trackStart = (version) => { if (isPlaywrightTestRunning()) { return; } @@ -58,16 +58,18 @@ const trackStart = () => { event: 'start', properties: { os: platformLib.os.family, - version: '1.38.1' + version: version } }); }; -const useTelemetry = () => { +const useTelemetry = ({ version }) => { useEffect(() => { - trackStart(); - setInterval(trackStart, 24 * 60 * 60 * 1000); - }, []); + if (posthogApiKey && posthogApiKey.length) { + trackStart(version); + setInterval(trackStart, 24 * 60 * 60 * 1000); + } + }, [posthogApiKey]); }; export default useTelemetry; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 7de849eea..c2532b3fd 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -32,6 +32,7 @@ import { selectEnvironment as _selectEnvironment, sortCollections as _sortCollections, updateCollectionMountStatus, + moveCollection, requestCancelled, resetRunResults, responseReceived, @@ -1151,6 +1152,22 @@ export const importCollection = (collection, collectionLocation) => (dispatch, g }); }; +export const moveCollectionAndPersist = ({ draggedItem, targetItem }) => (dispatch, getState) => { + dispatch(moveCollection({ draggedItem, targetItem })); + + return new Promise((resolve, reject) => { + const { ipcRenderer } = window; + const state = getState(); + + const collectionPaths = state.collections.collections.map((collection) => collection.pathname); + + ipcRenderer + .invoke('renderer:update-collection-paths', collectionPaths) + .then(resolve) + .catch(reject); + }); +}; + export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 3ae0fa4e5..905576a2f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1,5 +1,5 @@ import { uuid } from 'utils/common'; -import { find, map, forOwn, concat, filter, each, cloneDeep, get, set } from 'lodash'; +import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex } from 'lodash'; import { createSlice } from '@reduxjs/toolkit'; import { addDepth, @@ -59,7 +59,9 @@ export const collectionsSlice = createSlice({ updateCollectionMountStatus: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { - collection.mountStatus = action.payload.mountStatus; + if (action.payload.mountStatus) { + collection.mountStatus = action.payload.mountStatus; + } } }, setCollectionSecurityConfig: (state, action) => { @@ -100,6 +102,12 @@ export const collectionsSlice = createSlice({ break; } }, + moveCollection: (state, action) => { + const { draggedItem, targetItem } = action.payload; + state.collections = state.collections.filter((i) => i.uid !== draggedItem.uid); // Remove dragged item + const targetItemIndex = state.collections.findIndex((i) => i.uid === targetItem.uid); // Find target item + state.collections.splice(targetItemIndex, 0, draggedItem); // Insert dragged-item above target-item + }, updateLastAction: (state, action) => { const { collectionUid, lastAction } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); @@ -1761,6 +1769,12 @@ export const collectionsSlice = createSlice({ // we don't want to lose the draft in this case if (areItemsTheSameExceptSeqUpdate(item, file.data)) { item.seq = file.data.seq; + if (item?.draft) { + item.draft.seq = file.data.seq; + } + if (item?.draft && areItemsTheSameExceptSeqUpdate(item?.draft, file.data)) { + item.draft = null; + } } else { item.name = file.data.name; item.type = file.data.type; @@ -2082,7 +2096,8 @@ export const { runFolderEvent, resetCollectionRunner, updateRequestDocs, - updateFolderDocs + updateFolderDocs, + moveCollection } = collectionsSlice.actions; export default collectionsSlice.reducer; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js index ca6c232d8..062f367ca 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js @@ -1,7 +1,7 @@ import toast from 'react-hot-toast'; import { createSlice } from '@reduxjs/toolkit'; import { getAppInstallDate } from 'utils/common/platform'; - +import semver from 'semver'; const getReadNotificationIds = () => { try { let readNotificationIdsString = window.localStorage.getItem('bruno.notifications.read'); @@ -27,6 +27,26 @@ const initialState = { readNotificationIds: getReadNotificationIds() || [] }; +export const filterNotificationsByVersion = (notifications, currentVersion) => { + try { + if (!notifications) return []; + + if (!currentVersion) return notifications; + + return notifications.filter(notification => { + const { minVersion, maxVersion } = notification; + if (!minVersion && !maxVersion) return true; + if (!minVersion) return semver.lte(currentVersion, maxVersion); + if (!maxVersion) return semver.gte(currentVersion, minVersion); + + return semver.gte(currentVersion, minVersion) && semver.lte(currentVersion, maxVersion); + }); + } catch (error) { + console.error(error); + return []; + } +}; + export const notificationSlice = createSlice({ name: 'notifications', initialState, @@ -86,13 +106,14 @@ export const notificationSlice = createSlice({ export const { setNotifications, setFetchingStatus, markNotificationAsRead, markAllNotificationsAsRead } = notificationSlice.actions; -export const fetchNotifications = () => (dispatch, getState) => { +export const fetchNotifications = ({currentVersion}) => (dispatch, getState) => { return new Promise((resolve) => { const { ipcRenderer } = window; dispatch(setFetchingStatus(true)); ipcRenderer .invoke('renderer:fetch-notifications') .then((notifications) => { + notifications = filterNotificationsByVersion(notifications, currentVersion); dispatch(setNotifications({ notifications })); dispatch(setFetchingStatus(false)); resolve(notifications); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js new file mode 100644 index 000000000..80e84d9bd --- /dev/null +++ b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js @@ -0,0 +1,133 @@ +const { filterNotificationsByVersion } = require('./notifications'); + +describe('filterNotificationsByVersion - basic', () => { + it('should filter notifications by version', () => { + const notifications = [{ minVersion: '1.0.0', maxVersion: '1.1.0' }]; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([{ minVersion: '1.0.0', maxVersion: '1.1.0' }]); + }); + + it('should gracefully handle no notifications', () => { + const notifications = []; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([]); + }); + + it('should gracefully handle notifications are undefined', () => { + const notifications = undefined; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([]); + }); + + it('should gracefully handle scenario when no current version is provided', () => { + const notifications = [{ minVersion: '1.0.0', maxVersion: '1.1.0' }]; + const filteredNotifications = filterNotificationsByVersion(notifications); + expect(filteredNotifications).toEqual(notifications); + }); + + it('should gracefully handle scenario minVersion is undefined', () => { + const notifications = [{ minVersion: undefined, maxVersion: '1.1.0' }]; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual(notifications); + }); + + it('should gracefully handle scenario maxVersion is undefined', () => { + const notifications = [{ minVersion: '1.0.0', maxVersion: undefined }]; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual(notifications); + }); + + it('should gracefully handle scenario minVersion and maxVersion are undefined', () => { + const notifications = [{ minVersion: undefined, maxVersion: undefined }]; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual(notifications); + }); +}); + +describe('filterNotificationsByVersion - semver', () => { + it('should filter out notifications outside version range', () => { + const notifications = [ + { minVersion: '1.0.0', maxVersion: '1.1.0' }, // should be included + { minVersion: '2.0.0', maxVersion: '2.1.0' }, // should be filtered out + { minVersion: '0.5.0', maxVersion: '0.9.0' } // should be filtered out + ]; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([ + { minVersion: '1.0.0', maxVersion: '1.1.0' } + ]); + }); + + it('should handle mixed valid and invalid version ranges', () => { + const notifications = [ + { minVersion: '1.0.0', maxVersion: '2.0.0' }, // should be included + { minVersion: '3.0.0', maxVersion: '4.0.0' }, // should be filtered out + { minVersion: '1.5.0', maxVersion: '1.8.0' }, // should be included + { minVersion: '0.1.0', maxVersion: '0.5.0' } // should be filtered out + ]; + const currentVersion = '1.6.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([ + { minVersion: '1.0.0', maxVersion: '2.0.0' }, + { minVersion: '1.5.0', maxVersion: '1.8.0' } + ]); + }); + + it('should handle edge cases of version ranges', () => { + const notifications = [ + { minVersion: '1.0.0', maxVersion: '1.0.0' }, // should be included + { minVersion: '1.0.1', maxVersion: '2.0.0' }, // should be filtered out + { minVersion: '0.9.9', maxVersion: '1.0.0' } // should be included + ]; + const currentVersion = '1.0.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([ + { minVersion: '1.0.0', maxVersion: '1.0.0' }, + { minVersion: '0.9.9', maxVersion: '1.0.0' } + ]); + }); +}); + +describe('filterNotificationsByVersion - undefined version bounds', () => { + it('should include notifications when minVersion is undefined and current version is below maxVersion', () => { + const notifications = [ + { minVersion: undefined, maxVersion: '2.0.0' } + ]; + const currentVersion = '1.5.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual(notifications); + }); + + it('should exclude notifications when minVersion is undefined and current version is above maxVersion', () => { + const notifications = [ + { minVersion: undefined, maxVersion: '2.0.0' } + ]; + const currentVersion = '2.1.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([]); + }); + + it('should include notifications when maxVersion is undefined and current version is above minVersion', () => { + const notifications = [ + { minVersion: '1.0.0', maxVersion: undefined } + ]; + const currentVersion = '2.0.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual(notifications); + }); + + it('should exclude notifications when maxVersion is undefined and current version is below minVersion', () => { + const notifications = [ + { minVersion: '1.0.0', maxVersion: undefined } + ]; + const currentVersion = '0.9.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([]); + }); +}); \ No newline at end of file diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js index 91b39b33d..219655d70 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js @@ -11,24 +11,35 @@ const initialState = { activeTabUid: null }; +const tabTypeAlreadyExists = (tabs, collectionUid, type) => { + return find(tabs, (tab) => tab.collectionUid === collectionUid && tab.type === type); +}; + export const tabsSlice = createSlice({ name: 'tabs', initialState, reducers: { addTab: (state, action) => { const { uid, collectionUid, type, requestPaneTab, preview } = action.payload; - - const existingTab = find(state.tabs, (tab) => tab.uid === uid); - - if (existingTab) { - state.activeTabUid = existingTab.uid; - return; - } const nonReplaceableTabTypes = [ "variables", "collection-runner", "security-settings", ]; + + const existingTab = find(state.tabs, (tab) => tab.uid === uid); + if (existingTab) { + state.activeTabUid = existingTab.uid; + return; + } + + if (nonReplaceableTabTypes.includes(type)) { + const existingTab = tabTypeAlreadyExists(state.tabs, collectionUid, type); + if (existingTab) { + state.activeTabUid = existingTab.uid; + return; + } + } const lastTab = state.tabs[state.tabs.length - 1]; if (state.tabs.length > 0 && lastTab.preview) { @@ -39,7 +50,9 @@ export const tabsSlice = createSlice({ requestPaneTab: requestPaneTab || 'params', responsePaneTab: 'response', type: type || 'request', - preview: true, + preview: preview !== undefined + ? preview + : !nonReplaceableTabTypes.includes(type), ...(uid ? { folderUid: uid } : {}) } diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index eb53cfb48..3ac612c62 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -1,13 +1,4 @@ -import get from 'lodash/get'; -import each from 'lodash/each'; -import find from 'lodash/find'; -import findIndex from 'lodash/findIndex'; -import isString from 'lodash/isString'; -import map from 'lodash/map'; -import filter from 'lodash/filter'; -import sortBy from 'lodash/sortBy'; -import isEqual from 'lodash/isEqual'; -import cloneDeep from 'lodash/cloneDeep'; +import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash'; import { uuid } from 'utils/common'; import path from 'path'; import slash from 'utils/common/slash'; @@ -146,6 +137,20 @@ export const areItemsLoading = (folder) => { }, false); } +export const getItemsLoadStats = (folder) => { + let loadingCount = 0; + let flattenedItems = flattenItems(folder.items); + flattenedItems?.forEach(i => { + if(i?.loading) { + loadingCount += 1; + } + }); + return { + loading: loadingCount, + total: flattenedItems?.length + }; +} + export const moveCollectionItem = (collection, draggedItem, targetItem) => { let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid); diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index 89ab15c7e..e4acd9ba6 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -181,6 +181,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) = brunoParent.items = brunoParent.items || []; const folderMap = {}; const requestMap = {}; + const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'] each(item, (i) => { if (isItemAFolder(i)) { @@ -230,6 +231,11 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) = } else { if (i.request) { + if(!requestMethods.includes(i?.request?.method.toUpperCase())){ + console.warn("Unexpected request.method") + return; + } + const baseRequestName = i.name; let requestName = baseRequestName; let count = 1; diff --git a/packages/bruno-app/src/utils/importers/translators/postman_translation.js b/packages/bruno-app/src/utils/importers/translators/postman_translation.js index b386d719e..5d6573c16 100644 --- a/packages/bruno-app/src/utils/importers/translators/postman_translation.js +++ b/packages/bruno-app/src/utils/importers/translators/postman_translation.js @@ -24,6 +24,10 @@ const replacements = { 'postman\\.setEnvironmentVariable\\(': 'bru.setEnvVar(', 'postman\\.getEnvironmentVariable\\(': 'bru.getEnvVar(', 'postman\\.clearEnvironmentVariable\\(': 'bru.deleteEnvVar(', + 'pm\\.execution\\.skipRequest\\(\\)': 'bru.runner.skipRequest()', + 'pm\\.execution\\.skipRequest': 'bru.runner.skipRequest', + 'pm\\.execution\\.setNextRequest\\(null\\)': 'bru.runner.stopExecution()', + 'pm\\.execution\\.setNextRequest\\(\'null\'\\)': 'bru.runner.stopExecution()', }; const extendedReplacements = Object.keys(replacements).reduce((acc, key) => { @@ -50,7 +54,7 @@ export const postmanTranslation = (script, logCallback) => { } if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) { modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1'); - logCallback?.(); + //logCallback?.(); } return modifiedScript; } catch (e) { diff --git a/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js b/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js index c1bbb44e7..92086c4b6 100644 --- a/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js +++ b/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js @@ -1,14 +1,16 @@ -const { workerData, parentPort } = require('worker_threads'); +const { parentPort } = require('worker_threads'); const { bruToJsonV2, } = require('@usebruno/lang'); -try { - const bru = workerData; - const json = bruToJsonV2(bru); - parentPort.postMessage(json); -} -catch(error) { - console.error(error); - parentPort.postMessage({ error: error?.message }); -} \ No newline at end of file +parentPort.on('message', (workerData) => { + try { + const bru = workerData; + const json = bruToJsonV2(bru); + parentPort.postMessage(json); + } + catch(error) { + console.error(error); + parentPort.postMessage({ error: error?.message }); + } +}); \ No newline at end of file diff --git a/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js b/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js index e08be60b9..c2a4f88e4 100644 --- a/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js +++ b/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js @@ -1,13 +1,16 @@ -const { workerData, parentPort } = require('worker_threads'); +const { parentPort } = require('worker_threads'); const { jsonToBruV2, } = require('@usebruno/lang'); -try { - const json = workerData; - const bru = jsonToBruV2(json); - parentPort.postMessage(bru); -} -catch(error) { - console.error(error); - parentPort.postMessage({ error: error?.message }); -} \ No newline at end of file + +parentPort.on('message', (workerData) => { + try { + const json = workerData; + const bru = jsonToBruV2(json); + parentPort.postMessage(bru); + } + catch(error) { + console.error(error); + parentPort.postMessage({ error: error?.message }); + } +}); \ No newline at end of file diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 4b6494b2f..522df6c68 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -30,9 +30,9 @@ const lastOpenedCollections = new LastOpenedCollections(); // Reference: https://content-security-policy.com/ const contentSecurityPolicy = [ "default-src 'self'", - "script-src * 'unsafe-inline' 'unsafe-eval'", - "connect-src * 'unsafe-inline'", + "connect-src 'self' https://*.posthog.com", "font-src 'self' https:", + "frame-src data:", // this has been commented out to make oauth2 work // "form-action 'none'", // we make an exception and allow http for images so that diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 07f15926f..43a3a1283 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -20,7 +20,6 @@ const { normalizeWslPath, normalizeAndResolvePath, safeToRename, - sanitizeCollectionName, isWindowsOS, isValidFilename, hasSubDirectories, @@ -41,9 +40,9 @@ const collectionSecurityStore = new CollectionSecurityStore(); const uiStateSnapshotStore = new UiStateSnapshotStore(); // size and file count limits to determine whether the bru files in the collection should be loaded asynchronously or not. -const MAX_COLLECTION_SIZE_IN_MB = 5; -const MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB = 2; -const MAX_COLLECTION_FILES_COUNT = 100; +const MAX_COLLECTION_SIZE_IN_MB = 20; +const MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB = 5; +const MAX_COLLECTION_FILES_COUNT = 2000; const envHasSecrets = (environment = {}) => { const secrets = _.filter(environment.variables, (v) => v.secret); @@ -76,7 +75,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection async (event, collectionName, collectionFolderName, collectionLocation) => { try { collectionFolderName = sanitizeDirectoryName(collectionFolderName); - collectionName = sanitizeCollectionName(collectionName); const dirPath = path.join(collectionLocation, collectionFolderName); if (fs.existsSync(dirPath)) { const files = fs.readdirSync(dirPath); @@ -118,7 +116,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle( 'renderer:clone-collection', async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => { - collectionFolderName = sanitizeCollectionName(collectionFolderName); + collectionFolderName = sanitizeDirectoryName(collectionFolderName); const dirPath = path.join(collectionLocation, collectionFolderName); if (fs.existsSync(dirPath)) { throw new Error(`collection: ${dirPath} already exists`); @@ -168,7 +166,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // rename collection ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => { try { - newName = sanitizeCollectionName(newName); const brunoJsonFilePath = path.join(collectionPathname, 'bruno.json'); const content = fs.readFileSync(brunoJsonFilePath, 'utf8'); const json = JSON.parse(content); @@ -519,9 +516,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); + ipcMain.handle('renderer:update-collection-paths', async (_, collectionPaths) => { + lastOpenedCollections.update(collectionPaths); + }) + ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => { try { - let collectionName = sanitizeCollectionName(collection.name); + let collectionName = sanitizeDirectoryName(collection.name); let collectionPath = path.join(collectionLocation, collectionName); if (fs.existsSync(collectionPath)) { diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 3418f4694..2271a3d33 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -384,8 +384,8 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) => // Parse the charset from content type: https://stackoverflow.com/a/33192813 const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || ''); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals - const charsetValue = charsetMatch?.[1] || 'utf-8'; - const dataBuffer = Buffer.isBuffer(response.data) ? response.data : Buffer.from(response.data); + const charsetValue = charsetMatch?.[1]; + const dataBuffer = Buffer.from(response.data); // Overwrite the original data for backwards compatibility let data; if (iconv.encodingExists(charsetValue)) { @@ -407,25 +407,6 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) => console.log('Failed to parse response data as JSON'); } - // Handle Buffer responses that contain JSON - if (Buffer.isBuffer(response.data)) { - try { - const decodedString = response.data.toString('utf-8'); - const parsedData = JSON.parse(decodedString); - - if (parsedData && parsedData.type === "Buffer" && Array.isArray(parsedData.data)) { - data = Buffer.from(parsedData.data).toString('utf-8'); - if (!disableParsingResponseJson) { - data = JSON.parse(data); - } - } else { - data = parsedData; - } - } catch { - console.error('Failed to parse Buffer data as JSON'); - } - } - return { data, dataBuffer }; }; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 2b46327af..0b8e300ed 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -65,7 +65,11 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc const contentType = getContentType(request.headers); - if (contentType.includes('json')) { + /* + We explicitly avoid interpolating buffer values because the file content is read as a buffer object in raw body mode. + Even if the selected file's content type is JSON, this prevents the buffer object from being interpolated. + */ + if (contentType.includes('json') && !Buffer.isBuffer(request.data)) { if (typeof request.data === 'string') { if (request.data.length) { request.data = _interpolate(request.data); diff --git a/packages/bruno-electron/src/store/last-opened-collections.js b/packages/bruno-electron/src/store/last-opened-collections.js index 546b73b57..72452eef3 100644 --- a/packages/bruno-electron/src/store/last-opened-collections.js +++ b/packages/bruno-electron/src/store/last-opened-collections.js @@ -16,18 +16,20 @@ class LastOpenedCollections { } add(collectionPath) { - const collections = this.store.get('lastOpenedCollections') || []; + const collections = this.getAll(); - if (isDirectory(collectionPath)) { - if (!collections.includes(collectionPath)) { - collections.push(collectionPath); - this.store.set('lastOpenedCollections', collections); - } + if (isDirectory(collectionPath) && !collections.includes(collectionPath)) { + collections.push(collectionPath); + this.store.set('lastOpenedCollections', collections); } } + update(collectionPaths) { + this.store.set('lastOpenedCollections', collectionPaths); + } + remove(collectionPath) { - let collections = this.store.get('lastOpenedCollections') || []; + let collections = this.getAll(); if (collections.includes(collectionPath)) { collections = _.filter(collections, (c) => c !== collectionPath); @@ -36,7 +38,7 @@ class LastOpenedCollections { } removeAll() { - return this.store.set('lastOpenedCollections', []); + this.store.set('lastOpenedCollections', []); } } diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index 0a849f055..a9c8597e7 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -154,10 +154,6 @@ const searchForBruFiles = (dir) => { return searchForFiles(dir, '.bru'); }; -const sanitizeCollectionName = (name) => { - return name.trim(); -} - const sanitizeDirectoryName = (name) => { return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-').trim(); }; @@ -267,7 +263,6 @@ module.exports = { searchForFiles, searchForBruFiles, sanitizeDirectoryName, - sanitizeCollectionName, isWindowsOS, safeToRename, isValidFilename, diff --git a/packages/bruno-electron/src/workers/index.js b/packages/bruno-electron/src/workers/index.js index 04836e9fc..d1d1a1b74 100644 --- a/packages/bruno-electron/src/workers/index.js +++ b/packages/bruno-electron/src/workers/index.js @@ -4,8 +4,18 @@ class WorkerQueue { constructor() { this.queue = []; this.isProcessing = false; + this.workers = {}; } + async getWorkerForScriptPath(scriptPath) { + if (!this.workers) this.workers = {}; + let worker = this.workers[scriptPath]; + if (!worker || worker.threadId === -1) { + this.workers[scriptPath] = worker = new Worker(scriptPath); + } + return worker; + } + async enqueue(task) { const { priority, scriptPath, data } = task; @@ -36,22 +46,20 @@ class WorkerQueue { } async runWorker({ scriptPath, data }) { - return new Promise((resolve, reject) => { - const worker = new Worker(scriptPath, { workerData: data }); + return new Promise(async (resolve, reject) => { + let worker = await this.getWorkerForScriptPath(scriptPath); + worker.postMessage(data); worker.on('message', (data) => { if (data?.error) { reject(new Error(data?.error)); } resolve(data); - worker.terminate(); }); worker.on('error', (error) => { reject(error); - worker.terminate(); }); worker.on('exit', (code) => { reject(new Error(`stopped with ${code} exit code`)); - worker.terminate(); }); }); } diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json index ad400ab58..1f1cee55a 100644 --- a/packages/bruno-js/package.json +++ b/packages/bruno-js/package.json @@ -25,6 +25,7 @@ "btoa": "^1.2.1", "chai": "^4.3.7", "chai-string": "^1.5.0", + "cheerio": "^1.0.0", "crypto-js": "^4.1.1", "json-query": "^2.2.2", "lodash": "^4.17.21", @@ -34,7 +35,8 @@ "node-vault": "^0.10.2", "path": "^0.12.7", "quickjs-emscripten": "^0.29.2", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "xml2js": "^0.6.2" }, "devDependencies": { "@rollup/plugin-commonjs": "^23.0.2", diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index dcde3f27c..758b574e8 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -28,6 +28,8 @@ const fetch = require('node-fetch'); const chai = require('chai'); const CryptoJS = require('crypto-js'); const NodeVault = require('node-vault'); +const xml2js = require('xml2js'); +const cheerio = require('cheerio'); const { executeQuickJsVmAsync } = require('../sandbox/quickjs'); class ScriptRuntime { @@ -145,6 +147,8 @@ class ScriptRuntime { chai, 'node-fetch': fetch, 'crypto-js': CryptoJS, + 'xml2js': xml2js, + cheerio, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, 'node-vault': NodeVault diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index 71db9d83e..e2d1f4865 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -30,6 +30,8 @@ const axios = require('axios'); const fetch = require('node-fetch'); const CryptoJS = require('crypto-js'); const NodeVault = require('node-vault'); +const xml2js = require('xml2js'); +const cheerio = require('cheerio'); const { executeQuickJsVmAsync } = require('../sandbox/quickjs'); const getResultsSummary = (results) => { @@ -205,6 +207,8 @@ class TestRuntime { chai, 'node-fetch': fetch, 'crypto-js': CryptoJS, + 'xml2js': xml2js, + cheerio, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, 'node-vault': NodeVault diff --git a/packages/bruno-tests/collection/ping-another-one.bru b/packages/bruno-tests/collection/ping-another-one.bru deleted file mode 100644 index 84c1412a8..000000000 --- a/packages/bruno-tests/collection/ping-another-one.bru +++ /dev/null @@ -1,15 +0,0 @@ -meta { - name: ping-another-one - type: http - seq: 2 -} - -get { - url: {{host}}/ping - body: none - auth: none -} - -script:pre-request { - throw new Error('this should not execute in a collection run'); -} diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/cheerio/cheerio.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/cheerio/cheerio.bru new file mode 100644 index 000000000..07aad76b2 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/cheerio/cheerio.bru @@ -0,0 +1,42 @@ +meta { + name: cheerio + type: http + seq: 1 +} + +post { + url: https://echo.usebruno.com + body: text + auth: none +} + +body:text { +

Hello Bruno!

+} + +script:pre-request { + const cheerio = require('cheerio'); + + const $ = cheerio.load('

Hello world

'); + + $('h2.title').text('Hello there!'); + $('h2').addClass('welcome'); + + bru.setVar("cheerio-test-html", $.html()); +} + +tests { + const cheerio = require('cheerio'); + + test("cheerio html - from scripts", function() { + const expected = '

Hello there!

'; + const html = bru.getVar('cheerio-test-html'); + expect(html).to.eql(expected); + }); + + test("cheerio html - from tests", function() { + const expected = '

Hello Bruno!

'; + const $ = cheerio.load(res.body); + expect($.html()).to.eql(expected); + }); +} diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/xml2js/xml2js.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/xml2js/xml2js.bru new file mode 100644 index 000000000..db8748ec3 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/xml2js/xml2js.bru @@ -0,0 +1,41 @@ +meta { + name: xml2js + type: http + seq: 1 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +script:pre-request { + var parseString = require('xml2js').parseString; + var xml = "Hello xml2js!" + parseString(xml, function (err, result) { + bru.setVar("xml2js-test-result", result); + }); +} + +tests { + var parseString = require('xml2js').parseString; + + test("xml2js parseString in scripts", function() { + const expected = { + root: 'Hello xml2js!' + }; + const result = bru.getVar('xml2js-test-result'); + expect(result).to.eql(expected); + }); + + test("xml2js parseString in tests", async function() { + var xml = "Hello inside test!" + const expected = { + root: 'Hello inside test!' + }; + parseString(xml, function (err, result) { + expect(result).to.eql(expected); + }); + }); +} diff --git a/scripts/build-electron.js b/scripts/build-electron.js index 9825c3a09..ab44dcbdf 100644 --- a/scripts/build-electron.js +++ b/scripts/build-electron.js @@ -78,14 +78,14 @@ async function main() { console.log('The directory has been created successfully!'); // Copy build - await copyFolderIfExists('packages/bruno-app/out', 'packages/bruno-electron/web'); + await copyFolderIfExists('packages/bruno-app/dist', 'packages/bruno-electron/web'); // Change paths in next const files = await fs.readdir('packages/bruno-electron/web'); for (const file of files) { if (file.endsWith('.html')) { let content = await fs.readFile(`packages/bruno-electron/web/${file}`, 'utf8'); - content = content.replace(/\/_next\//g, '_next/'); + content = content.replace(/\/static/g, './static'); await fs.writeFile(`packages/bruno-electron/web/${file}`, content); } }