diff --git a/e2e-tests/001-sanity-tests/001-home-screen.spec.ts b/e2e-tests/001-sanity-tests/001-home-screen.spec.ts
index d993fb7bc..326ff895c 100644
--- a/e2e-tests/001-sanity-tests/001-home-screen.spec.ts
+++ b/e2e-tests/001-sanity-tests/001-home-screen.spec.ts
@@ -2,4 +2,4 @@ import { test, expect } from '../../playwright';
test('Check if the logo on top left is visible', async ({ page }) => {
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
-});
\ No newline at end of file
+});
diff --git a/e2e-tests/002-cookies-tests/001-cookie-persistence.spec.ts b/e2e-tests/002-cookies-tests/001-cookie-persistence.spec.ts
new file mode 100644
index 000000000..beefb3b39
--- /dev/null
+++ b/e2e-tests/002-cookies-tests/001-cookie-persistence.spec.ts
@@ -0,0 +1,43 @@
+import { test, expect } from '../../playwright';
+
+test('should persist cookies across app restarts', async ({ createTmpDir, launchElectronApp }) => {
+ // Create a temporary user-data directory so we control where the cookies store file is written.
+ const userDataPath = await createTmpDir('cookie-persistence');
+
+ const app1 = await launchElectronApp({ userDataPath });
+ const page1 = await app1.firstWindow();
+ await page1.waitForSelector('[data-trigger="cookies"]');
+
+ // Open Cookies modal via the status-bar button.
+ await page1.click('[data-trigger="cookies"]');
+
+ // When no cookies are present the modal shows a centred "Add Cookie" button.
+ await page1.getByRole('button', { name: /Add Cookie/i }).click();
+
+ // Fill out the form.
+ await page1.fill('input[name="domain"]', 'example.com');
+ await page1.fill('input[name="path"]', '/');
+ await page1.fill('input[name="key"]', 'session');
+ await page1.fill('input[name="value"]', 'abc123');
+ await page1.check('input[name="secure"]');
+ await page1.check('input[name="httpOnly"]');
+
+ await page1.getByRole('button', { name: 'Save' }).click();
+
+ await expect(page1.getByText('example.com')).toBeVisible();
+
+ await app1.close();
+
+ // Second launch – verify the cookie was persisted and re-loaded
+ const app2 = await launchElectronApp({ userDataPath });
+ const page2 = await app2.firstWindow();
+
+ // Open the Cookies modal again.
+ await page2.waitForSelector('[data-trigger="cookies"]');
+ await page2.click('[data-trigger="cookies"]');
+
+ // The domain we added earlier should still be present.
+ await expect(page2.getByText('example.com')).toBeVisible();
+
+ await app2.close();
+});
diff --git a/e2e-tests/002-cookies-tests/002-corrupted-passkey.spec.ts b/e2e-tests/002-cookies-tests/002-corrupted-passkey.spec.ts
new file mode 100644
index 000000000..02c2a5010
--- /dev/null
+++ b/e2e-tests/002-cookies-tests/002-corrupted-passkey.spec.ts
@@ -0,0 +1,47 @@
+import { test, expect } from '../../playwright';
+import * as path from 'path';
+import * as fs from 'fs/promises';
+
+test('should handle corrupted passkey and still display saved cookie list', async ({ createTmpDir, launchElectronApp }) => {
+ const userDataPath = await createTmpDir('corrupted-passkey');
+
+ const app1 = await launchElectronApp({ userDataPath });
+ // 1. First run – add a cookie via the UI so `cookies.json` is created.
+ const page1 = await app1.firstWindow();
+
+ await page1.waitForSelector('[data-trigger="cookies"]');
+ await page1.click('[data-trigger="cookies"]');
+ await page1.getByRole('button', { name: /Add Cookie/i }).click();
+
+ await page1.fill('input[name="domain"]', 'example.com');
+ await page1.fill('input[name="path"]', '/');
+ await page1.fill('input[name="key"]', 'session');
+ await page1.fill('input[name="value"]', 'abc123');
+ await page1.check('input[name="secure"]');
+ await page1.check('input[name="httpOnly"]');
+
+ await page1.getByRole('button', { name: 'Save' }).click();
+
+ await expect(page1.getByText('example.com')).toBeVisible();
+
+ await app1.close();
+
+ // 2. Corrupt the encryptedPasskey in cookies.json
+ const cookiesFilePath = path.join(userDataPath, 'cookies.json');
+ const raw = await fs.readFile(cookiesFilePath, 'utf-8');
+ const cookiesJson = JSON.parse(raw);
+ cookiesJson.encryptedPasskey = 'deadbeef'; // clearly invalid value
+ await fs.writeFile(cookiesFilePath, JSON.stringify(cookiesJson, null, 2));
+
+ // 3. Second run – Bruno should recover and still list the cookie domain
+ const app2 = await launchElectronApp({ userDataPath });
+ const page2 = await app2.firstWindow();
+
+ await page2.waitForSelector('[data-trigger="cookies"]');
+ await page2.click('[data-trigger="cookies"]');
+
+ // The domain row should still be visible (even if cookie values are blank).
+ await expect(page2.getByText('example.com')).toBeVisible();
+
+ await app2.close();
+});
diff --git a/e2e-tests/persistent-env-tests/001-persistent-env-test.spec.ts b/e2e-tests/persistent-env-tests/001-persistent-env-test.spec.ts
new file mode 100644
index 000000000..e319e178f
--- /dev/null
+++ b/e2e-tests/persistent-env-tests/001-persistent-env-test.spec.ts
@@ -0,0 +1,33 @@
+import { test, expect } from '../../playwright';
+
+test.describe.serial('Persistent Environment Test', () => {
+ test.setTimeout(2 * 10 * 1000);
+
+ test('add env using script', async ({ pageWithUserData: page, restartApp }) => {
+ await page.locator('#sidebar-collection-name').click();
+ await page.getByRole('button', { name: 'Save' }).click();
+ await page.getByText('ping', { exact: true }).click();
+ await page.getByText('No Environment').click();
+ await page.getByRole('tooltip').locator('div').filter({ hasText: 'Env' }).nth(3).click();
+ await page.locator('#send-request').getByRole('img').nth(2).click();
+ await page.waitForTimeout(1000);
+ await page.locator('div').filter({ hasText: /^Env$/ }).nth(3).click();
+ await page.getByText('Configure', { exact: true }).click();
+ await expect(page.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
+ await page.getByText('×').click();
+
+ const newApp = await restartApp();
+ const newPage = await newApp.firstWindow();
+ await newPage.locator('#sidebar-collection-name').click();
+ await newPage.getByRole('button', { name: 'Save' }).click();
+ await newPage.getByText('ping', { exact: true }).click();
+ await newPage.getByText('No Environment').click();
+ await newPage.getByRole('tooltip').locator('div').filter({ hasText: 'Env' }).nth(3).click();
+ await newPage.locator('div').filter({ hasText: /^Env$/ }).nth(3).click();
+ await newPage.getByText('Configure', { exact: true }).click();
+ await expect(newPage.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
+ await newPage.getByText('×').click();
+ await newPage.waitForTimeout(1000);
+ await newPage.close();
+ });
+});
diff --git a/e2e-tests/persistent-env-tests/002-env-test-without-persistence.spec.ts b/e2e-tests/persistent-env-tests/002-env-test-without-persistence.spec.ts
new file mode 100644
index 000000000..4be151bb0
--- /dev/null
+++ b/e2e-tests/persistent-env-tests/002-env-test-without-persistence.spec.ts
@@ -0,0 +1,40 @@
+import { test, expect } from '../../playwright';
+
+test.describe.serial('Persistent Environment Test', () => {
+ test.setTimeout(2 * 10 * 1000);
+
+ test('add env using script', async ({ pageWithUserData: page, restartApp }) => {
+ await page.locator('#sidebar-collection-name').click();
+ await page.getByText('ping2', { exact: true }).click();
+ await page.getByText('Env', { exact: true }).click();
+ await page.getByText('Stage', { exact: true }).click();
+ await page.locator('#send-request').getByRole('img').nth(2).click();
+ await page.waitForTimeout(1000);
+ await page
+ .locator('div')
+ .filter({ hasText: /^Stage$/ })
+ .nth(3)
+ .click();
+ await page.getByText('Configure', { exact: true }).click();
+ await expect(page.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
+ await page.getByText('×').click();
+
+ const newApp = await restartApp();
+ const newPage = await newApp.firstWindow();
+ await newPage.locator('#sidebar-collection-name').click();
+ await newPage.getByRole('button', { name: 'Save' }).click();
+ await newPage.getByText('ping2', { exact: true }).click();
+ await newPage.getByText('No Environment').click();
+ await newPage.getByText('Stage').click();
+ await newPage
+ .locator('div')
+ .filter({ hasText: /^Stage$/ })
+ .nth(3)
+ .click();
+ await newPage.getByText('Configure', { exact: true }).click();
+ await expect(newPage.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).not.toBeVisible();
+ await newPage.getByText('×').click();
+ await newPage.waitForTimeout(1000);
+ await newPage.close();
+ });
+});
diff --git a/e2e-tests/persistent-env-tests/collection/bruno.json b/e2e-tests/persistent-env-tests/collection/bruno.json
new file mode 100644
index 000000000..fa729847c
--- /dev/null
+++ b/e2e-tests/persistent-env-tests/collection/bruno.json
@@ -0,0 +1,5 @@
+{
+ "version": "1",
+ "name": "collection",
+ "type": "collection"
+}
\ No newline at end of file
diff --git a/e2e-tests/persistent-env-tests/collection/collection.bru b/e2e-tests/persistent-env-tests/collection/collection.bru
new file mode 100644
index 000000000..e69de29bb
diff --git a/e2e-tests/persistent-env-tests/collection/environments/Env.bru b/e2e-tests/persistent-env-tests/collection/environments/Env.bru
new file mode 100644
index 000000000..909243fd2
--- /dev/null
+++ b/e2e-tests/persistent-env-tests/collection/environments/Env.bru
@@ -0,0 +1,4 @@
+vars {
+ host: https://testbench-sanity.usebruno.com
+ persistent-env-test: persistent-env-test-value
+}
diff --git a/e2e-tests/persistent-env-tests/collection/environments/Stage.bru b/e2e-tests/persistent-env-tests/collection/environments/Stage.bru
new file mode 100644
index 000000000..0b756aa68
--- /dev/null
+++ b/e2e-tests/persistent-env-tests/collection/environments/Stage.bru
@@ -0,0 +1,3 @@
+vars {
+ host: https://testbench-sanity.usebruno.com
+}
\ No newline at end of file
diff --git a/e2e-tests/persistent-env-tests/collection/persist-env-request.bru b/e2e-tests/persistent-env-tests/collection/persist-env-request.bru
new file mode 100644
index 000000000..eefb4e827
--- /dev/null
+++ b/e2e-tests/persistent-env-tests/collection/persist-env-request.bru
@@ -0,0 +1,15 @@
+meta {
+ name: ping2
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ bru.setEnvVar("persistent-env-test", "persistent-env-test-value");
+}
\ No newline at end of file
diff --git a/e2e-tests/persistent-env-tests/collection/request.bru b/e2e-tests/persistent-env-tests/collection/request.bru
new file mode 100644
index 000000000..9ae6899c5
--- /dev/null
+++ b/e2e-tests/persistent-env-tests/collection/request.bru
@@ -0,0 +1,15 @@
+meta {
+ name: ping
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ bru.setEnvVar("persistent-env-test", "persistent-env-test-value", { persist: true });
+}
\ No newline at end of file
diff --git a/e2e-tests/persistent-env-tests/init-user-data/preferences.json b/e2e-tests/persistent-env-tests/init-user-data/preferences.json
new file mode 100644
index 000000000..f9c1fdc7e
--- /dev/null
+++ b/e2e-tests/persistent-env-tests/init-user-data/preferences.json
@@ -0,0 +1,6 @@
+{
+ "maximized": true,
+ "lastOpenedCollections": [
+ "{{projectRoot}}/e2e-tests/persistent-env-tests/collection"
+ ]
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index fcef3b196..da15f2bd9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,6 +25,7 @@
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.51.1",
+ "@rollup/plugin-json": "^6.1.0",
"@types/jest": "^29.5.11",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1",
@@ -4490,6 +4491,37 @@
}
}
},
+ "node_modules/@grpc/grpc-js": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
+ "integrity": "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/proto-loader": "^0.7.13",
+ "@js-sdsl/ordered-map": "^4.4.2"
+ },
+ "engines": {
+ "node": ">=12.10.0"
+ }
+ },
+ "node_modules/@grpc/proto-loader": {
+ "version": "0.7.15",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
+ "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash.camelcase": "^4.3.0",
+ "long": "^5.0.0",
+ "protobufjs": "^7.2.5",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@headlessui/react": {
"version": "1.7.19",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz",
@@ -5274,6 +5306,16 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@js-sdsl/ordered-map": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
+ "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/js-sdsl"
+ }
+ },
"node_modules/@jsep-plugin/assignment": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz",
@@ -6360,6 +6402,70 @@
"node": ">=16.9"
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
@@ -7053,6 +7159,27 @@
}
}
},
+ "node_modules/@rollup/plugin-json": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
+ "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
@@ -8393,6 +8520,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/google-protobuf": {
+ "version": "3.15.12",
+ "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
+ "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
+ "license": "MIT"
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -8543,6 +8676,15 @@
"@types/lodash": "*"
}
},
+ "node_modules/@types/lodash.set": {
+ "version": "4.3.9",
+ "resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.9.tgz",
+ "integrity": "sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
"node_modules/@types/markdown-it": {
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
@@ -8572,7 +8714,6 @@
"version": "22.15.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -10646,6 +10787,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/builtin-modules": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz",
+ "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/builtin-status-codes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
@@ -13723,15 +13877,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/emitter-component": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz",
- "integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==",
- "dev": true,
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/emittery": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
@@ -15928,6 +16073,12 @@
"csstype": "^3.0.10"
}
},
+ "node_modules/google-protobuf": {
+ "version": "3.21.4",
+ "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz",
+ "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==",
+ "license": "(BSD-3-Clause AND Apache-2.0)"
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -16042,6 +16193,22 @@
"node": ">= 6"
}
},
+ "node_modules/grpc-reflection-js": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/grpc-reflection-js/-/grpc-reflection-js-0.3.0.tgz",
+ "integrity": "sha512-3lhTlQluPxVgbowCXA3tAZC3RJW+GSOUkguLNYl1QffYRiutUB3RDfPkQFTcrCFJgNiIIxx+iJkr8s3uSp3zWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/google-protobuf": "^3.7.2",
+ "@types/lodash.set": "^4.3.6",
+ "google-protobuf": "^3.12.2",
+ "lodash.set": "^4.3.2",
+ "protobufjs": "^7.2.2"
+ },
+ "peerDependencies": {
+ "@grpc/grpc-js": "^1.0.0"
+ }
+ },
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -16144,7 +16311,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -19498,7 +19664,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/lodash.curry": {
@@ -19576,6 +19741,12 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
+ "node_modules/lodash.set": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
+ "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==",
+ "license": "MIT"
+ },
"node_modules/lodash.uniq": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -19617,6 +19788,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -22958,6 +23135,30 @@
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
"license": "MIT"
},
+ "node_modules/protobufjs": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
+ "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -25818,16 +26019,6 @@
"node": ">= 0.4"
}
},
- "node_modules/stream": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz",
- "integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emitter-component": "^1.1.1"
- }
- },
"node_modules/stream-browserify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
@@ -26932,6 +27123,22 @@
"@popperjs/core": "^2.9.0"
}
},
+ "node_modules/tldts": {
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.12.tgz",
+ "integrity": "sha512-M9ZQBPp6FyqhMcl233vHYyYRkxXOA1SKGlnq13S0mJdUhRSwr2w6I8rlchPL73wBwRlyIZpFvpu2VcdSMWLYXw==",
+ "dependencies": {
+ "tldts-core": "^7.0.12"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.12.tgz",
+ "integrity": "sha512-3K76aXywJFduGRsOYoY5JzINLs/WMlOkeDwPL+8OCPq2Rh39gkSDtWAxdJQlWjpun/xF/LHf29yqCi6VC/rHDA=="
+ },
"node_modules/tmp": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
@@ -27261,7 +27468,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -31097,6 +31303,9 @@
"name": "@usebruno/common",
"version": "0.1.0",
"license": "MIT",
+ "dependencies": {
+ "tough-cookie": "^6.0.0"
+ },
"devDependencies": {
"@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.27.0",
@@ -31663,6 +31872,17 @@
"dev": true,
"license": "MIT"
},
+ "packages/bruno-common/node_modules/tough-cookie": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
+ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"packages/bruno-common/node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@@ -31790,6 +32010,8 @@
"version": "2.0.0",
"dependencies": {
"@aws-sdk/credential-providers": "3.750.0",
+ "@grpc/grpc-js": "^1.13.2",
+ "@grpc/proto-loader": "^0.7.13",
"@usebruno/common": "0.1.0",
"@usebruno/converters": "^0.1.0",
"@usebruno/filestore": "^0.1.0",
@@ -31825,6 +32047,7 @@
"nanoid": "3.3.8",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
+ "tough-cookie": "^6.0.0",
"uuid": "^9.0.0",
"yup": "^0.32.11"
},
@@ -32907,6 +33130,17 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "packages/bruno-electron/node_modules/tough-cookie": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
+ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"packages/bruno-filestore": {
"name": "@usebruno/filestore",
"version": "0.1.0",
@@ -33098,9 +33332,7 @@
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"rollup": "3.29.5",
- "rollup-plugin-terser": "^7.0.2",
- "stream": "^0.0.2",
- "util": "^0.12.5"
+ "rollup-plugin-terser": "^7.0.2"
},
"peerDependencies": {
"@usebruno/vm2": "^3.9.13"
@@ -33166,17 +33398,24 @@
"version": "0.1.0",
"license": "MIT",
"dependencies": {
+ "@faker-js/faker": "^9.7.0",
+ "@grpc/grpc-js": "^1.13.3",
+ "@grpc/proto-loader": "^0.7.15",
"@types/qs": "^6.9.18",
- "axios": "^1.9.0"
+ "axios": "^1.9.0",
+ "grpc-reflection-js": "^0.3.0"
},
"devDependencies": {
"@babel/preset-env": "^7.22.0",
"@babel/preset-typescript": "^7.22.0",
+ "@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^23.0.2",
+ "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"@types/jest": "^29.5.11",
"babel-jest": "^29.7.0",
+ "builtin-modules": "^5.0.0",
"jest": "^29.2.0",
"rollup": "3.29.5",
"rollup-plugin-dts": "^5.0.0",
@@ -33185,6 +33424,22 @@
"typescript": "^4.8.4"
}
},
+ "packages/bruno-requests/node_modules/@faker-js/faker": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz",
+ "integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fakerjs"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=9.0.0"
+ }
+ },
"packages/bruno-requests/node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
diff --git a/package.json b/package.json
index a71957ed1..aa01a08f2 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.51.1",
+ "@rollup/plugin-json": "^6.1.0",
"@types/jest": "^29.5.11",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1",
diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js
index 621b2b86b..affe1f7db 100644
--- a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js
@@ -38,6 +38,48 @@ const StyledWrapper = styled.div`
outline: none !important;
}
}
+
+ .protocol-placeholder {
+ height: 100%;
+ position: relative;
+ display: inline-block;
+ width: 60px;
+ overflow: hidden;
+ }
+
+ .protocol-https,
+ .protocol-grpcs {
+ position: absolute;
+ right: 8px;
+ top: 0;
+ bottom: 0;
+ transition: transform 0.3s ease-in-out;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .protocol-https {
+ animation: slideUpDown 6s infinite;
+ transform: translateY(0);
+ }
+
+ .protocol-grpcs {
+ animation: slideUpDown 6s infinite 3s;
+ transform: translateY(100%);
+ }
+
+ @keyframes slideUpDown {
+ 0%, 45% {
+ transform: translateY(0);
+ }
+ 50%, 95% {
+ transform: translateY(100%);
+ }
+ 100% {
+ transform: translateY(0);
+ }
+ }
`;
export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js
index 7ae9ac85e..3f62fc23e 100644
--- a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js
@@ -132,7 +132,10 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
- https://
+
+ https://
+ grpcs://
+
props.theme.requestTabPanel.url.bg};
+
+ button.remove-certificate {
+ color: ${(props) => props.theme.colors.text.danger};
+ }
+ }
+`;
+
+export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/CollectionSettings/Grpc/index.js b/packages/bruno-app/src/components/CollectionSettings/Grpc/index.js
new file mode 100644
index 000000000..db2313efd
--- /dev/null
+++ b/packages/bruno-app/src/components/CollectionSettings/Grpc/index.js
@@ -0,0 +1,263 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { useFormik } from 'formik';
+import { useDispatch } from 'react-redux';
+import StyledWrapper from './StyledWrapper';
+import toast from 'react-hot-toast';
+import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
+import cloneDeep from 'lodash/cloneDeep';
+import { IconTrash, IconFile, IconFileImport, IconAlertCircle } from '@tabler/icons';
+import { getRelativePath, getBasename, getDirPath } from 'utils/common/path';
+import { Tooltip } from 'react-tooltip';
+import { existsSync, resolvePath } from '../../../utils/filesystem';
+
+const GrpcSettings = ({ collection }) => {
+ const dispatch = useDispatch();
+ const {
+ brunoConfig: { grpc: grpcConfig = {} }
+ } = collection;
+
+ const fileInputRef = useRef(null);
+ const [protoFileValidity, setProtoFileValidity] = useState({});
+
+ const formik = useFormik({
+ enableReinitialize: true,
+ initialValues: {
+ protoFiles: grpcConfig.protoFiles || []
+ },
+ onSubmit: (newGrpcConfig) => {
+ const brunoConfig = cloneDeep(collection.brunoConfig);
+ brunoConfig.grpc = newGrpcConfig;
+ dispatch(updateBrunoConfig(brunoConfig, collection.uid));
+ toast.success('gRPC settings updated');
+ }
+ });
+
+ // Get file path using the ipcRenderer
+ const getProtoFile = (event) => {
+ const files = event?.files;
+ if (files && files.length > 0) {
+ const newProtoFiles = [...formik.values.protoFiles];
+
+ for (let i = 0; i < files.length; i++) {
+ const filePath = window?.ipcRenderer?.getFilePath(files[i]);
+ if (filePath) {
+ const relativePath = getRelativePath(filePath, collection.pathname);
+ const protoFileObj = {
+ path: relativePath,
+ type: 'file'
+ };
+
+ // Check if this path already exists
+ const exists = newProtoFiles.some(pf => pf.path === protoFileObj.path);
+ if (!exists) {
+ newProtoFiles.push(protoFileObj);
+ }
+ }
+ }
+
+ formik.setFieldValue('protoFiles', newProtoFiles);
+ // Reset the file input
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ }
+ };
+
+ // Handler for removing a proto file
+ const handleRemoveProtoFile = (index) => {
+ const updatedProtoFiles = [...formik.values.protoFiles];
+ updatedProtoFiles.splice(index, 1);
+ formik.setFieldValue('protoFiles', updatedProtoFiles);
+ };
+
+ // Handle the browse button click
+ const handleBrowseClick = () => {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ };
+
+ // Check if a proto file path is valid
+ const isProtoFileValid = async (protoFile) => {
+ try {
+ const absolutePath = await resolvePath(protoFile.path, collection.pathname);
+ return await existsSync(absolutePath);
+ } catch (error) {
+ return false;
+ }
+ };
+
+ // Validate all proto files and update state
+ useEffect(() => {
+ const validateProtoFiles = async () => {
+ const validityMap = {};
+ for (const file of formik.values.protoFiles) {
+ validityMap[file.path] = await isProtoFileValid(file);
+ }
+ setProtoFileValidity(validityMap);
+ };
+
+ validateProtoFiles();
+ }, [formik.values.protoFiles, collection.pathname]);
+
+ // Handle replacing an invalid proto file
+ const handleReplaceProtoFile = (index) => {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ // Store the index to replace after file selection
+ fileInputRef.current.dataset.replaceIndex = index;
+ }
+ };
+
+ // Handle file input change
+ const handleFileInputChange = (e) => {
+ const replaceIndex = e.target.dataset.replaceIndex;
+ if (replaceIndex !== undefined) {
+ // Handle replacement
+ const files = e.target.files;
+ if (files && files.length > 0) {
+ const filePath = window?.ipcRenderer?.getFilePath(files[0]);
+ if (filePath) {
+ const relativePath = getRelativePath(filePath, collection.pathname);
+ const updatedProtoFiles = [...formik.values.protoFiles];
+ updatedProtoFiles[replaceIndex] = {
+ path: relativePath,
+ type: 'file'
+ };
+ formik.setFieldValue('protoFiles', updatedProtoFiles);
+ }
+ }
+ delete e.target.dataset.replaceIndex;
+ } else {
+ getProtoFile(e.target);
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default GrpcSettings;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/CollectionSettings/Presets/index.js b/packages/bruno-app/src/components/CollectionSettings/Presets/index.js
index e16884e16..8683fa4f9 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Presets/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Presets/index.js
@@ -1,13 +1,15 @@
-import React, { useEffect } from 'react';
+import React from 'react';
import { useFormik } from 'formik';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import cloneDeep from 'lodash/cloneDeep';
+import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
const PresetsSettings = ({ collection }) => {
const dispatch = useDispatch();
+ const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
const {
brunoConfig: { presets: presets = {} }
} = collection;
@@ -15,10 +17,15 @@ const PresetsSettings = ({ collection }) => {
const formik = useFormik({
enableReinitialize: true,
initialValues: {
- requestType: presets.requestType || 'http',
+ requestType: presets.requestType === 'grpc' && !isGrpcEnabled ? 'http' : presets.requestType || 'http',
requestUrl: presets.requestUrl || ''
},
onSubmit: (newPresets) => {
+ // If gRPC is disabled but the preset is set to grpc, change it to http
+ if (!isGrpcEnabled && newPresets.requestType === 'grpc') {
+ newPresets.requestType = 'http';
+ }
+
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.presets = newPresets;
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
@@ -62,6 +69,23 @@ const PresetsSettings = ({ collection }) => {
+
+ {isGrpcEnabled && (
+ <>
+
+
+ >
+ )}
@@ -74,7 +98,7 @@ const PresetsSettings = ({ collection }) => {
id="request-url"
type="text"
name="requestUrl"
- placeholder='Request URL'
+ placeholder="Request URL"
className="block textbox"
autoComplete="off"
autoCorrect="off"
@@ -87,6 +111,7 @@ const PresetsSettings = ({ collection }) => {
+