Compare commits

...

229 Commits

Author SHA1 Message Date
dependabot[bot]
8f37eb2d1f chore(deps): bump actions/github-script from 7 to 9
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 17:34:29 +00:00
lohit
cd06f28430 fix: rename signatureEncoding to signatureMethod for OAuth1 in opencollection types (#7724)
Align with @opencollection/types 0.9.1 which renamed the OAuth1 field from
signatureEncoding to signatureMethod. Update converters, filestore, and all
YML test fixtures. Increase OAuth1 UI test timeouts from 30s to 60s.
2026-04-09 23:36:39 +05:30
Sid
3b502fd63d give sid-bruno some nice privileges (#7702) 2026-04-08 11:36:18 +05:30
naman-bruno
d4cd34fc50 fix: fix scroll in querybar component (#7700) 2026-04-07 19:25:51 +05:30
Pragadesh-45
58942b383d Feat: Support PAC file upload (#7651)
* Add proxy .pac file resolver

chore(dependencies): update package-lock.json with new dependencies and version upgrades

- Added new dependencies: ajv, git-url-parse, @opencollection/types, and storybook packages.
- Updated existing dependencies to their latest versions, including eslint and babel packages.
- Removed deprecated entries and cleaned up the package-lock structure for better maintainability.

* tests

* wip

* wip

* wip

* wip

* feat: file upload .pac

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* feat: Refactor proxy settings to use a new structure for PAC configuration. Introduced 'source' field to determine proxy type (manual or PAC) and updated related validation and state management. Removed deprecated 'pacUrl' field in favor of 'pac.source'. Updated preferences schema and test data accordingly.

* fix: Update proxy settings to correctly reference 'source' field for PAC configuration. Adjusted state management and validation logic to align with new structure. Enhanced tests for backward compatibility and new format handling.

* feat: Enhance proxy configuration by adding 'proxyModeReason' to provide context for proxy settings. Updated related functions to accommodate the new parameter and improved logging for proxy mode changes.

* wip

* refactor: Update proxy settings to remove 'inherit' field and replace it with 'source' for better clarity. Adjusted validation schema, default preferences, and migration logic to align with the new structure. Enhanced tests to ensure compatibility with the updated proxy configuration.

* wip

* wip

* wip

* wip

* wip

* chore: consistent path check

* chore: consistency

* tests(pac): fix unit params

---------

Co-authored-by: Gianluca D'Abrosca <gianluca.dabrosca.1999@gmail.com>
Co-authored-by: Sid <siddharth@usebruno.com>
2026-04-07 17:00:32 +05:30
gopu-bruno
476d30a49e feat: replace send button with Send/Cancel buttons on request url (#7675)
* feat: replace request send icon with Send/Cancel buttons

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
2026-04-07 13:42:09 +05:30
Pooja
4d6032ba0d fix: timeline url race condition (#7154)
* fix: timeline url race condition

* add: requestSent in catch block

* add: requestSent in catch
2026-04-06 17:09:37 +05:30
Abhishek S Lal
fabba4d296 fix: resolve process.env variables in global environment level (#7600)
* feat: enhance environment variable resolution in EnvironmentVariablesTable

- Added logic to populate process environment variables from the active workspace when no collection is selected, allowing for proper resolution of {{process.env.X}}.
- Updated workspace actions to map scratch collections to their respective workspaces, ensuring that environment variables can be resolved correctly.

This improves the user experience by providing access to workspace-specific environment variables in the environment variables table.

* refactor: improve state selection and add test IDs for better testing

- Refactored the state selection logic in EnvironmentVariablesTable for clarity.
- Added data-testid attributes to various components including CollapsibleSection, DotEnvFileDetails, DotEnvRawView, and EnvironmentList to enhance testability.
- This change aims to streamline component interactions and facilitate easier testing.

* feat: add test IDs to EnvironmentList for improved testability

- Introduced data-testid attributes to the EnvironmentList component, enhancing the ability to target elements in tests.
- This update includes test IDs for the CollapsibleSection, create .env file button, and the input field for the .env name, facilitating better integration with testing frameworks.

* refactor: simplify global environment test setup

- Removed unnecessary timeout setting and afterEach cleanup logic from the global environment process.env resolution test.
- This change streamlines the test structure, focusing on the core functionality being tested.
2026-04-04 14:57:50 +05:30
Chirag Chandrashekhar
c273c10f0c fix: add uuid v7 support in pre-request scripts (#7377) 2026-04-03 17:20:23 +05:30
Chirag Chandrashekhar
073b1ef036 fix: validate environment variables in unsaved changes dialog (#7403) 2026-04-03 17:05:05 +05:30
Abhishek S Lal
5db34dff11 fix: allow __Host- prefixed cookies to be stored via script API (#7549)
* fix: allow __Host- prefixed cookies to be stored via script API (#7452)

* refactor: rename URL constant to TEST_URL for clarity in cookie tests
2026-04-03 15:03:14 +05:30
Chirag Chandrashekhar
233013df20 fix: clear inherited DNS lookup for non-localhost URLs in redirect handling (#7426) 2026-04-03 14:44:55 +05:30
Pooja
5086ac4b8c fix: graphql doc close button (#7667)
* fix: graphql doc close button

* fix

* fix
2026-04-03 13:57:39 +05:30
Bijin A B
f112c4fdd8 fix: sample collection creation race condition (#7665) 2026-04-03 13:55:12 +05:30
Chirag Chandrashekhar
5e1a36f8c8 fix: close previous SSE connection before sending new request (#7474)
* fix: update system proxy fetching to use finally (#7652)

* fix: update system proxy fetching to use finally for improved reliability

* Update packages/bruno-electron/src/index.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: allow file selection in multipart form without entering a key first (#7640)

* fix: close previous SSE connection before sending new request

When resending an SSE (Server-Sent Events) request using Cmd+Enter,
the previous connection was not being closed, causing connection leaks.

Changes:
- Add SSE cancellation logic to sendRequest action - checks for running
  stream and cancels it before sending new request
- Add return to cancelRequest action to make it properly chainable
- Simplify RequestTabPanel by removing duplicate cancel logic (now
  handled centrally in sendRequest)
- Add SSE endpoints to test server for e2e testing
- Add Playwright e2e test to verify SSE connection cancellation

* fix: address PR review feedback for SSE connection cancellation

- Use platform-aware modifier (Meta on macOS, Control on Linux/Windows)
  instead of hardcoded Meta+Enter for cross-platform CI compatibility
- Replace waitForTimeout with expect.poll for deterministic assertions
- Remove dead try/catch around cancelRequest (errors already swallowed
  by cancelRequest's internal .catch)

* fix: updated the test to check of connectionIds

---------

Co-authored-by: Sid <siddharth@usebruno.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Pooja <pooja@usebruno.com>
Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-04-03 01:59:11 +05:30
Abhishek S Lal
9465de02ee fix: support response query filtering in safe mode (#7441)
* fix(js): support response query filters in safe mode

* feat: add tests and improve bruno-response shim for query argument handling

- Introduced a new test suite for the bruno-response shim to validate query filtering and function callback behavior.
- Refactored the `res` function to enhance argument handling, allowing for better marshalling of QuickJS function handles and other values.
- Improved error handling within the response processing to ensure robust behavior during query execution.

* fix: correct argument handling in bruno-response shim for function callbacks

- Updated the `toHostQueryArg` function to use `vm.undefined` instead of `vm.global` when calling the provided function argument. This change ensures proper context handling during function execution, improving the reliability of query argument processing.

* chore: clean up .gitignore and enhance error handling in bruno-response tests

- Removed redundant entries from .gitignore to streamline ignored files.
- Improved error handling in the `afterEach` and `afterAll` hooks of the bruno-response tests to ensure proper disposal of resources, preventing potential memory leaks.

---------

Co-authored-by: cryst <230207759+cryst-hq@users.noreply.github.com>
2026-04-03 01:53:11 +05:30
Abhishek S Lal
5c1dc1184a fix: isJson assertion should accept arrays as valid JSON (#7620)
* fix(assert-runtime): update JSON validation to allow arrays and enhance test coverage

- Modified the JSON validation logic to accept arrays as valid JSON objects.
- Updated tests to ensure correct behavior for various array scenarios, including empty arrays and arrays of strings and objects.

* fix(assert-runtime): refine JSON validation logic to correctly handle arrays

- Updated the JSON validation to allow arrays as valid JSON objects, ensuring compatibility with various data structures.
- Adjusted the test assertions to reflect the new validation criteria.
2026-04-03 01:47:57 +05:30
lohit
bae5934137 perf: optimize DNS resolution to reduce request latency (#7664)
* perf: optimize DNS resolution to reduce request latency by ~31%

Replace default dns.lookup (libuv thread pool) with async dns.resolve4/6
(c-ares) that bypasses the thread pool bottleneck, falling back to
dns.lookup for /etc/hosts and mDNS hostnames.

* fix: address PR review feedback for DNS optimization

- Guard against undefined options in fastLookup to prevent runtime errors
- Document that options.family is not yet respected (safe today, noted for future)
- Replace callback as any with proper typed forwarding wrapper
- Extract shared agent config (defaultAgentOptions) to eliminate duplication
  between axios-instance.ts and agent-cache.ts, with documented rationale
- Mock DNS in test to avoid real network calls to google.com in CI

* fix: removed explicit resolve6 in fastLookup

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-04-02 21:27:51 +05:30
shubh-bruno
5cd3e7abbd fix: handle copy/paste item for requests & js file (#7656) 2026-04-02 18:13:37 +05:30
Chirag Chandrashekhar
765c9f1060 fix: add size and duration fields to CLI bru.runRequest() response (#7429)
* fix: add size and duration fields to CLI bru.runRequest() response

Add `size` and `duration` fields to the response object in CLI to match
GUI behavior, ensuring consistent API for bru.runRequest() across both
environments.

- `duration` is an alias for `responseTime` for GUI compatibility
- `size` is the byte length of the response buffer (0 for errors/skipped)

Fixes #7352

* fix: address PR review feedback for CLI response consistency

- Coerce responseTime header to number (was string from headers.get())
- Add comment explaining duration vs responseTime difference between
  GUI (wall-clock) and CLI (approximation using responseTime)
- Add integration tests for duration/size fields across skipped,
  success, and network error response paths

* fix: add missing setupProxyAgents mock in response-fields test

The success path calls setupProxyAgents which was missing from the
proxy-util mock, causing CI failure.

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-04-02 17:55:42 +05:30
Pooja
7ddd2d3f17 fix: allow file selection in multipart form without entering a key first (#7640) 2026-04-02 11:38:36 +05:30
Sid
ce87289616 fix: update system proxy fetching to use finally (#7652)
* fix: update system proxy fetching to use finally for improved reliability

* Update packages/bruno-electron/src/index.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 23:23:43 +05:30
sanish chirayath
c7ebe25cd6 Fix: ensure string authvalues, string header processing (#7646)
* feat: add helper to ensure string conversion for non-string values in Postman to Bruno conversion

- Introduced `ensureString` function to convert numeric and non-string values to strings, defaulting null/undefined to an empty string.
- Updated request handling in `importPostmanV2CollectionItem` to utilize `ensureString` for headers, parameters, and body fields.
- Added tests to verify correct conversion of numeric values to strings in headers, query parameters, and body fields.

* test: add test for numeric value conversion in Postman to Bruno transformation

- Implemented a new test case to verify that numeric values in example request and response fields are correctly converted to strings during the Postman to Bruno conversion process.
- The test checks various components including request headers, query parameters, path parameters, and body fields to ensure proper string conversion.

* test: add multipart form value test for numeric conversion in Postman to Bruno transformation

- Added a new test case to verify that numeric values in multipart form data are correctly converted to strings during the Postman to Bruno conversion process.
- The test checks the conversion of numeric values in the request body to ensure proper handling in the transformation.

* feat: enhance header parsing in Postman to Bruno conversion

- Added `parseStringHeader` and `normalizeHeaders` functions to handle various header formats, including string headers and concatenated strings.
- Updated the request and response handling in `importPostmanV2CollectionItem` to utilize the new header normalization logic.
- Introduced tests to verify correct parsing of string headers, including cases with no values and concatenated headers.

* refactor: enhance ensureString function for flexible fallback values

- Updated the `ensureString` function to accept a fallback parameter, allowing for customizable default values instead of a fixed empty string for null/undefined inputs.
- Modified the usage of `ensureString` in the `processAuth` function to utilize the new fallback feature for various authentication fields, improving the handling of optional values.

* refactor: update ensureString function to handle empty values

- Modified the `ensureString` function to return the fallback for null, undefined, or empty string values, enhancing its flexibility in handling various input scenarios.

* chore: update ESLint configuration and enhance Postman to Bruno conversion tests

- Added 'no-case-declarations' rule to ESLint configuration to enforce stricter coding standards.
- Modified the `processAuth` function to ensure proper block scoping for OAuth2 case handling.
- Improved header parsing logic to check for string type in content-type header.
- Added new tests to verify conversion of numeric authentication values to strings in both array-backed and object-backed formats during Postman to Bruno transformation.

* chore: update ESLint configuration to enforce stricter rules

- Added 'no-case-declarations' rule to ESLint configuration to enhance code quality.
- Adjusted existing rules for consistency and clarity in the configuration.

---------

Co-authored-by: Bijin A B <bijin@usebruno.com>
2026-04-01 21:41:05 +05:30
lohit
0a9988f80d feat: add gRPC proxy support (#7575)
* feat: add gRPC proxy support

* fix: respect channelOptions grpc.primary_user_agent over User-Agent header

* fix: remove non-standard grpc_proxy/no_grpc_proxy env var support

These are not recognized by any standard gRPC implementation. gRPC proxy
now uses the standard http_proxy/https_proxy/no_proxy variables like
grpc-core, grpc-go, and grpc-java.

* chore: add resolveGrpcProxyConfig tests and clean up grpc-client

Export resolveGrpcProxyConfig for testability and add unit tests covering
all proxy modes (off, on, system), auth encoding, protocol rejection,
bypass lists, and edge cases. Remove redundant cancelAndCloseConnection
call in startConnection (already guarded by addConnection). Document why
internal @grpc/grpc-js channel options are used for programmatic proxy.
2026-04-01 21:39:34 +05:30
Abhishek S Lal
d73e01993d feat(request-tabs): prevent browser autoscroll on middle-button mouse actions (#7443)
Added event handlers to prevent the browser's autoscroll behavior when the middle mouse button is pressed in the ExampleTab and RequestTab components. This improves user experience by avoiding unintended scrolling during tab interactions.
2026-04-01 21:36:31 +05:30
Abhishek S Lal
64bdef23ec fix: include examples when writing collection items in CLI OpenAPI import (#7613)
* feat: add support for examples in collection items

- Enhanced the processCollectionItems function to include examples from imported collection items.
- Added a new test case to verify that examples are correctly written to the output file during collection creation.

* fix: coerce response status to number in collection creation tests

Updated the test for createCollectionFromBrunoObject to ensure the response status is compared as a number, improving type consistency in assertions.
2026-04-01 21:34:23 +05:30
Abhishek S Lal
97467c57bf feat: add blur event handling to MultiLineEditor and SingleLineEditor components (#7619)
- Implemented a new _onBlur method to set the cursor position when the editor loses focus.
- Updated event listeners to include the blur event for both MultiLineEditor and SingleLineEditor, enhancing user experience by preserving cursor position.
- Ensured proper cleanup of event listeners during component unmounting to prevent memory leaks.
2026-04-01 21:33:18 +05:30
Abhishek S Lal
c8abb5be16 fix: forward cookies from 4XX/5XX responses in runner and CLI (#7498)
When axios receives a 4XX/5XX response it throws an error, causing
execution to jump to the catch block. saveCookies() was only called
in the try block (2XX path), so error-status cookies were silently
dropped in collection runs.

Fix applied to both affected code paths:
- packages/bruno-cli/src/runner/run-single-request.js (CLI runner)
- packages/bruno-electron/src/ipc/network/index.js (app collection runner)

Manual single-request execution was already correct — saveCookies() is
called after the try/catch there, so both status paths were covered.

Fixes #7475
2026-04-01 21:24:32 +05:30
gopu-bruno
8e978ae305 Add support for importing Swagger 2.0 specifications into Bruno collections (#7622)
* feat: support Swagger 2.0 OpenAPI import

* feat: support Swagger 2.0 OpenAPI import

* fix: refactor swagger2 converter, fix env creation, and update import UI labels

* fix: coderabbit comments

* fix: address coderabbit comments for body type handling

* fix: disallow OpenAPI Sync for Swagger 2.0 specs in UI

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
2026-04-01 21:19:47 +05:30
Abhishek S Lal
00bc93d3ac fix: ensure tags are always an array in bruToJson function (#7631)
Updated the bruToJson function to coerce the tags property into an array, ensuring consistent data structure when processing JSON input. This change enhances the reliability of the function by preventing potential errors when tags are not provided as an array.
2026-04-01 21:14:31 +05:30
Abhishek S Lal
3c3acf33a0 fix: ensure tags are always an array in parseBruRequest function (#7616)
Updated the parseBruRequest function to guarantee that the tags extracted from the JSON input are always returned as an array, improving data consistency and preventing potential errors when handling non-array values.
2026-04-01 21:12:47 +05:30
Chirag Chandrashekhar
8c9cad6d78 fix: cURL paste not updating request tab editors visually (#7610)
* fix: cURL paste not updating request tab editors visually

Remove the focus-based guard from PR#7098 that blocked editor value
updates when the editor had focus. This caused cURL paste to not
reflect in the request tab until switching tabs. Editors now always
accept incoming prop values and preserve cursor position. Also set
cursor to end of new URL after cURL paste using setTimeout.

* fix: added optional chaining

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-04-01 21:03:10 +05:30
lohit
0b3f5100e7 fix: recreate HTTP/HTTPS agents on redirect to prevent stale agent reuse (#7597) (#7615)
When a request redirected from HTTP to HTTPS (or vice versa), the
original httpAgent/httpsAgent leaked into the redirect config. The
httpsAgent — which carries custom CA certificates and TLS options — was
never created for the redirect URL, causing UNABLE_TO_VERIFY_LEAF_SIGNATURE.

Changes:
- setupProxyAgents (electron) now deletes stale agents at the top of
  every call so they are always recreated for the current URL
- setupProxyAgents extracted to bruno-cli/proxy-util.js (mirrors the
  electron version) and called on every redirect in the CLI path
- Removed the else-branch in bruno-requests/http-https-agents.ts that
  only created one agent based on initial protocol
- Added HTTP→HTTPS redirect test server and request to the
  custom-ca-certs SSL test suite
2026-04-01 20:55:28 +05:30
sanish chirayath
c502f959b4 feat: add helper to ensure string conversion for non-string values in Postman to Bruno conversion (#7644)
* feat: add helper to ensure string conversion for non-string values in Postman to Bruno conversion

- Introduced `ensureString` function to convert numeric and non-string values to strings, defaulting null/undefined to an empty string.
- Updated request handling in `importPostmanV2CollectionItem` to utilize `ensureString` for headers, parameters, and body fields.
- Added tests to verify correct conversion of numeric values to strings in headers, query parameters, and body fields.

* test: add test for numeric value conversion in Postman to Bruno transformation

- Implemented a new test case to verify that numeric values in example request and response fields are correctly converted to strings during the Postman to Bruno conversion process.
- The test checks various components including request headers, query parameters, path parameters, and body fields to ensure proper string conversion.

* test: add multipart form value test for numeric conversion in Postman to Bruno transformation

- Added a new test case to verify that numeric values in multipart form data are correctly converted to strings during the Postman to Bruno conversion process.
- The test checks the conversion of numeric values in the request body to ensure proper handling in the transformation.
2026-04-01 20:45:51 +05:30
Sid
87ca5a85d0 chore: add a promise based wait group for the shell variables (#7647) 2026-04-01 20:41:30 +05:30
Pooja
40298b96a4 fix: preserve query params without values by not appending = sign (#7567)
* fix: preserve query params without values by not appending = sign

* fix: parseCurlCommand test
2026-04-01 20:08:31 +05:30
Sid
9e89255f6d security: fix all critical vuln dependency reports (#7645)
* chore: remove form-data vuln

* chore: stale aws in lock

* chore: other critical vulns

* chore: correct deps
2026-04-01 18:28:47 +05:30
Pooja
28d1ba2438 improve: graphql query builder test (#7618) 2026-04-01 17:11:22 +05:30
Sid
652f3cc3fe feat: basic annotation syntax support for lang (#7609)
* chore: basic annotation support

* chore: string and escape cases

* feat: add basic multiline support

* fix: simplify dedentation logic in annotation multiline text block

* chore: fix asserts

* feat: add annotation support to env and collection

* feat: enhance annotation parsing with support for quoted arguments and nested parentheses

* refactor: feedback, remove inline annotations

* feat: move serializeAnnotations function to utils and update imports
2026-04-01 16:04:34 +05:30
Pooja
aa7b8f4ca1 fix: autosave playwright test (#7641)
* fix: autosave playwright test

* fix
2026-04-01 15:06:31 +05:30
gopu-bruno
bcc1b535ff fix: prevent rerun flicker and fix runner configuration list order (#7639) 2026-04-01 14:20:08 +05:30
Chirag Chandrashekhar
ce105aea58 fix: refine dotenv serialization for special characters handling (#7592) 2026-04-01 13:10:36 +05:30
Chirag Chandrashekhar
8338f91487 fix: app crash on clicking close button (#7637)
* fix: app crash on clicking close button \n Added collection, workspace, and api spec watcher cleanup on app close method

* fix: close file watchers before app exit to prevent crash on macOS

Close all chokidar file watchers (collection, workspace, apiSpec) before
the Node environment is torn down. The native FSEvents watchers run on
their own threads and their cleanup races with FreeEnvironment, causing
an abort when fse_instance_destroy tries to lock a destroyed mutex.

Watchers are closed in both mainWindow.on('close') and app.on('before-quit')
to cover the native close button path and the app.exit() path.

* fix: move watcher cleanup from close handler to before-quit only

The close event is cancelable — if the user cancels the unsaved changes
dialog, watchers would remain closed for the rest of the session.
Move closeAllWatchers() to before-quit which only fires on actual quit.

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-04-01 10:54:50 +05:30
Sid
4a78f637d3 fix: 'axios' module not found locally (#7638) 2026-03-31 20:14:08 +05:30
Sid
3b38b14362 fix: update keybinding actions and tests for reopening closed tabs (#7635) 2026-03-31 18:43:14 +05:30
Pooja
4f5c73840c fix: folder docs edit button style (#7630) 2026-03-31 16:39:14 +05:30
Sid
3ea489816c chore: pin axios version (#7632)
* chore: pin axios version

* limit it to the max mentioned by the repo

* chore: pin to 1.13.6

* chore: pin transitive

* chore: update axios version to 1.13.6

* chore: min release age for deps
2026-03-31 15:30:00 +05:30
shubh-bruno
f0866be3b3 feat: keybindings customisation (#7603) 2026-03-31 12:39:00 +05:30
Pooja
882b11ca3d fix: multipart form button alignment (#7629)
* fix: multipart form button alignment

* rm: styles
2026-03-31 11:42:31 +05:30
Sid
53aa9ed6e3 fix(dependencies): update fast-xml-parser to 5.5.7 and simple-git to … (#7602)
* fix(dependencies): update fast-xml-parser to 5.5.7 and simple-git to 3.32.3; add path-expression-matcher and fast-xml-builder
2026-03-30 19:20:50 +05:30
shubh-bruno
c01942a6f3 fix: status & statusText swap (#7589)
* fix: status & statusText swap

* chore: typo

* test: tests for swapping status and statusText

---------

Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-03-28 15:12:42 +05:30
Abhishek S Lal
f491e9091b refactor: update file name display in MultipartFormParams and ResponseExampleMultipartFormParams components (#7595)
- Replaced the static file name display with a SingleLineEditor for better readability and consistency.
- Removed unnecessary padding in StyledWrapper for a cleaner layout.
- Enhanced value interpolation logic to handle arrays in interpolate-vars.js for improved data processing.
2026-03-27 20:15:34 +05:30
Abhishek S Lal
2977fc7bea fix: coerce Postman header values to string during import (#7564)
Postman collections can contain numeric header values (e.g., status code 200),
which fail Bruno's schema validation expecting strings. Wrap header.value in
String() for example request and response headers, matching the existing
pattern used for regular request headers.
2026-03-27 20:08:36 +05:30
Ayush
d07c323755 prevent Enter key from submitting form during autocomplete selection (#7221) 2026-03-27 20:04:52 +05:30
Abhishek S Lal
f1b84e09c3 fix: update regex for path parameter parsing to handle alphanumeric and underscore characters (#7388) 2026-03-27 20:03:00 +05:30
Chirag Chandrashekhar
13e97f0367 fix: preserve global environment color during script execution (#7427)
When executing requests with pre-request or post-response scripts, the
global environment color property was being stripped from YAML files.
This happened because the save operation only passed `variables` through
the IPC chain, and the workspace-environments store created a new
environment object without preserving the existing `color` property.

The fix passes the `color` property through the entire IPC chain:
- Redux actions now include `color` in the save-global-environment IPC call
- IPC handler accepts and forwards `color` to both stores
- workspace-environments store includes `color` when creating the environment object
- global-environments store preserves `color` when updating

Fixes #7348

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-03-27 19:56:19 +05:30
Chirag Chandrashekhar
7ef3981656 fix: resolve theme, overflow, and z-index bugs in Remove Collection modal (#7590)
Use themed styled-component classes instead of hardcoded Tailwind colors
for the drafts confirmation modal, add text truncation for long collection
names, and lower EditableTable resize-handle z-index so it no longer
renders above modals.

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-03-27 19:52:57 +05:30
SahilShameerDev
d2f6eb146b fix: make documentation, folder docs and collection docs edit button … (#7151)
* fix: make documentation, folder docs and collection docs edit button sticky

* Update packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/bruno-app/src/components/FolderSettings/StyledWrapper.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* refactor: move sticky edit button styles to specific doc components

* style: clean up redundant css rules in markdown-body

* Update packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-27 19:44:59 +05:30
sanish chirayath
bef4b6bbee feat(cookies): add direct cookie access methods and update translations (#7073)
* feat(cookies): add direct cookie access methods and update translations

- Introduced new methods for direct cookie access: `bru.cookies.get`, `bru.cookies.has`, and `bru.cookies.toObject`.
- Updated translation mappings in `bruno-to-postman-translator` and `postman-to-bruno-translator` to support these new methods.
- Enhanced tests to verify correct translation between `bru` and `pm` cookie methods, including mixed usage scenarios.
- Updated `Bru` class to handle cookie access based on the current request URL.

* feat(cookies): enhance cookie management with new methods and refactor

- Added new cookie methods: `toString`, `clear`, `delete`, `one`, `all`, `idx`, `count`, `indexOf`, `find`, `filter`, `each`, `map`, and `reduce` to `bru.cookies`.
- Refactored `Bru` class to utilize a new `CookieList` for cookie management, improving structure and readability.
- Updated translation mappings in `bruno-to-postman-translator` and `postman-to-bruno-translator` to include new cookie methods.
- Introduced `PropertyList` and `ReadOnlyPropertyList` classes for better data structure management.
- Enhanced tests for comprehensive coverage of new cookie functionalities and their interactions.

* docs(readonly-property-list): clarify array usage in constructor comments

* feat(cookies): add direct cookie manipulation tests and methods

* feat(cookies): add hasCookie method for checking cookie existence

* fix

* refactor(cookies): simplify cookie method translations

* feat(cookies): expand cookie API with new methods and tests

- Added new cookie methods: `get`, `has`, `toString`, `clear`, `upsert`, `remove`, `idx`, and `indexOf` to enhance cookie management.
- Updated translation mappings for `bru.cookies` to include new methods in `bruno-to-postman-translator` and `postman-to-bruno-translator`.
- Introduced tests for new methods and their interactions, ensuring comprehensive coverage of cookie functionalities.
- Enhanced existing tests to validate correct behavior of cookie methods across different scenarios.

* refactor(cookies): update CookieList to extend PropertyList and improve error handling

* test(cookies): add regression tests for jar and direct cookie patterns

- Introduced regression tests to ensure that jar patterns are correctly prioritized over direct cookie access patterns in translations.
- Updated `CookieList` to extend `ReadOnlyPropertyList` instead of `PropertyList`, clarifying its functionality.
- Refactored cookie method handling in the `bru` shim to utilize a new asynchronous bridge for improved error handling and consistency.

* refactor(cookies): update translations and remove PropertyList

- Enhanced comments in `postman-translations.js` to clarify the order of cookie jar translations.
- Updated `cookie-list.js` comments to better describe the factory function for the cookie jar.
- Removed the `PropertyList` class and its associated tests, streamlining the codebase and focusing on `ReadOnlyPropertyList` and `CookieList` for cookie management.

* fix(cookies): normalize tough-cookie objects and improve remove method comments

- Updated `CookieList` to normalize tough-cookie instances to plain objects, preventing circular references and exposing internal structures.
- Enhanced comments in the `remove` method to clarify behavior when removing non-existent or empty-named cookies.

* test(cookies): update tests to use async/await for consistency

* test(cookies): use async/await in cookie tests for consistency

* refactor(readonly-property-list): update get and reduce methods for improved behavior

* fix(cookies): update cookie method signature in autocomplete hints and enhance translation comments

- Modified the autocomplete hint for `bru.cookies.has` to include the new signature with an optional value parameter.
- Improved comments in `postman-translations.js` to clarify the order of cookie jar translation patterns for better understanding.

* refactor(cookies): introduce PropertyList for enhanced cookie management

* refactor(property-list): simplify repopulate method and enhance item handling logic

* feat(cookies): implement PropertyList bridge for enhanced cookie management

- Introduced a new `createPropertyListBridge` utility to streamline the integration of cookie methods into the QuickJS VM.
- Replaced the previous async cookie bridge with a more flexible approach, allowing for both synchronous and asynchronous cookie operations.
- Added comprehensive tests to validate the functionality of the new cookie methods in both developer and safe modes.
- Updated existing cookie tests to ensure compatibility with the new PropertyList structure.

* fix(tests): correct expected passed requests in cookie tests

- Updated the expected number of passed requests in the cookie tests from 34 to 6 to reflect the correct validation results.
- Ensured consistency in test assertions across multiple test cases for the PropertyList API.

* fix(cookies): update cookie URLs to use localhost for testing

- Changed all cookie-related test scripts to use `{{localhost}}` instead of `{{host}}` for the ping URL, ensuring consistency in local testing environments.
- Updated the cookie test suite to reflect the new URL structure, enhancing the reliability of the tests.
- Removed outdated cookie test files to streamline the test suite.

* refactor(cookies): standardize cookie handling with localhost variable

- Updated cookie test scripts to utilize the `{{localhost}}` variable for setting and retrieving cookies, ensuring consistency across tests.
- Enhanced clarity in comments regarding cookie behavior for different domains.
- Improved test assertions to validate cookie management functionality more effectively.

* refactor(property-list, readonly-property-list): update methods to use private class fields

* feat(cookies): enhance CookieList API with detailed documentation and method improvements

- Updated the `CookieList` class to provide comprehensive documentation on cookie management methods, including `add`, `upsert`, `remove`, and `delete`.
- Improved method signatures to support both callback and Promise-based usage for asynchronous operations.
- Added detailed descriptions for read and write methods, including examples and expected behavior.
- Enhanced the integration of the `CookieList` with the QuickJS VM by updating the property list bridge to include `toJSON` in sync read object methods.

* feat(cookies): add detailed examples and improve async bridge documentation

- Enhanced the `createPropertyListBridge` function documentation with comprehensive examples for setting up cookie methods in QuickJS.
- Clarified the two-phase setup process for async write methods, detailing the registration of bridge functions and the generation of JavaScript code for method wrappers.
- Added a new test case for the `toJSON()` method to ensure it returns a cloned array of all cookies, validating the expected structure and properties.

* fix(assert-runtime): correct syntax error in response parser assignment

- Added a semicolon at the end of the response parser assignment to ensure proper syntax in the AssertRuntime class.
2026-03-27 19:42:23 +05:30
gopu-bruno
c2de480091 feat: revamp Runner UI with Timings and Filters sections (#7505)
* feat: revamp Runner UI with Timings and Filters sections

* fix: use configurable radio name for runner tags

* fix: update Run Collection modal ui

* refactor: improve runner radios accessibility and ux

* fix: address runner review nits

* fix: update tag list hover styling

* fix: add data-testid for runner button

* fix: preserve runner delay when updating request selection config

* fix: preserve runner requestItemsOrder on run

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
2026-03-27 19:38:36 +05:30
Sid
f5a9a485ed fix(security): santize HTML before being rendered in documentation blocks (#7598)
* fix: purify markdown before rendering

* chore: resolve stale html
2026-03-27 19:34:34 +05:30
lohit
95de14adcb feat: add OAuth 1.0 authentication support (#7482)
* feat: add OAuth 1.0 authentication support

Add full OAuth 1.0 (RFC 5849) authentication with support for
HMAC-SHA1/256/512, RSA-SHA1/256/512, and PLAINTEXT signature methods.
Includes UI components, bru/yml serialization, Postman import, code
generation, CLI support, and comprehensive playwright and unit tests.

* test: replace real-looking PEM literals with fake markers in oauth1 tests

Avoid tripping secret scanners by using obviously fake BEGIN/END markers
and non-sensitive base64 content in serialization and round-trip tests.

* fix: remove invalid OAuth1 placeholder header from code generator

OAuth1 requires runtime-computed nonce, timestamp, and signature that
cannot be pre-computed for a static code snippet. Return an empty array
instead of emitting an Authorization header with literal <signature>,
<timestamp>, <nonce> placeholders.

* fix: remove unreachable oauth1 case from WSAuth component

The oauth1 switch branch was dead code since it was not in
supportedAuthModes and the useEffect would reset it to 'none'
before it could render.

* fix: remove unused collectionPath param and use path.basename for filename extraction

* refactor: rename OAuth1 fields for clarity

- tokenSecret → accessTokenSecret
- signatureMethod → signatureEncoding
- addParamsTo value 'queryparams' → 'query'

* refactor: rename addParamsTo to placement in OAuth1 auth

* fix: add missing oauth1: null in buildOAuth2Config and upgrade @opencollection/types to 0.9.0

* test: add oauth1 import tests and fix missing oauth1: null in auth assertions

* ci: add auth playwright tests workflow for Linux, macOS, and Windows

* refactor: rename signatureEncoding to signatureMethod and fix timeline race condition

- Rename OAuth1 signatureEncoding to signatureMethod across all packages
- Fix timeline showing "No Headers/Body found" when request-sent IPC event
  arrives after response by retroactively updating the timeline entry
- Store requestUid in timeline entries for precise matching
- Correct timeline entry timestamp on retroactive update for proper sort order

* ci: add OAuth1 CLI tests and reorganize auth actions under oauth1/

- Add CLI tests that run full BRU and YML collections via bru run
- Add start-test-server actions for Linux, macOS, and Windows
- Move auth e2e and setup actions under auth/oauth1/ directory
- Fix Windows Playwright failures caused by unescaped backslashes in collectionPath template variable

* ci: reorder auth tests to run E2E tests before CLI tests

* ci: start test server after E2E tests to fix port 8081 conflict
2026-03-27 18:59:42 +05:30
sanish chirayath
784e851d4c refactor: update Bru constructor to accept a single options obj for improved readability (#7562)
* refactor: update Bru constructor to accept a single options object for improved readability

- Changed the Bru class constructor to accept a single options object instead of multiple parameters, enhancing code clarity and maintainability.
- Updated all instances of Bru instantiation across the codebase to align with the new constructor format.

* docs: enhance Bru constructor documentation with additional certs and proxy configuration options

- Updated the documentation for the Bru class constructor to include new parameters related to certs and proxy configuration, improving clarity for users on available options.
- Added descriptions for collectionPath, options, clientCertificates, collectionLevelProxy, and systemProxyConfig to provide comprehensive guidance on their usage.

* docs: refine Bru constructor documentation for clarity and default values

- Updated the constructor documentation for the Bru class to enhance clarity by consolidating parameter descriptions into a single options object format.
- Added default values for optional parameters, improving guidance for users on expected input and usage.
2026-03-27 18:56:09 +05:30
Abhishek S Lal
708e88241f style: update CodeMirror bracket highlighting in StyledWrapper components (#7596)
- Enhanced the styling for matching and non-matching brackets in CodeMirror across multiple StyledWrapper components.
- Updated background and text colors to align with the theme for better visibility and user experience.
2026-03-27 18:13:08 +05:30
Chirag Chandrashekhar
bbf3cb8dd3 fix: preserve user-defined boundary in multipart/mixed Content-Type header (#7531)
* fix: preserve user-defined boundary in multipart/mixed Content-Type header

When users specify a boundary parameter in their Content-Type header for
multipart/mixed requests with TEXT body mode, Bruno now preserves the
user-defined boundary instead of generating a new one.

Fixes: https://github.com/usebruno/bruno/issues/7523

* updated the test to use local server and changed the request method to GET

* fix: handle quoted boundary values in Content-Type header extraction

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-03-27 16:49:50 +05:30
Pooja
53b75d083f fix: multipart form upload icon visibility (#7571) 2026-03-27 16:41:36 +05:30
Pooja
9cea60477a fix: prompt variable in URL path incorrectly parsed as query parameter (#7216) 2026-03-27 15:47:21 +05:30
Pooja
35cd72534b feat: graphql query builder (#7468)
* feat: graphql query builder

* fix: bug

* improvements

* fix

* fix: playright test

* fix

* fix

* improvements

* chore: types

* fix

* chore: minimal error boundary

* imp: use button component

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2026-03-27 12:29:01 +05:30
Thomas
f69afd7fa2 fix: add the meta block to the object returned by transformFolderRootToSave (#7582)
Co-authored-by: fantpmas <fantpmas@users.noreply.github.com>
2026-03-26 20:41:31 +05:30
Chirag Chandrashekhar
ff975c44f2 fix: cross-collection drag and drop tab and format issues (#7584)
Close the open tab when a request is moved to a different collection
via drag and drop, preventing the "Request no longer exists" error.

Add format conversion when dragging requests between collections with
different formats (.bru vs .yml). A new IPC handler parses the source
file and re-serializes it in the target collection's format. Folder
cross-format moves are blocked with a toast error.

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-03-26 20:32:29 +05:30
Abhishek S Lal
03dcb6b7b9 feat: implement item sorting for Postman export (#7581)
- Added functions to sort items by sequence and name, ensuring folders are prioritized over requests in the export output.
- Enhanced the `brunoToPostman` function to utilize the new sorting logic.
- Introduced comprehensive tests to validate the sorting behavior for folders and requests, including nested structures.
2026-03-26 20:16:14 +05:30
Abhishek S Lal
c8d835ef4d fix: re-apply masking in MultiLineEditor and SingleLineEditor after setValue() to preserve CodeMirror marks (#7585) 2026-03-26 20:10:38 +05:30
Sid
9944819f73 chore: add in more react standards (#7577)
* chore: add in more react standards

* Update CODING_STANDARDS.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 14:56:42 +05:30
shubh-bruno
73df422c4e feat: persist window frames and widths (#7409) 2026-03-26 13:11:04 +05:30
shubh-bruno
304f6c8b80 feat: support for pkg installer (#7561)
Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-03-25 16:35:18 +05:30
Abhishek S Lal
4245944ccc fix(theme): convert theme bg to hex for Electron backgroundColor (#7569)
* fix(theme): convert theme bg to hex for Electron backgroundColor

* fix(theme): simplify background color conversion to hex in ThemeProvider
2026-03-25 15:30:24 +05:30
Abhishek S Lal
590a5a968d fix(import): handle EEXIST when importing OpenAPI collections with paths grouping (#7499)
Use { recursive: true } in mkdirSync during collection import so that
directories which already exist (e.g. due to duplicate or case-colliding
path params like {customerID} vs {customerId}) do not throw EEXIST and
abort the import.
2026-03-24 19:19:29 +05:30
statxc
650ad0fe60 fix: overlapping help text issue in Environment Variables (#7225)
* fix: overlaping help text

* fix: add tooltip-mod class to InfoTip in VarsTable to fix overlapping help text

* chore: fix width for request pane infotip

---------

Co-authored-by: statxc <statxc@users.noreply.github.com>
Co-authored-by: Ubuntu <ubuntu@vps-eae40731.vps.ovh.ca>
Co-authored-by: Sid <siddharth@usebruno.com>
2026-03-24 18:05:59 +05:30
dependabot[bot]
367465b371 chore(deps): bump dorny/test-reporter from 2 to 3 (#7555)
Bumps [dorny/test-reporter](https://github.com/dorny/test-reporter) from 2 to 3.
- [Release notes](https://github.com/dorny/test-reporter/releases)
- [Changelog](https://github.com/dorny/test-reporter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dorny/test-reporter/compare/v2...v3)

---
updated-dependencies:
- dependency-name: dorny/test-reporter
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 17:49:11 +05:30
sanish chirayath
7182cee629 fix: enhance error handling and context retrieval for script errors (#7537)
* refactor: enhance error handling and context retrieval for script errors

- Updated the error formatter to utilize in-memory script content for error context, improving accuracy when users have unsaved changes.
- Introduced a new utility function, `getSourceContextFromContent`, to extract context lines from in-memory scripts.
- Enhanced tests to verify that draft script errors display the correct code context, ensuring users see the most relevant information during debugging.
- Added new Playwright tests to validate error handling in draft states across pre-request, post-response, and test scripts.

* refactor: enhance error context retrieval in error formatter

- Updated `getSourceContext` to accept in-memory content, improving context extraction for unsaved changes.
- Deprecated `getSourceContextFromContent` in favor of the new parameterized approach.
- Adjusted related tests to ensure accurate context handling for draft script errors.

* refactor: enhance error context handling for draft scripts

* refactor: streamline script error tests and enhance utility functions

- Consolidated helper functions for sending requests and waiting for responses into the actions module.
- Introduced new utility functions for selecting script sub-tabs and editing CodeMirror editors.
- Updated test cases to utilize the new utility functions, improving readability and maintainability.
- Enhanced locators for better integration with testing frameworks.

* refactor: improve script error context handling and utility functions

- Introduced a new utility function to streamline the retrieval of script block start lines for .bru and .yml files.
- Enhanced the error formatter to prioritize in-memory draft content when resolving error contexts, improving accuracy for unsaved changes.
- Consolidated context extraction logic into a single function to reduce redundancy and improve maintainability.
- Updated related tests to ensure accurate context handling for both draft and disk-based scripts.

* refactor: add comments to clarify line index calculations in error formatter
2026-03-24 15:31:21 +05:30
sanish chirayath
86b6e2f4f3 feat: add hasCookie hint to autocomplete suggestions for cookie management (#7516) 2026-03-24 12:52:03 +05:30
Pooja
37c0a76146 fix: html report collapse for repeated requests (#7153) 2026-03-24 12:44:20 +05:30
Pooja
4461dfded3 fix: global search filter by active workspace (#7156) 2026-03-24 12:41:04 +05:30
Abhishek S Lal
123c2893f3 feat(theme): enhance theme management with background color support (#7454)
- Updated ThemeProvider to send background color along with theme changes to the renderer.
- Introduced WindowStateStore methods for managing theme mode and background color.
- Set the main window's background color based on the stored theme during app initialization.

This improves the user experience by ensuring the application reflects the selected theme accurately.
2026-03-24 12:35:09 +05:30
naman-bruno
f1d7f007fe remove activeEnvironmentUid and migration (#7545)
* remove activeEnvironmentUid and migration

* fix: no environment handling

* fix: standardize workspace path handling
2026-03-23 21:02:53 +05:30
Chirag Chandrashekhar
32b9f527ea fix: quote values containing hash (#) in .env file serialization (#7380)
* fix: quote values containing hash (#) in .env file serialization

Values containing # characters were being truncated when saved to .env
files because the dotenv parser interprets # as a comment character.

This fix adds a shared jsonToDotenv utility in bruno-common that properly
quotes values containing special characters (#, \n, ", ', \) to ensure
they are preserved through serialization and parsing.

- Add jsonToDotenv utility with comprehensive test coverage
- Update bruno-electron and bruno-app to use shared utility
- Remove duplicate serialization logic from multiple locations

Fixes https://github.com/usebruno/bruno/issues/7375
Fixes https://github.com/usebruno/bruno/issues/7327

* fix: escape carriage returns in .env file serialization for Windows CRLF handling

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-03-23 20:58:28 +05:30
Chirag Chandrashekhar
79f9dbff9f fix: handle nested parentheses in URL link detection (#7406)
The LinkifyIt library was truncating URLs containing nested parentheses,
such as Kibana/RISON formatted links. For example, a URL like:
https://example.com/?_g=(filters:!(),time:(from:now))&_a=(data)
would be cut off at the first balanced parenthesis, losing the &_a=...
portion.

Added extendUrlWithBalancedParentheses helper function that:
- Counts unbalanced opening parentheses in the detected URL
- Extends the URL to include closing parens and following content
- Stops at URL terminators (whitespace, quotes, angle brackets)
- Stops if parentheses would become over-balanced (more closing than opening)

Fixes #7402

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-03-23 20:42:56 +05:30
sanish chirayath
646c90819d feat: enhance ScriptError with source context and remove auto-commenting of untranslated pm commands (#7449)
* feat: enhance ScriptError with source context, code snippets, and navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: remove auto-commenting of untranslated pm commands during import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: update CodeSnippet styles to use theme colors for error and warning highlights

* fix: remove unused SCRIPT_TYPES import from network IPC module

* refactor: remove unused functions and clean up source-context utility

- Removed `getUnifiedScriptContext`, `getWarningSourceGroups`, and related helper functions from `source-context.js` to streamline the utility.
- Updated tests in `source-context.spec.js` to reflect the removal of unused functions, ensuring only relevant tests for `findLineInSource` and `getScriptContext` remain.

* refactor: simplify ScriptError component and update styles

* refactor: streamline tab management in Script components

* refactor: enhance tab management in ScriptError and add testsMetadata handling in prepare-request

* refactor: improve error source identification in ScriptError component

- Enhanced the logic for determining error source types by introducing separate checks for folder and collection files.
- Updated the handling of folder file names to ensure accurate UID retrieval and labeling.
- Streamlined the overall structure of the getErrorSourceInfo function for better readability and maintainability.

* refactor: improve ScriptError component and enhance styling

- Simplified the conditions for displaying the ScriptError component in ResponsePane.
- Updated navigation logic in ScriptErrorCard to ensure proper handling of source information.
- Adjusted styles in StyledWrapper to allow for visible overflow.
- Enhanced ScriptErrorIcon to accept additional class names for better styling flexibility.
- Minor layout adjustments in RunnerResults ResponsePane for improved UI consistency.

* refactor: simplify test description for untranslated pm commands and consolidate error formatter imports

* fixes

* refactor: update focusedTab logic in Script components to use collection and folder UIDthe respective collection and folder UID instead of the activeTabUid.

* feat: add buildErrorContext utility for enhanced error handling in network IPC

- Introduced a new utility function `buildErrorContext` to construct detailed error context from script errors.
- This function parses error locations, adjusts line numbers, and retrieves relevant source context, improving error reporting.
- Removed the previous inline error handling logic from the network IPC module to streamline the codebase.

* feat: added playwright test cases to test scriptError behavior

* refactor: enhance ScriptError component and improve error handling

- Updated labels for error source types in ScriptError to be more concise (e.g., "Request Script" to "Request").
- Improved navigation logic in ScriptErrorCard to ensure proper handling of navigable file paths.
- Enhanced styling in StyledWrapper for better visual consistency and user experience.
- Added tests to verify fallback behavior for missing error types and non-navigable file paths.
- Refined utility functions for better context extraction from script errors.

* refactor: normalize file paths for cross-platform compatibility in ScriptError component

* refactor: improve file path handling in ScriptError component

* refactor: enhance buildErrorContext for improved error handling

* docs: add detailed comments to build-error-context for better understanding of error handling

* refactor: enhance error block line detection in error formatter

- Updated `findScriptBlockEndLine` and `findYmlScriptBlockEndLine` functions to return null for empty or missing blocks, improving accuracy in line detection.
- Added comprehensive tests for both functions to ensure correct behavior across various scenarios, including handling of non-.bru and non-.yml files.

* refactor: improve error handling and testing in ScriptError and buildErrorContext

- Updated ScriptError component to streamline tab management by replacing focusTab with addTab for better request handling.
- Enhanced buildErrorContext to return null for empty or missing script blocks in .bru and .yml files, ensuring accurate error reporting.
- Added tests to validate behavior for empty script blocks and improved error context extraction in various scenarios.

* test: add new tests for script error navigation and handling

- Implemented tests for post-response file-path navigation to the Script tab and verification of active sub-tabs.
- Added keyboard navigation tests to trigger file-path navigation using the Enter key.
- Included a test for multiple error cards to ensure closing one does not affect others.
- Enhanced runner tests to verify navigation to the Tests tab from script error results.

* refactor: enhance CodeSnippet line rendering and remove unused source-context utilities

* refactor: update locators in script-errors tests for improved readability and maintainability

* test: enhance script-errors tests to verify error line content

* review fixes

* refactor: update RunnerResults component and enhance locators for improved testability

* refactor: remove buildErrorContext and replace with formatErrorWithContextV2

- Deleted the buildErrorContext function and its associated tests.
- Updated network IPC to utilize formatErrorWithContextV2 for improved error context handling.
- Enhanced error reporting by ensuring structured error context is returned for desktop UI.

* refactor: enhance tab components with data-testid attributes for improved testability

- Added data-testid attributes to tab elements in CollectionSettings and FolderSettings components for better integration with testing frameworks.
- Updated Tabs and ResponsiveTabs components to include data-testid attributes for tab triggers, enhancing the ability to select and verify tabs in tests.
- Modified script-errors tests to utilize new locators for improved readability and maintainability.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 21:36:02 +05:30
Pragadesh-45
37be721922 feat: include pinned data in filtering for more accurate results in env variables search (#7513) 2026-03-18 17:56:25 +05:30
naman-bruno
d6429cbb6d fix: clear draft on save and update dependencies in useEffect (#7512) 2026-03-18 14:49:07 +05:30
naman-bruno
0109d72475 refactor: optimize formik value handling and improve save conditions (#7507)
* refactor: optimize formik value handling and improve save conditions

* fix
2026-03-18 14:27:58 +05:30
sanish chirayath
68d80b8f78 feat(bruno-js): add hasCookie function to cookie jar shim for improved cookie management (#7501) 2026-03-16 23:17:54 +05:30
Abhishek S Lal
1877119b81 fix(openapi-sync): simplify IPC calls, fix state priorities, and improve stored spec missing UX (#7489)
* refactor(OpenAPISyncTab): remove unused props and streamline IPC calls

- Eliminated unnecessary sourceUrl prop from various components and hooks in the OpenAPISyncTab.
- Improved pretty-printing logic in OpenAPISpecTab to handle non-JSON content gracefully.
- Updated IPC calls to remove redundant parameters, enhancing code clarity and maintainability.

* feat(OpenAPISyncTab): enhance user interaction and visual feedback

- Added onTabSelect prop to OpenAPISyncTab for improved tab navigation.
- Updated color properties in StyledWrapper for better consistency with theme.
- Replaced IconClock with IconAlertTriangle in CollectionStatusSection for clearer status indication.
- Enhanced messaging in OverviewSection and SpecStatusSection to provide clearer user guidance.
- Introduced handleRestoreSpec function in useSyncFlow for better spec restoration handling.

* fix(OpenAPISyncTab): update button labels for clarity in OverviewSection

- Changed button label from 'restore' to 'spec-details' for better context.
- Updated the button text from 'View Details' to 'Go to Spec Updates' to enhance user understanding of navigation options.

* refactor(OpenAPISyncTab): remove unused props and streamline component logic

- Eliminated unnecessary props from OpenAPISyncTab, CollectionStatusSection, and SpecStatusSection for cleaner code.
- Removed commented-out code in OverviewSection and SpecStatusSection to enhance readability.
- Introduced posixifyPath utility function in filesystem.js to standardize path formatting.

* fix(OpenAPISyncTab): update openapi config handling to support array format

- Modified the logic in loadBrunoConfig to handle openapi as an array, ensuring consistent resolution of source URLs for all entries. This change improves the configuration handling for OpenAPI specifications.

* fix(OpenAPISyncTab): improve openapi config handling and merge logic

- Updated loadBrunoConfig to ensure openapi is treated as an array, enhancing source URL resolution.
- Modified mergeWithUserValues to handle cases where specItems may be undefined, improving robustness in merging user values with specifications.
2026-03-16 18:14:53 +05:30
Chirag Chandrashekhar
7e717768d2 fix(RequestTabPanel): update loading message for better user feedback (#7492)
Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-03-16 16:59:06 +05:30
naman-bruno
c1dff11fa1 refactor: optimize debounced save functionality (#7495) 2026-03-16 16:47:27 +05:30
Pooja
7e0b8d9f9d fix: convert non-string variable values to strings during postman import (#7476) 2026-03-16 12:05:24 +05:30
Abhishek S Lal
83ddfc33d2 refactor(OpenAPISyncTab): streamline component logic and enhance user feedback (#7483)
- Removed unused props and improved error handling in OpenAPISyncTab components.
- Updated messaging in CollectionStatusSection and OverviewSection for clarity.
- Enhanced the SpecDiffModal to provide better visual feedback on changes.
- Refactored sync flow logic to ensure accurate endpoint categorization and improved performance.
- Added new utility functions for better handling of spec changes and endpoint comparisons.
2026-03-14 01:15:10 +05:30
Bohdan
1ab296f1e3 add missing color to scrollbar-color property (#7481) 2026-03-13 23:51:44 +05:30
Abhishek S Lal
384bf4f190 feat: improve OpenAPI Sync tab UX and fix sync flow bugs (#7467)
* fix: specify OpenAPI 3.x in error messages for file uploads and URL validation

Updated error messages in ConnectSpecForm and ConnectionSettingsModal to clarify that only OpenAPI 3.x specifications are valid. Enhanced useOpenAPISync hook to reflect the same specificity in error handling for invalid URLs.

* feat(OpenAPISpecTab): add pretty-printing for JSON content in API spec viewer

Implemented a new function to pretty-print JSON content for improved readability in the OpenAPISpecTab component. This enhancement ensures that JSON specifications are displayed in a more user-friendly format while leaving YAML content unchanged.

* feat(OpenAPISyncHeader): resolve and display absolute file paths for local sources

Added functionality to resolve relative file paths to absolute paths for better user experience in the OpenAPISyncHeader component. Implemented state management and side effects to handle path resolution based on the source URL, enhancing the display of local file paths.

* feat(OpenAPISyncTab): enhance collection status display and add endpoint counting utility

Refactored the CollectionStatusSection to streamline the display of collection drift status, integrating loading states and improved messaging for initial sync scenarios. Introduced a new utility function to count HTTP endpoints in OpenAPI specifications, enhancing the overall functionality of the OpenAPISyncTab. Additionally, updated the OpenAPISyncHeader and OverviewSection to utilize stored specification metadata for better user experience.

* refactor: improve OpenAPI Sync endpoint handling

- Enhanced the logic for adding new requests by ensuring existing files are verified before removal to prevent accidental deletions.
- Streamlined the process of adding new endpoints, including checks for existing files and merging requests to maintain user customizations.
- Added comments for clarity on the purpose of changes, particularly regarding filename collision prevention and file content verification.

* style(OpenAPISyncTab): update styles for improved visual feedback

- Changed background color for the 'type-spec-modified' class to a warning color for better distinction.
- Updated text color and background for the SyncReviewPage to enhance readability and visual hierarchy.
- Adjusted default expanded states for endpoint sections to improve user experience during sync reviews.

* chore: update .gitignore and enhance OpenAPISyncTab components

- Added new entries to .gitignore for agent-related files and skills-lock.json.
- Modified StyledWrapper to improve overflow handling and added sticky headers for better visibility.
- Introduced loading state in SpecDiffModal with a spinner for improved user feedback during rendering.

* feat(OpenAPISpecTab): integrate fast-json-format for improved JSON rendering

- Replaced the JSON parsing and stringifying logic with fast-json-format for better performance in pretty-printing API specifications.
- Updated StyledWrapper in OpenAPISyncTab to change background and text colors for enhanced visual consistency.
- Modified DisconnectSyncModal button to include a secondary color for improved visibility during user interactions.

* fix(OpenAPISyncTab): correct punctuation in status messages and subtitles

- Removed unnecessary trailing periods in messages related to syncing and restoring specifications across CollectionStatusSection and OverviewSection components.
- Updated SyncReviewPage to correct grammatical error in the description of spec updates.

* fix(OpenAPISyncTab): update URL validation to use isHttpUrl

- Replaced isValidUrl with isHttpUrl in ConnectSpecForm and ConnectionSettingsModal components to ensure only valid HTTP URLs are accepted.
- Updated the logic for enabling the save button based on the new URL validation method.

* fix(OpenAPISyncTab): normalize source URL before validation

- Trimmed the source URL in ConnectionSettingsModal to ensure consistent validation with isHttpUrl.
- Updated state initialization for URL and filePath to use the normalized source URL, improving handling of user input.
2026-03-13 23:48:46 +05:30
Abhishek S Lal
1e25825e74 fix(collection-watcher): prevent crash when deleting collections (#7470)
* fix(collection-watcher): guard against events firing after collection deletion

When deleting an OpenAPI-synced collection, saveBrunoConfig() writes to
bruno.json which creates buffered chokidar events (80ms stabilityThreshold).
If the collection directory is removed before those events fire,
getCollectionFormat() throws "No collection configuration found" for each
.bru file in the collection.

Add fs.existsSync(collectionPath) guards in the change, unlink, and
unlinkDir handlers to bail out early when the collection root no longer exists.

* fix(workspaces): ensure collection watcher stops before deletion

Added logic to remove the collection from the watcher when deleting files, preventing chokidar from firing events on a directory that is being removed. This change enhances stability during collection deletions by ensuring the watcher is properly managed.

* refactor(workspaces): remove redundant collection watcher logic during deletion

Eliminated the logic for stopping the collection watcher before deletion, streamlining the action for removing collections from workspaces. This change simplifies the code and maintains functionality without unnecessary complexity.

* test(collection): add integration test for collection deletion functionality

Introduced a new test suite to verify the deletion of collections from the workspace overview. The test ensures that collections are properly removed from both the UI and the file system, confirming the absence of any uncaught errors during the deletion process. This addition enhances the test coverage for collection management features.

* feat(collection): implement deleteCollectionFromOverview utility function

Added a new utility function to delete a collection directly from the workspace overview page. This function encapsulates the steps required to navigate the UI, confirm deletion, and ensure the collection is removed from both the interface and the file system. Updated the corresponding test to utilize this new function, enhancing code reusability and test clarity.

* fix(collection-watcher): add guards for collection path existence and error handling

Enhanced the unlink and unlinkDir functions to check for the existence of the collection path before proceeding. Added error handling for the getCollectionFormat function to prevent crashes when the collection format cannot be retrieved. These changes improve stability and robustness during collection deletion operations.
2026-03-13 23:47:09 +05:30
Thomas
994d51b680 feat: remove .bru reference in error message (#7479)
Co-authored-by: Thomas Vackier <thomas.vackier@inthepocket.com>
2026-03-13 22:38:09 +05:30
naman-bruno
ab18a6ba84 feat: integrate deferred loading for saving state in DotEnvFileEditor (#7463) 2026-03-13 17:14:23 +05:30
Sid
a8542c7312 Replace SpaceX external API with local graphql-yoga mock server (#7471)
* chore: switch to locally hosted graphql server

* chore: additional graphql check

* chore: error handling
2026-03-13 16:00:08 +05:30
naman-bruno
ab8a730bc3 feat: implement temporary workspace creation and confirmation flow (#7462)
* feat: implement temporary workspace creation and confirmation flow

* fixes
2026-03-13 12:24:45 +05:30
Abhishek S Lal
c0a2d74789 Feat/openapi sync beta tag (#7461)
* feat: introduce OpenAPI Sync beta feedback feature

- Added a feedback section in the OpenAPISyncTab and ConnectSpecForm to encourage user input during the beta phase.
- Styled the feedback message and button for better visibility.
- Updated the beta features list to include OpenAPI Sync and adjusted related components to reflect its beta status with appropriate badges.
- Enhanced the StatusBadge component to support a new 'xs' size for better integration in various UI elements.

* feat: integrate OpenAPI Sync beta feature toggle

- Updated the ImportCollectionLocation component to conditionally enable the "Check for Spec Updates" option based on the OpenAPI Sync beta feature status.
- Modified default preferences to disable OpenAPI Sync by default, ensuring users are not prompted for updates unless explicitly enabled.

* feat: enhance beta features integration in Preferences

- Updated the BETA_FEATURES array to use constants from utils/beta-features for better maintainability.
- Improved the handling of beta preferences by merging new preferences with existing ones, ensuring a smoother user experience when toggling features.

* feat: enhance OpenAPI Sync polling with beta feature toggle

- Integrated a beta feature toggle for OpenAPI Sync polling, allowing conditional activation based on user settings.
- Updated the pollingEnabled logic to incorporate the new beta feature status, ensuring better control over sync behavior.
2026-03-13 11:29:04 +05:30
Pragadesh-45
b25b6f36bb refactor: simplify environment list actions and improve styling (#7459) 2026-03-12 21:41:11 +05:30
Sid
670f11be37 revert: feat(phase-1): allow user to customize keybindings#7163 (#7457)
* Revert "feat(phase-1): allow user to customize keybindings (#7163)"

This reverts commit 14532b48a6.

* Revert "chore: UI Polish for Zoom and Keybindings panel (#7376)"

This reverts commit 5151d29aac.
2026-03-12 20:48:16 +05:30
naman-bruno
ddb1c69fc9 Revert "workspace renaming with path update (#7437)" (#7455)
This reverts commit e7c2c7c872.
2026-03-12 18:40:52 +05:30
William Rodrigues
6f6a9100e9 removed button changed possition to make more acessibility (#7341) 2026-03-12 13:10:12 +05:30
lohit
7c58740c74 fix: cookie wrapper callback mode returns never-resolving Promise (#7442)
* fix: cookie wrapper callback mode returns never-resolving Promise

tough-cookie's createPromiseCallback() intentionally never resolves the
returned Promise when a callback is provided — only the callback fires.
The cookie jar wrapper was propagating this never-resolving Promise via
`return cookieJar.getCookies(url, cb)` in callback-mode paths. When user
scripts did `await jar.getCookie(url, name, callback)` in the Node VM
(developer sandbox), the await hung forever, blocking the CLI runner.

Fix: drop the return value in all callback-mode paths so the wrapper
returns void (undefined). `await undefined` resolves immediately.

Affected methods: getCookie, getCookies, hasCookie, clear, deleteCookies.

* fix: validation-error callback paths also return void instead of callback result

The validation guards (e.g. missing URL) did `return callback(error)` which
leaks whatever the user's callback returns. Apply the same pattern used for
the main callback paths: call the callback, then return void.

Also makes deleteCookie's `return executeDelete(callback)` consistent
(executeDelete already returns void, but the explicit pattern is clearer).
2026-03-11 20:56:09 +05:30
naman-bruno
e7c2c7c872 workspace renaming with path update (#7437)
* workspace renaming with path update

* fixes

* update: test

* fix: test
2026-03-11 19:07:38 +05:30
naman-bruno
f6ff8efabe refactor: update path imports to use utils/common/path (#7440) 2026-03-11 19:05:42 +05:30
Pooja
ce8775c75c fix: multipart header check (#7444)
* fix: multipart header check

* fix
2026-03-11 19:01:14 +05:30
Chirag Chandrashekhar
803b3f0d1f fix: normalize paths when comparing workspace and redux collection paths on Windows (#7436)
Without path normalization, collections appear stuck in "mounting" state on Windows
because workspace YAML uses forward slashes while Redux uses backslashes.

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-03-11 16:13:06 +05:30
Nizam Chaudhary
9bdd439472 feat(request-pane): restore body tab scroll position on tab switch (#7250)
* feat(request-pane): restore body tab scroll position on tab switch

When editing large request bodies (JSON/XML/text/sparql), switching to
another tab (params, headers, auth, etc.) and back would reset the
CodeMirror editor scroll position to the top.

Fix by persisting the scroll position to Redux on editor unmount (via
CodeEditor's onScroll prop) and restoring it on mount (via initialScroll),
mirroring the existing scroll restoration pattern in QueryResultPreview.

* test: add playwright tests for body scroll restoration

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
2026-03-10 19:01:55 +05:30
Abhishek S Lal
4d17809562 enhance OpenAPI sync with validation, enum support, and bug fixes (#7408)
* Enhance OpenAPISyncTab functionality with error handling and UI improvements

- Updated ConnectSpecForm to include error handling for invalid OpenAPI specifications when uploading files.
- Added a sync info notice in CollectionStatusSection to inform users about tracked changes.
- Improved styling in StyledWrapper for better visual feedback and layout consistency.
- Adjusted button colors and properties in ConfirmSyncModal and ConnectionSettingsModal for better UX.
- Refactored useOpenAPISync hook to validate URLs before syncing, ensuring only valid OpenAPI specs are processed.
- Enhanced parameter handling in openapi-to-bruno.js to support enum and default values more effectively.

* Refactor OpenAPISyncTab components for improved URL validation and error handling

- Updated ConnectSpecForm to streamline file upload error handling for OpenAPI specifications.
- Enhanced OpenAPISyncHeader to utilize isHttpUrl for better URL validation.
- Refactored useOpenAPISync hook to replace isValidUrl with isHttpUrl for consistency in URL checks.
- Improved file parsing logic in file-reader.js to handle case-insensitive JSON file extensions.
- Added isHttpUrl utility function to validate HTTP/HTTPS URLs effectively.

* Enhance file parsing logic in file-reader.js to improve error handling for JSON and YAML files

- Updated parseFileAsJsonOrYaml function to handle case-insensitive JSON file extensions more robustly.
- Added error handling to ensure the document root is an object and not an array, improving data validation.

* Update StatusBadge component to include new 'xs' size preset and adjust documentation accordingly

- Added 'xs' size preset with specific font size and padding for minimal use cases.
- Updated documentation to reflect the new size options available for the StatusBadge component.
2026-03-10 17:15:45 +05:30
sanish chirayath
f123a2b574 fix: app crash error (Rendered fewer hooks than expected) (#7407)
* fix:  app crash error (Rendered fewer hooks than expected)

* empty commit
2026-03-09 19:20:39 +05:30
shubh-bruno
facfe325b1 fix: allow edit keybinding shortcuts (#7404)
Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-03-09 17:17:42 +05:30
gopu-bruno
707fd405ff Fix: resolve default location missing path (#7391)
* fix: refocus collection name input on error and resolve missing default location folder

* revert: remove focus refocus logic from InlineCollectionCreator

Reverts the setTimeout focus change that was causing test failures.
Keeps the preferences.js fix for default location validation.

Made-with: Cursor

* fix: update default-collection-location test to use project path

Use {{projectRoot}} template variable for defaultLocation so the path
exists when the app validates it with fs.existsSync.

Made-with: Cursor

* fix: improve save test to use alternate path and verify persistence

Made-with: Cursor

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
2026-03-07 00:23:01 +05:30
naman-bruno
12ebfee9c6 fix: update collection path handling to use path.join for consistency across components (#7394) 2026-03-06 23:06:47 +05:30
Abhishek S Lal
553c45833c refactor: enhance OpenAPISyncTab functionality and clean up unused code (#7392)
- Updated OpenAPISyncTab to utilize Redux state for active tab management, improving state consistency.
- Removed unnecessary loading state checks from OverviewSection and SpecStatusSection for cleaner logic.
- Streamlined prop usage in OverviewSection by eliminating the isLoading prop.
- Cleaned up useOpenAPISync hook by removing unused state clearing logic on unmount.
- Improved file handling in openapi-sync IPC by ensuring new files are created in the appropriate folder based on tags.
2026-03-06 22:12:52 +05:30
lohit
af4c4b24e6 fix: preserve existing process.env values in initializeShellEnv (#7390) 2026-03-06 22:09:19 +05:30
gopu-bruno
1b8cee4706 fix: use Title Case for default "Untitled Collection" and "Untitled Workspace" (#7389) 2026-03-06 18:12:10 +05:30
shubh-bruno
5151d29aac chore: UI Polish for Zoom and Keybindings panel (#7376)
* chore: ui polishing for zoom panel

* fix: replace IconRefresh with IconReload in Zoom and Keybindings components

* chore: resolved ui issues

* test: check test cases

* fix: adjust width properties in StyledWrapper component

* fix: update keybindings UI layout and tooltip removal

* chore: semantics

---------

Co-authored-by: Sid <siddharth@usebruno.com>
Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-03-06 13:15:40 +05:30
Abhishek S Lal
1748741d7f refactor: simplify OpenAPISyncTab and related components by removing unused state and styles (#7378)
- Removed the `viewMode` state and associated logic from OpenAPISyncTab and useSyncFlow, streamlining the component's functionality.
- Eliminated the `review-active` class from StyledWrapper, cleaning up the styling.
- Updated Redux state management by removing `viewMode` from the tab UI state structure, reducing complexity.
2026-03-05 21:48:31 +05:30
Abhishek S Lal
b2f8b3bb5b Feat/opeanpi sync updates (#7374)
* fix: update button colors and streamline props in OpenAPISyncTab and CollectionHeader

- Changed button color from 'warning' to 'primary' in OpenAPISyncTab for better visual consistency.
- Simplified prop usage for OpenAPISyncIcon in CollectionHeader by removing unnecessary function wrapper.

* fix: update StatusBadge variant in OpenAPISyncHeader for improved styling

- Changed StatusBadge in OpenAPISyncHeader to use 'outline' variant for better visual distinction of version information.
2026-03-05 19:16:13 +05:30
lohit
f5e437adaf fix: enable SSL session caching and HTTP agent reuse for faster consecutive requests (#6987)
* fix: enable SSL session caching for faster consecutive requests (#6929)

* fix: enable SSL session caching for faster consecutive requests

Previously, Bruno created a new HTTPS agent for every request, which meant
SSL/TLS sessions couldn't be reused. This caused the full TLS handshake
(~450ms) to run on every request, even to the same endpoint.

Changes:
- Add agent caching based on TLS configuration (certs, proxy, SSL options)
- Reuse cached agents for requests with matching configuration
- SSL sessions are now cached and reused, significantly reducing
  response time for consecutive requests to the same host

The fix maintains backward compatibility:
- Timeline logging moved to setup phase (before agent creation)
- Proxy and SSL validation behavior unchanged
- Added clearAgentCache() for testing and configuration changes

Fixes #5574

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address review feedback for SSL session caching

- Add passphrase to cache key to prevent incorrect agent reuse
- Add MAX_AGENT_CACHE_SIZE (100) with LRU-style eviction
- Use consistent node: prefix for crypto import

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: lohit <lohit@usebruno.com>

* feat(bruno-requests): add timeline agent for TLS event logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(bruno-requests): add agent cache for SSL session reuse

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(bruno-requests): add tests for agent cache

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(bruno-requests): integrate agent cache into http-https-agents

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(bruno-electron): use shared agent cache from bruno-requests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(bruno-cli): use agent cache for SSL session reuse

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(bruno-requests): add HTTP agent timeline support

Add createTimelineHttpAgentClass for logging HTTP connection events
including proxy usage, DNS lookups, and connection establishment.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(bruno-requests): extract shared agent caching logic

Add getOrCreateAgentInternal helper to reduce code duplication
between getOrCreateAgent and getOrCreateHttpAgent functions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(bruno-requests): use HTTP agent cache for connection reuse

Export getOrCreateHttpAgent and use it in http-https-agents for
HTTP requests to enable connection pooling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(bruno-cli): improve HTTP agent handling and error logging

- Use { keepAlive: true } instead of tlsOptions for HTTP agents
- Add warning log for system proxy configuration errors
- Fix brace style consistency

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(bruno-electron): improve HTTP agent handling

- Use { keepAlive: true } instead of tlsOptions for HTTP agents
- Fix brace style consistency
- Add missing newline at EOF

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(bruno-requests): address code review findings for agent caching

- Fix Buffer hashing bug: properly handle Buffer values in hashValue()
- Add CA array support: new hashCaValue() handles string[] | Buffer[]
- Fix timeline race condition: capture timeline reference in closure
  at createConnection start to isolate concurrent requests
- Fix SSL verify message: check socket.authorized for accurate status
- Fix HTTP/HTTPS agent logic: only set httpsAgent for HTTPS requests
- Add tests for concurrent requests timeline isolation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(bruno-requests): log when reusing cached agent

- HTTPS agents: "Reusing cached agent (SSL session reuse enabled)"
- HTTP agents: "Reusing cached agent (connection reuse enabled)"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(preferences): add cache.httpHttpsAgents.enabled preference

* feat(agent-cache): add disableCache option to getOrCreateAgent

* feat(proxy-util): respect httpHttpsAgents cache preference

* refactor(agent-cache): use named props for getOrCreateAgent and getOrCreateHttpAgent

* feat(ipc): add renderer:clear-http-https-agent-cache handler

* feat(redux): add cache.httpHttpsAgents preferences to initial state

* feat(ui): add Cache tab to Preferences

* feat(cli): add --disable-http-https-agents-cache flag

* refactor(cache): replace window.ipcRenderer calls with redux actions

Add getCacheStats, purgeCache, and clearHttpHttpsAgentCache thunks to
the app slice. Update the Cache preferences component to dispatch these
actions instead of calling window.ipcRenderer directly.

Also move handleSave and handleSaveRef above useFormik to fix declaration
order — onSubmit closes over handleSaveRef, so the ref must be initialized
before useFormik is called.

* fix: tests

* fix(cache): thread disableCache and hostname through all agent-creation paths

- Forward disableHttpHttpsAgentsCache through getHttpHttpsAgents → createAgents
  so OAuth2 token requests and bru.sendRequest honour the CLI flag
- Add hostname to agent cache keys (getAgentCacheKey, getHttpAgentCacheKey)
  for per-host TLS session reuse; extract hostname at every call site in
  run-single-request.js, proxy-util.js, and http-https-agents.ts
- Add extractHostname helper in http-https-agents.ts to safely parse hostnames
- Add test coverage for cert, key, pfx, passphrase, and hostname cache-key
  differentiation in agent-cache.spec.ts

* refactor(cache): rename getOrCreateAgent to getOrCreateHttpsAgent

* refactor: simplify UI labels, optimize agent timeline wrapping, silence proxy errors

* fix: tests

* fix(proxy): fix proxy agent construction and CA cert handling

Three fixes:

1. Proxy agents (HttpsProxyAgent, HttpProxyAgent, SocksProxyAgent) expect
   (proxyUri, options) constructor signature, but the agent cache was packing
   proxyUri into options as a single argument. Fixed the non-timeline code
   path in getOrCreateAgentInternal.

2. HTTP requests through an HTTPS proxy need TLS options (ca certs) to
   validate the proxy's certificate. All getOrCreateHttpAgent call sites
   now pass TLS options when the proxy protocol is HTTPS.

3. Setting the `ca` option on any Node.js TLS connection replaces the
   default OpenSSL trust store entirely. CAs only in the OpenSSL default
   trust store (e.g. /etc/ssl/cert.pem) but not in tls.rootCertificates
   were lost. Fixed by converting `ca` to a secureContext via addCACert(),
   which appends custom CAs on top of the OpenSSL defaults instead of
   replacing them.

Also simplified PatchedHttpsProxyAgent to selectively forward only the
relevant TLS options (cert, key, pfx, passphrase, rejectUnauthorized,
secureContext) to the target TLS upgrade instead of blindly merging all
constructor options.

* fix(tls): load client certs into secureContext to prevent silent drop

Add Cache tab to Preferences UI

* fix(proxy): align proxy auth check to use auth.disabled field consistently

* refactor(cache): rename CLI flag to --cache-ssl-session and disable caching by default

- Rename --disable-http-https-agents-cache to --cache-ssl-session (opt-in)
- Rename disableHttpHttpsAgentsCache to cacheSslSession across CLI and bruno-requests
- Default caching to disabled in both bruno-electron and bruno-cli
- Add cacheSslSession to buildCertsAndProxyConfig for bru.sendRequest
- Update Preferences UI labels to "Cache SSL Session"

* refactor(cache): rename httpHttpsAgents to sslSession across preferences and UI

* refactor(cache): remove unused getCacheStats and purgeCache IPC actions

---------

Co-authored-by: karthik <47263234+kxbnb@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 18:46:20 +05:30
Abhishek S Lal
39f8ce2a2f feat: enhance OpenAPI Sync tab with sync status indicators and improved styling (#7371)
- Added sync status logic to determine if the collection is in sync with the spec, displaying appropriate indicators in the UI.
- Updated OpenAPISyncHeader to include linked collection information and sync status icons.
- Refined styling in StyledWrapper for better layout and visual feedback on sync status.
- Removed unused styles and components to streamline the codebase.
2026-03-05 17:23:31 +05:30
Abhishek S Lal
5944a9cf06 feat/openapi sync (#7279)
* feat: implement OpenAPI Sync

* feat: enhance focus styles and error handling across components

- Added focus-visible styles for buttons and tags in Swagger and Modal components to improve accessibility.
- Updated ConnectSpecForm to ensure source URL is set only if the file path is valid.
- Enhanced clipboard copy functionality in SpecInfoCard with error handling and success notifications.
- Improved ExpandableEndpointRow to handle loading state more robustly.
- Refined SyncReviewPage to ensure correct filtering of updated endpoints.
- Updated file handling in OpenAPI Sync IPC to support both .yml and .yaml extensions.

* fix: improve filename sanitization in OpenAPI Sync IPC

- Updated filename sanitization logic to ensure proper handling of both `name` and `filename` properties, enhancing compatibility with various file formats.
- Adjusted the logic to derive the base name from the filename when necessary, ensuring consistent output for generated files.

* feat: enhance OpenAPI Sync tab with new overview and header components

- Introduced OverviewSection to display summary of collection and spec status, including total endpoints, in-sync counts, and pending updates.
- Added OpenAPISyncHeader for improved navigation and actions related to the OpenAPI spec.
- Updated CollectionStatusSection to better handle and display collection drift information.
- Refined styling for status banners and added new visual elements for better user experience.
- Enhanced tooltip functionality in Help component for improved accessibility.

* refactor: remove VisualDiffViewer components and add diff package

- Deleted VisualDiffViewer components including VisualDiffMeta, VisualDiffDocs, VisualDiffVars, and others to streamline the codebase.
- Introduced the 'diff' package in package-lock.json to enhance diff functionality.
- Updated utility functions to improve diff status handling and maintainability.
2026-03-05 02:25:08 +05:30
gopu-bruno
fb65edea9e fix: focus and text selection in workspace creation flow (#7363)
* fix: workspace creation rename focus

* refactor: remove duplicate focus/select timer from rename handler
2026-03-05 02:14:56 +05:30
gopu-bruno
84d8051c18 fix: show '+ Add request' when only transient items exist (#7361) 2026-03-04 23:05:06 +05:30
Chirag Chandrashekhar
0b7cd0e540 Revert "Performance/file parse and mount (#6975)" (#7360)
* Revert "Performance/file parse and mount (#6975)"

This reverts commit f76f487211.

* fix: import duplication

* Revert "fix(batch-events): fix order of directory file and folder events (#7300)"

This reverts commit bf4af42a25.

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
Co-authored-by: Sid <siddharth@usebruno.com>
2026-03-04 19:20:26 +05:30
sanish chirayath
17c3dc0e2b refactor: comment out unused APIs (#7323)
* refactor: comment out unused API hints in autocomplete.js

* refactor: comment out unused API translations in postman and bruno translators

Temporarily disable certain API translations due to UI update issues affecting their functionality. A note has been added to restore these translations once the UI fixes are implemented.

* refactor: temporarily skip tests for collection variable translations due to UI update issues

Commented out tests related to `setCollectionVar`, `deleteCollectionVar`, and related functionalities until the necessary UI updates are implemented. A note has been added to restore these tests once the fixes are live.

* refactor: comment out variable deletion and retrieval methods due to UI sync issues

* revert: ping.bru

* refactor: update postman translation tests to enable previously skipped cases
2026-03-04 18:00:10 +05:30
Bijin A B
75c3ab8032 chore: update coderabbit instructions to make sure the code is os agnostic (#7355) 2026-03-04 13:50:08 +05:30
gopu-bruno
6d86c76b21 feat: inline create collection and workspace editor (#7324)
* feat: inline create collection and workspace editor

* refactor: use inline collection creation from workspace overview

* fix: improve inline collection creation UX from workspace overview

* fix: update E2E tests for inline collection creation flow

* fix: update default location test for inline collection creation flow

* fix: derive inline workspace/collection names from filesystem

* feat: inline workspace create form manage workspace

* feat: prefill create modal with name

* fix: minor code style fixes

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
2026-03-04 13:25:30 +05:30
Niklas Krebs
7218c66d5a Fix persistence of additional parameters using open-collection format (#7296)
Co-authored-by: TaylorJerry <47517046+TaylorJerry@users.noreply.github.com>
Co-authored-by: MrcBoo <63651816+MarcBoo@users.noreply.github.com>
2026-03-04 10:14:23 +05:30
Abhishek S Lal
e0dd79418b feat: enhance API spec export with environment variables support (#7170)
* feat: enhance API spec export with environment variables support

- Updated `exportApiSpec` function to accept and process environment variables for multi-server exports.
- Added logic to convert environment variables into a structured format for OpenAPI server entries.
- Enhanced the `CreateApiSpec` component to include environments in the exported YAML content.
- Introduced unit tests to validate the handling of server variables and their integration into the exported API specifications.

* refactor: streamline API spec export logic and improve variable handling

- Simplified variable extraction in `exportApiSpec` by directly assigning capture groups.
- Updated URL interpolation to use request variables instead of global variables for better accuracy.
- Enhanced handling of request body types by replacing early returns with breaks for clearer flow control.
- Adjusted tests to ensure backward compatibility with OpenAPI specifications and server variable handling.

* refactor: improve variable handling and URL processing in OpenAPI exporters

- Streamlined server variable assignment in `exportApiSpec` to handle undefined values more gracefully.
- Enhanced URL path extraction to ensure leading slashes are preserved in `getDefaultUrl` and `extractServerVars`.
- Updated string replacement logic to use `replaceAll` for consistent variable substitution in URLs.
2026-03-03 21:24:01 +05:30
Chirag Chandrashekhar
574324e784 feat: add collection creation flow in SaveTransientRequest modal (#7328) 2026-03-03 19:24:20 +05:30
Pooja
caf073c185 fix: file extension for clone and rename request (#7278)
* fix: file extension for clone and rename request

* fix
2026-03-03 19:12:59 +05:30
shubh-bruno
14532b48a6 feat(phase-1): allow user to customize keybindings (#7163)
* feat(phase-1): allow user to customize keybindings

* fix: necessary changes for customizied keybindings to work

* fix: updated hotkeys provider

* fix: test cases for edit keybindings in preferences

* fix: removed old keyboard shortcuts test cases

* fix: resolved coderabbit coments

* fix: fixed move tabs test cases

* feat: provided customized keybindings shorcut for codemirror instacnces

* fix: handle closetabs/closeAllTabs in RequestTab/index.js for better consitency

* fix: resolved comments

* fix: resolved zoom issues

* fix: revert codemirror instacnces

* fix: handle codemirror instances shortcut in .Pass

* feat: integrate shorcut keys with codemirror instacneces

* fix: ui updates

* fix: updated shortcuts

* fix: test cases

* chore: revert `alt-enter` keybind

* chore: allow jest to replace esm spec in store

* chore: lint whitespace fix

---------

Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-03-03 18:49:18 +05:30
naman-bruno
e42b015867 refactor: streamline onboarding process and preferences handling (#7349)
* refactor: streamline onboarding process and preferences handling

* fixes

* fixes
2026-03-03 16:41:42 +05:30
gopu-bruno
e159a442d0 feat(sidebar): show "Add request" cta when collection or folder is empty (#7273)
* feat(sidebar): show "Add request" cta when collection or folder is empty

* fix: add connector lines and keyboard accessibility to empty-state CTA

* fix: debounce empty collection state to prevent flicker

Add 300ms delay before showing "Add request" button for empty collections.
This fixes a race condition where isLoading becomes false before the
items batch arrives from IPC, causing a brief flicker of the empty state.

Made-with: Cursor

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
2026-03-03 16:40:42 +05:30
naman-bruno
4b15b14cf7 feat: add functionality to create new HTTP requests from the welcome modal and collections section (#7350) 2026-03-03 14:42:39 +05:30
shubh-bruno
ca0412b58b fix: allow user to delete default bruno headers in pre-request (#7331)
* fix: allow user to delete default bruno headers

* fix: resolved comments

---------

Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-03-03 14:35:54 +05:30
lohit
bba0e97435 fix: ensure system proxy is initialized before use in network calls (#7264)
getCachedSystemProxy now awaits the initialization promise, preventing a race
condition where API calls made early in startup would bypass the system proxy.
2026-03-03 14:28:12 +05:30
shubh-bruno
834a4fe020 fix: resolved zoom ui remarks (#7326)
* fix: resolved zoom ui remarks

* fix: resolved comments

* fix: resolved comments

---------

Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-03-03 13:28:06 +05:30
gopu-bruno
910581a627 feat: improve stack traces for script and test failures (#7181)
* chore: update package-lock.json to include yaml dependency

* feat: add script-aware stack traces and source context for script/test failures

* chore: sync package-lock.json with yaml dependency in bruno-js

* fix: handle null check in getErrorTypeName and align JSDoc style

* refactor: derive script path from request.pathname and use SCRIPT_TYPES constant

* fix: avoid showing source context for collection/folder script errors

* feat: map collection/folder script errors to source file and line

* fix: update error formatting and avoid undefined message

* fix: resolve script block location in collection/folder yml files

* refactor: use script wrapper utils and rename wrapper offsets

* refactor: move script wrapper to utils, add wrapScriptInClosure fn
2026-03-02 15:27:18 +05:30
lohit
4797abbeff feat: add tokenType support for OAuth2 (#7314)
* feat: add tokenType support for OAuth2

* refactor: rename tokenType to source in OpenCollection OAuth2 mapping

* refactor: rename tokenType to source in OAuth2 configuration

* chore: bump @opencollection/types to ~0.8.0

* fix: correct OAuth2 token type label in token viewer

* refactor: replace Dropdown with MenuDropdown in OAuth2 components

Migrate all 12 dropdown instances across 5 OAuth2 auth components to use
the MenuDropdown component, removing manual tippy ref management and
forwardRef icon patterns in favor of a declarative items-based API.
2026-02-27 20:50:23 +05:30
lohit
4f4faec359 fix(oauth2): prevent false callback matches on root path URLs (#7315)
* fix(oauth2): prevent false callback matches on root path URLs and handle errors first

Move error check before callback URL matching in onWindowRedirect so
OAuth error responses are rejected immediately. Remove redundant error
param check from matchesCallbackUrl and require a code param or hash
fragment to match, preventing false positives on intermediate pages
when the callback URL is a root path like https://hostname/.

* fix(oauth2): clarify error handling comment

Remove "on the callback URL" from comment since error checking
now happens before callback URL matching.
2026-02-27 20:48:51 +05:30
Chirag Chandrashekhar
a9709fb82a fix: Postman import compatibility for multipart form-data file params (#7325)
* fix: multipart form-data file param export/import for Postman

* fix: Postman import compatibility for multipart form-data file params

This commit fixes two issues that caused Postman to fail importing
Bruno-exported collections with multipart form-data file parameters:

1. Changed `src` field format to match Postman's export format:
   - Single file: export as string (e.g., "/path/to/file")
   - Multiple files: export as array (e.g., ["/path/a", "/path/b"])
   - Empty/null: export as null

   Previously, Bruno always exported `src` as an array, but Postman's
   importer expects a string for single files and fails to recognize
   the file type and path when given an array.

2. Added `protocolProfileBehavior.disableBodyPruning` for GET/HEAD/OPTIONS
   requests that have a body:

   By default, Postman discards request bodies for HTTP methods that
   typically don't have bodies (GET, HEAD, OPTIONS). Without this flag,
   importing a GET request with a form-data body would result in the
   body being silently dropped, making Postman unable to identify that
   the request has a formdata body at all.

Both changes align Bruno's Postman export format with Postman's own
export format, ensuring full import compatibility.

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-02-27 17:30:20 +05:30
Melroy van den Berg
5e75bc5fcb Update copyright year to 2026 (#7302)
* Update copyright year to 2026

* Update footer year to current year dynamically
2026-02-27 16:24:16 +05:30
naman-bruno
c8e57b7f9f feat: implement onboarding preferences and welcome modal for new users (#7319)
* feat: implement onboarding preferences and welcome modal for new users

* fixes

* adding: defaultPreferences

* fixes

* fix: tests

* fixes

* fix: test

* fix: test

* fixes

* fixes
2026-02-27 16:15:06 +05:30
sanish chirayath
8b230043c1 Enable encodeUrl setting to control URL encoding in generated snippets (#7187)
* feat(snippet-generator): implement encodeUrl setting to control URL encoding in generated snippets

* refactor(snippet-generator): rename and enhance URL encoding logic for better clarity and functionality

* feat(snippet-generator): enhance raw URL handling to preserve user encoding choices and improve snippet generation

* test(snippet-generator): add tests for URL fragment handling based on encodeUrl setting

* test(snippet-generator): improve comments on URL fragment handling to clarify RFC compliance

* feat(url): enhance interpolateUrlPathParams to support raw URL handling, preserving user encoding choices for snippet generation

* fix(url): ensure URLs are prefixed with http:// if missing in interpolateUrlPathParams function

* refactor(snippet-generator): streamline URL handling logic to improve snippet generation and ensure proper encoding based on settings

* feat(url): add stripOrigin utility to simplify URL processing in snippet generation

* test(snippet-generator): add test for double-encoding of pre-encoded URLs when encodeUrl is true

* feat(encoding): implement URL encoding settings and add tests for encoding behavior

* fix: address PR review comments (#7187)

- Remove unnecessary no-op jest.mock for @usebruno/common/utils
- Add length guard to prevent catastrophic replaceAll('/') on root-path URLs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* empty commit

* fix(tests): update interpolateUrlPathParams tests to use correct parameter structure

* empty commit

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:46:24 +05:30
Sanjai Kumar
5dd684f7a3 fix: wrong workspace request shown after closing tab (#7259)
* feat: add ensureActiveTabInCurrentWorkspace action and improve tab management

* refactor: enhance tab focus logic for current workspace and improve tab management

* tests: implement test for getTabToFocusForCurrentWorkspace logic and added a playwright

* refactor: improve comments for tab management logic and enhance workspace tab focus handling in tests

* refactor: ensure active tab in current workspace after collection removal and enhance tests for tab focus logic

* trigger build
2026-02-27 15:38:13 +05:30
austenadler
27e22bd857 Force text/plain mimetype when copying request code (#7321)
* Force text/plain mimetype when copying requests

* chore: lint

---------

Co-authored-by: Austen Adler <agadler@austenadler.com>
2026-02-27 14:11:04 +05:30
shubh-bruno
7a652503b6 fix: tags validation error for openapi import for BRU and YAML compatibility (#7294)
* fix: tags schema updatd to array of string

* tests: added test cases for sanitizing tags, openapi-tags

* fix: enhance tag sanitization to support object input and UTF characters

---------

Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-02-27 14:04:28 +05:30
Sid
bf4af42a25 fix(batch-events): fix order of directory file and folder events (#7300)
* fix: order of events

* fix: update constants handling in CollectionTreeBatcher and related tests
2026-02-27 13:43:15 +05:30
naman-bruno
39d6999cb2 fix: prevent triggering rename action with modifier keys (#7322) 2026-02-27 12:56:39 +05:30
Nikhil
3fdb81849c fix#6247: Interpolate dynamic variables in path param (#6251) 2026-02-27 12:21:32 +05:30
Miro Metsänheimo
fcfb7d409c fix(schema): support all Unicode letters in tag validation (#7311)
* allow international characters in tag regex for UI and schema
  validation
* update validation messages to match this

Co-authored-by: Miro Metsänheimo <miro.metsanheimo@cgi.com>
2026-02-26 21:56:02 +05:30
Chirag Chandrashekhar
da1d7e51d2 fix(graphql): handle invalid schemas gracefully in query editor (#7269)
Prevent app crashes when loading GraphQL schemas with validation errors
(e.g., object types with no fields). The fix:

- Validates schemas using validateSchema() and shows warnings for issues
- Still loads the schema so autocomplete continues to work
- Wraps the CodeMirror GraphQL linter with error handling to catch
  any validation errors during linting

Fixes #4529

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-02-26 18:29:19 +05:30
Pragadesh-45
b0d0e4aabc Feat: Support multipart/mixed (#7155)
* feat(): support multipart mixed

fix: support vars interpolation on mixed multi-part

Update packages/bruno-electron/src/ipc/network/interpolate-vars.js

Co-authored-by: Timon <39559178+Its-treason@users.noreply.github.com>

refactor: use startsWith

feat: best effort for other multipart/* contentypes

* feat: enhance variable interpolation for multipart requests

- Updated `interpolateVars` function to support interpolation in multipart/form-data and multipart/mixed requests.
- Added handling for empty multipart arrays and parts with missing or undefined values.
- Improved type checks for content types to ensure proper interpolation behavior.

Includes new tests to validate the interpolation functionality for multipart requests.

* fix: normalize error handling in sendRequest and improve test reliability

---------

Co-authored-by: Alfonso Presa <alfonso-presa@users.noreply.github.com>
2026-02-26 17:43:37 +05:30
shubh-bruno
234d0df449 fix: storing status in example for yml file (#6876)
* fix: storing status in example for yml file

* fix: temporary check for tests

* fix: temporary check for tests

* fix: temporary check for tests

* fix: temporary check for tests

* fix: temporary check for tests

* fix: temporary check for tests

* fix: temporary check for tests

* fix: temporary check for tests

* fix: temporary check for tests

* fix: temporary check for tests

* fix: temporary check for tests

* fix: temporary check for tests

* fix: test cases for status and statusText

* chore: removed logs

* fix: test cases for response status and text

* fix: test cases for response status and text

* fix: resolved comments

* fix: openapi test import test cases

* chore: removed console logs

* fix: status type in response example while import/export of collection

* fix: postman to bruno import

---------

Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-02-26 17:33:02 +05:30
gopu-bruno
8ce38e8480 feat: change default collection location to default location (#7291)
* feat: change default collection location to default location

* refactor: migrate defaultCollectionLocation to defaultLocation in preference.json

* refactor: resolveDefaultLocation function

* fix: rename variables in default-location
2026-02-26 16:10:56 +05:30
Pragadesh-45
4d61ecacb3 Fix Environment Search Behavior, UI Updates, and Result Handling (#7287) 2026-02-26 12:27:03 +05:30
Chirag Chandrashekhar
81a7544853 feature: added support for unix sockets and named pipes(windows) for grpc local IPC support (#7021) 2026-02-25 19:18:33 +05:30
Chirag Chandrashekhar
f76f487211 Performance/file parse and mount (#6975)
* Refactor: optimize collection updates with batch processing

- Introduced BatchAggregator to handle IPC events in batches, reducing Redux dispatch overhead during collection mounting.
- Updated collection watcher to utilize batch processing for adding files and directories, improving UI performance.
- Implemented ParsedFileCacheStore using LMDB for efficient caching of parsed file content, enhancing loading speed and reducing redundant parsing.
- Adjusted collection slice to support batch addition of items, minimizing re-renders and improving state management.
- Updated relevant components to reflect changes in loading states and collection data handling.

* feat: add cache management to preferences

- Introduced a new Cache component in the Preferences section to display cache statistics and allow users to purge the cache.
- Implemented IPC handlers for fetching cache stats and purging the cache in the Electron main process.
- Added styled components for better UI presentation of cache information.
- Updated Preferences component to include a new tab for cache management.

* fix: update package-lock.json to change 'devOptional' to 'dev' for several Babel dependencies

* refactor: update batch aggregation parameters for improved performance

- Increased DISPATCH_INTERVAL_MS from 150ms to 200ms for better timing control.
- Adjusted MAX_BATCH_SIZE from 200 to 300 items to enhance batch processing efficiency.

* feat: enhance collection loading state and improve batch aggregator functionality

- Added isLoading property to collections slice to manage loading state during collection operations.
- Updated getAggregator function calls in collection-watcher to include collectionUid for better context in batch processing.
- Normalized directory path handling in parsed-file-cache to ensure consistent prefix creation for cache keys.

* fix: update loading state and transient file handling in collections slice

- Changed isLoading property to false during collection initialization for accurate loading state representation.
- Introduced isTransient flag for directories and files to differentiate between transient and non-transient items.
- Enhanced logic for handling transient directories and files during collection processing to improve state management.

* feat: add batch processing support for file additions in task middleware

- Implemented a new listener for collectionBatchAddItems to handle batch file additions.
- Enhanced task management by checking for pending OPEN_REQUEST tasks that match added files.
- Improved tab management by dispatching addTab actions for matching files and removing corresponding tasks from the queue.

* feat: enable ASAR packaging and unpacking for LMDB binaries in Electron build configuration

- Added ASAR support to the Electron build configuration for the Bruno application.
- Specified unpacking rules for LMDB native binaries to ensure proper loading during runtime.

* feat: implement parsed file cache using IndexedDB for improved performance

- Introduced a new `parsedFileCacheStore` utilizing IndexedDB for caching parsed file data.
- Replaced the previous LMDB-based cache implementation to enhance performance and reliability.
- Updated IPC handlers to manage cache operations such as get, set, invalidate, and clear.
- Integrated the new cache store into various components, ensuring efficient data retrieval and storage.
- Added pruning functionality to remove outdated cache entries on startup.

* refactor: update collection root and item handling to preserve UIDs

- Modified the way collection roots and folder items are assigned by using `mergeRootWithPreservedUids` and `mergeRequestWithPreservedUids` to ensure UIDs are maintained during updates.
- This change enhances data integrity when managing collections and their associated files.

* refactor: pass mainWindow reference to parsedFileCacheStore initialization

- Updated the `initialize` method in `ParsedFileCacheStore` to accept a `mainWindow` parameter, allowing for direct access to the main window instance in IPC handlers.
- This change improves the handling of IPC requests by ensuring the correct window context is used for sending messages.

* refactor: optimize getStats method in parsedFileCacheStore for performance

- Replaced manual counting of total files with a direct count() call for O(1) performance.
- Updated the collection counting logic to utilize openKeyCursor with 'nextunique' for improved efficiency in counting unique collection paths.
- These changes enhance the performance of the getStats method by reducing the complexity of file and collection counting.

* fix: update key generation in parsedFileCache to use newline separator

- Changed the key generation logic in `generateKey` from a null character to a newline character for improved readability and consistency in cache keys.

* refactor: rename batch-aggregator to collection-tree-batcher and add tests

- Rename BatchAggregator class to CollectionTreeBatcher
- Rename getAggregator/removeAggregator to getBatcher/removeBatcher
- Update imports and variable names in collection-watcher.js
- Add backward-compatible aliases for old names
- Add 22 unit tests covering all functionality

* refactor: update key generation in parsedFileCache to use custom separator

- Changed the key generation logic in `generateKey` to use a custom separator (↝) instead of a newline character for improved readability and consistency in cache keys.

* fix: add missing reject handler and fix directory prefix collision

- Add reject to Promise and pendingRequests in parsed-file-cache-idb.js
- Normalize dirPath with trailing separator in invalidateDirectory to
  prevent false matches (e.g., /foo/bar matching /foo/barley)
- Use platform-specific path.sep for cross-platform compatibility

* fix: add error handling in parsedFileCache and update window close event

- Added a catch block to handle errors in the database promise in parsedFileCache.
- Updated the window close event listener in collection-tree-batcher to use `once` for better resource management.

* fix: add LRU eviction when IndexedDB quota is exceeded

Handle QuotaExceededError in setEntry by automatically evicting
the oldest 20% of cache entries and retrying the write operation.

* fix: use once instead of on in mock window for batcher tests

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-02-25 19:15:48 +05:30
lohit
d3da8a3021 fix: default normalizeProxyUrl to http protocol for all proxy URLs (#7285) 2026-02-25 17:31:05 +05:30
Abhishek S Lal
757b635b0d feat: add options to skip request and response bodies in reporter output (#7114)
* feat: add options to skip request and response bodies in reporter output

- Introduced `--reporter-skip-request-body` and `--reporter-skip-response-body` flags to omit respective bodies from the reporter output.
- Updated examples in the CLI documentation to reflect new options.
- Refactored result sanitization to handle new flags.

* feat: add shorthand option to skip both request and response bodies in reporter output

- Introduced `--reporter-skip-body` as a shorthand for omitting both request and response bodies from the reporter output.
- Updated CLI documentation examples to include the new shorthand option.
- Adjusted result sanitization to accommodate the new option.

* refactor: simplify documentation and tests for reporter-skip-body option

- Updated the description of the `--reporter-skip-body` option to remove redundancy.
- Removed outdated shorthand references from the test suite for clarity.
- Cleaned up examples in the CLI documentation to focus on the current functionality.

* fix: handle optional chaining for request and response properties in result sanitization

- Updated the `sanitizeResultsForReporter` function to use optional chaining when accessing request and response headers and data.
- This change prevents potential errors when these properties are undefined.

* test: enhance reporter-skip-body tests for JSON and HTML outputs

- Added comprehensive tests for the `--reporter-skip-request-body` and `--reporter-skip-response-body` options in both JSON and HTML report formats.
- Verified that the appropriate request and response bodies are included or excluded based on the specified flags.
- Improved test coverage for scenarios where both flags are used simultaneously.

* fix: remove optional chaining for request and response headers in result sanitization

- Updated the `sanitizeResultsForReporter` function to directly assign empty objects to request and response headers, ensuring consistent behavior regardless of their initial state.
- This change simplifies the code and maintains functionality for skipping headers.
2026-02-25 16:35:48 +05:30
Timon
0045b16e06 perf: Improve search performance in code editor (#6920)
* perf: Improve search performance in code editor

Added cache key to reduce duplicate searches over the complete text.
Added incremental update logic, to not update every marked entry when
clicking "next" or "previous".

Fixes: https://github.com/usebruno/bruno/issues/6913

* Update delimeter to unicode symbol
2026-02-25 14:37:00 +05:30
sanish chirayath
e950640205 fix: skip null query parameters in Postman to Bruno conversion (#7193)
* fix: skip null query parameters in Postman to Bruno conversion

Updated the importPostmanV2CollectionItem function to skip query parameters where both key and value are null. Added a test case to ensure that such parameters are not included in the converted Bruno collection, while preserving other valid parameters.

* fix: skip null parameters in Postman to Bruno conversion

Updated the importPostmanV2CollectionItem function to skip headers, URL-encoded parameters, and form data where both key and value are null. Added corresponding test cases to ensure proper handling of these scenarios in the conversion process.
2026-02-25 13:30:26 +05:30
Pooja
ce15fbb6df fix: phone number faker function (#7046) 2026-02-25 12:13:46 +05:30
naman-bruno
8d301df329 fix: normalize collection pathnames in EnvironmentSecretsStore (#7283)
* fix: normalize collection pathnames in EnvironmentSecretsStore

* fix

* fix
2026-02-25 01:20:31 +05:30
Pooja
5c0a49af10 fix: handle special characters in collection path for dotenv watcher (#7190)
* fix: handle special characters in collection path for dotenv watcher

* fix
2026-02-25 00:00:10 +05:30
Sanjai Kumar
ade4bfb7e1 fix: response viewer not updating when focused (read-only editors) (#7218)
* fix: update cursor state handling in CodeEditor for read-only mode

* tests: add response pane update test for re-sent requests

* refactor: add data-testid attributes for improved testing in RequestBody and RequestBodyMode components; update locators and tests accordingly

* test: verify response status code is 200

* test: enhance response pane update tests to verify body editor content after request re-sent

* test: add data-testid for method selector and update locators for improved testability
2026-02-23 20:21:43 +05:30
Bijin A B
4e2303ecf3 chore: fix tab selection (#7260) 2026-02-23 19:51:58 +05:30
Pooja
5bca0cdd84 fix: openapi cli import (#7028)
* fix: openapi cli import

* chore: seperate bru and opencollection by a flag

* fix: pass down format correctly

* fix: pass format option correctly in collection tests

* Add opencollection version for YAML format

Set opencollection version for YAML format.

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2026-02-23 18:53:03 +05:30
shubh-bruno
89bf2fbf44 feat: interface zoom control settings (#7255)
* feat: interface zoom control settings

* fix: allow zoom controls using shortcuts

* fix: maintain consitency in zoom shortcuts and ui interface

* fix: added min max to 50% and 150% for zoom

* fix: moved percentageToZoomLevel function in bruno-common

* chore: abstractions

---------

Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-02-23 16:32:34 +05:30
Bijin A B
04ef477f3b feat(CI): refactor github workflow for tests (#7252) 2026-02-22 16:51:10 +05:30
naman-bruno
689e0c6573 fix: window normlize path comparison (#7240) 2026-02-20 16:04:17 +05:30
Sanjai Kumar
71227224dd fix: collection reorder not persisting after restart (#7093)
* feat: implement workspace collection reordering functionality

feat: add tests and improve collection reordering functionality

fix: handle IPC errors during collection reordering and update tests for persistence validation

refactor: enhance collection reordering logic and update related tests for path consistency

fix: prevent unnecessary promise resolution in collection reordering when no collections are present

* fix: ensure consistent path normalization for workspace collections during reordering
2026-02-20 14:00:44 +05:30
shubh-bruno
dfa1533b72 fix: updated error message for renaming requests (#7010)
Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-02-20 12:13:54 +05:30
lohit
cd33cb76fb fix: call initializeShellEnv directly in app ready handler (#7228)
Move the shell environment initialization from module-level into the
app ready callback, avoiding a dangling promise at import time.
2026-02-20 01:28:22 +05:30
Ngô Quốc Đạt
0376d38860 feat: add reveal in file manager option to workspace collections menu (#6944) 2026-02-19 21:25:39 +05:30
lohit
6ea079f6b1 fix: load shell environment variables on app startup (#7223)
Add shell-env integration to fetch environment variables from the user's
shell config files (.zshrc, .zshenv, etc.) so that proxy settings and
other exports are available in process.env for both Electron and CLI.
2026-02-19 21:17:27 +05:30
lohit
2fcfdfc338 fix: oauth2 credential management improvements (#7220)
* fix: oauth2 credential management improvements

Add bru.resetOauth2Credential() API for programmatic credential invalidation
from scripts, fix credential clearing to match on credentialsId, expose
oauth2 credential variables in test runtime, and add input validation
with deduplication to prevent redundant IPC messages. Remove unused
collectionGetOauth2CredentialsByUrlAndCredentialsId reducer.

* fix: handle invalid URLs in oauth2 callback redirect handler

Wrap new URL() calls in try-catch within onWindowRedirect to prevent
uncaught TypeError when redirect or callback URLs are invalid.

* Update packages/bruno-app/src/utils/codemirror/autocomplete.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-19 21:11:28 +05:30
Ram
09b8e8a32a fix(cli): preserve request item type during import and fail on unsupported types (#7207)
* fix(cli): preserve request type when importing collections

* fix(cli): fail fast on unsupported imported item types

* fix(cli): force BRU format when writing imported files

* chore: apply code rabbit fixes

* chore: adress review comment - keep changes minimal

* add additional test to test bru folder format

* agree with coderabbit, error handling is required

---------

Co-authored-by: Ramesh Sunkara <rs@rsunkara.com>
2026-02-19 20:43:44 +05:30
Mickael V
d060544da6 Prevent cursor state loss on empty nextValue (#7180) 2026-02-19 17:23:05 +05:30
Bastien Dumont
d35394c714 fix: consistent button size on save requests modal (#7197) 2026-02-19 16:55:20 +05:30
sanish chirayath
3c585a30b7 Fix: incorrect translations (#7214)
* feat: update req.setHeader translation to use pm.request.headers.add with object argument

empty commit

* refactor: update req.setHeader translation to use pm.request.headers.upsert
2026-02-19 15:53:42 +05:30
Pooja
ab2a16ac05 fix: env var edit (#7066) 2026-02-19 14:59:53 +05:30
shubh-bruno
cb716e5978 fix: import ndjson-curl handling binary data check with type file or not (#7210)
* fix: import ndjson-curl handling binary data check with type file

* chore: handle ansi escaping manually

* chore: avoid duplicate replacements

---------

Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-02-19 14:56:56 +05:30
Pooja
c093354938 fix: collection scope undefined var (#7211) 2026-02-19 13:40:05 +05:30
naman-bruno
b0a88bf00c fix: normalize Windows paths for cross-platform compatibility (#7185)
* fix: normalize Windows paths for cross-platform compatibility in workspace

* fixes
2026-02-18 17:32:58 +05:30
Chirag Chandrashekhar
8b80166170 fix: change transient request directory from os temp to app data (#7184)
* fix: change transient request directory from os temp to app data

Move transient request files from os.tmpdir() to app.getPath('userData')/transient
to prevent data loss when the OS clears temporary files.

Changes:
- Add helper functions for transient directory paths
- Update findCollectionPathByItemPath to use new transient path check
- Update renderer:mount-collection to create temp dirs in app data
- Update renderer:mount-workspace-scratch to create temp dirs in app data
- Update renderer:delete-transient-requests prefix validation

* change transient directory path to userData/tmp/transient

---------

Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
Co-authored-by: naman-bruno <naman@usebruno.com>
2026-02-18 17:30:27 +05:30
lohit
479fc160d7 fix: isJson assertion fails after res.setBody() with object in node-vm (#7191)
* fix: isJson assertion fails after res.setBody() with object in node-vm

Objects created inside Node's vm.createContext() have a different Object
constructor than the host realm. When res.setBody() is called with a JS
object from a script, _.cloneDeep preserves the cross-realm prototype,
causing obj.constructor === Object to fail in the isJson assertion.

Replace with Object.prototype.toString.call() which is cross-realm safe.

* fix: register isJson chai assertion in QuickJS test runtime

The bundled chai in QuickJS only exposes { expect, assert } via
requireObject — no Assertion class. Access the prototype through
Object.getPrototypeOf(expect(null)) and use Object.defineProperty
to register the json property directly.

* fix: enable assertion chaining on isJson in QuickJS runtime

The QuickJS isJson property getter was missing `return this`, preventing
chai assertion chaining (e.g. expect(body).to.be.json.and...).
2026-02-18 17:24:08 +05:30
tobiasgjerstrup
2337d77092 feature/f2-rename-shortcut (#7077)
* feature/f2-rename-shortcut

* feat(hotkeys): added OS-specific key bindings for renaming items

* fix(hotkeys): updated hotkey function call to somethhing more performant and standard
2026-02-17 19:17:43 +05:30
Joren-vanGoethem
2e58621759 update redux state to keep currently active script tab in state (#6947) 2026-02-17 19:02:20 +05:30
Bijin A B
78c629e7a6 chore(playwright): enhance playwright config to reduce flakiness (#7174) 2026-02-17 18:57:39 +05:30
Jeroen Vinke
03f7e60c66 fix cookies not being set when follow redirect = false (#6679)
* fix follow redirect cookies not being set

* Set default value of followredirects in axios-instance

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Jeroen Vinke <jeroen.vinke@iddinkgroup.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-17 18:41:14 +05:30
Evgeniy
540bb706e5 fix: parsing dates from insomnia (#7003)
js-yaml uses DEFAULT_SCHEMA by default and implicitly casts date-like strings to Date (timestamp). This caused unexpected config values where dates were supposed to remain plain strings.

Switched YAML parsing to JSON_SCHEMA to disable timestamp resolution and keep date-like values as strings.
2026-02-17 18:38:40 +05:30
Pooja
d8367e28ad fix: sse response download button (#7081)
Co-authored-by: Sid <siddharth@usebruno.com>
2026-02-17 17:34:36 +05:30
naman-bruno
5021226360 fix: update protobuf and import path handling in opencollection (#7166) 2026-02-17 15:15:32 +05:30
Bijin A B
dfc3a1b78c chore: fix flaky playwright tests (#7159) 2026-02-17 01:25:41 +05:30
Simon AUBERT
634b62642f feat: add some doc about variable usage (#6443)
* add some doc about variable usage

* add some doc about variable usage

* add some doc about variable usage
2026-02-16 18:24:18 +05:30
Steven
8724201148 Feature: Object Variable Interpolation (#6317)
* Added JSON stringify for object values to allow for proper display
Added styling changes to accommodate objects

* Switching to a multi line approach
2026-02-16 18:19:40 +05:30
Chae Jeong Ah
f7cedcbd92 fix: incorrect response formatting when saving examples (#6528) 2026-02-16 18:15:19 +05:30
Simon Pauget
22ff82f57a fix: importing from openapi excludes documentation for requests (#6439)
* fix: importing from openapi excludes documentation for requests

fixes issue 5094 for requests

* fix: revert accidental changes

---------

Co-authored-by: Simon PAUGET <simon.pauget@depinfonancy.net>
Co-authored-by: Simon PAUGET <simon.pauget@decathlon.com>
2026-02-16 18:11:17 +05:30
SahilShameerDev
f766ec2239 fix: improve visual hierarchy of markdown headers in docs (#7145) 2026-02-16 18:06:40 +05:30
shubh-bruno
9e939a2188 feat: remove headers from request using scripts (#7122) 2026-02-16 12:07:35 +05:30
Bijin A B
471333fb80 chore: fix flaky tests (#7144) 2026-02-14 21:03:58 +05:30
Bijin A B
1d126dcb65 fix: flaky tests - standardize save keyboard shortcut across tests (#7141) 2026-02-14 03:58:02 +05:30
Bijin A B
0c3b828b09 fix: update header validation test to use triple-click for selecting all text (#7140) 2026-02-14 01:40:51 +05:30
Abhishek S Lal
e000e377d1 feat: move import collection from git url and spec url from enterprise edition to opensource (#7127)
* feat: move import collection from git url and spec url from enterprise edition to opensource

* fix: corrected a typo

* test: add unit and e2e tests for import collection migration

* fix: guard against missing userAgentData platform in getOSName — Default platform to '' to prevent TypeError when navigator.userAgentData is unavailable (GitNotFoundModal/index.js)

fix: UID mismatch between status tracking and UI rendering in bulk import — Preserve synthetic file-${index} UID on converted collections so initialStatus, rename tracking, and the render loop all use the same key (BulkImportCollectionLocation/index.js)

fix: isConfirmDisabled returning non-boolean value — Changed .length checks to explicit comparisons (> 0, === 0) so the function always returns true/false (CloneGitRespository/index.js)

fix: missing ipcRenderer declaration in cloneGitRepository and scanForBrunoFiles — Added const { ipcRenderer } = window; to both actions to prevent ReferenceError at runtime (collections/actions.js)

fix: use strict equality in filterItemsInCollection — Changed == to === for item.name and item.type comparisons (importers/common.js)

fix: variable shadowing in transformItemsInCollection and hydrateSeqInCollection — Renamed forEach callback parameter from collection to col to avoid shadowing the outer parameter (importers/common.js)

fix: scanForBrunoFiles traversing node_modules and .git directories — Added exclusion for node_modules and .git to match getCollectionStats pattern, preventing app freezes on large repos (filesystem.js)

fix: diff hunk header using string character count instead of line count — Preserved prefixedLines array to compute lineCount before joining, so the @@ header has the correct line count (git.js)

fix: test locators not scoped to modal in bulk import e2e test — Changed page.getByTestId to bulkImportModal.getByTestId for grouping dropdown interactions (002-all-collection-types.spec.ts)

fix: missing afterEach cleanup in GitHub repository import test — Added closeAllCollections hook to match sibling test specs, replaced unused dotenv/config import (github-repository-import.spec.ts)

* fix: batch name tracking and git utility fixes

- Fix usedNamesInBatch tracking original name instead of final name, which
  could produce duplicate environment names within the same batch
  (BulkImportCollectionLocation/index.js)

- Remove unused lodash import (git.js)

- Add missing early return in fetchRemotes when gitRootPath is falsy,
  preventing getSimpleGitInstanceForPath from running with undefined (git.js)

* fix: correct variable naming and state management in CloneGitRepository component

- Renamed `collectionpaths` to `collectionPaths` for consistency and clarity.
- Updated references throughout the component to use the corrected variable name.
- Removed error toast notification to streamline error handling during repository cloning.
2026-02-13 19:35:23 +05:30
Sid
4e1123bd2d tests: fix breaking tests (#7132)
* fix: update placeholder text for environment variable input

* fix: handle undefined color in environment objects

Don't export if `undefined`

* fix: update collection import logic for YML and BRU formats

* fix: ensure error icon is not visible after header validation

* fix: specify format for collection and environment serialization
2026-02-13 19:08:02 +05:30
Pooja
ac33c909ef fix: env draft loss on color change and rename (#7130) 2026-02-13 16:14:20 +05:30
Chirag Chandrashekhar
53e158c6d1 Feature/scratch requests (#7087)
* feat: implement workspace-level scratch requests

Add support for temporary "scratch" requests at the workspace level that
are not tied to any collection. These requests are stored in a temp
directory and displayed as tabs in the workspace home.

Key changes:
- Add IPC handlers for mounting scratch directory and creating requests
- Add scratch directory watcher in collection-watcher.js
- Extend workspaces Redux slice with scratch state and reducers
- Add IPC listeners for scratch request events
- Create ScratchRequestPane and CreateScratchRequest components
- Update WorkspaceTabs with "+" button for creating scratch requests
- Update WorkspaceHome to render scratch request tabs
- Filter scratch collections from sidebar display

Supports all request types: HTTP, GraphQL, gRPC, and WebSocket.

* style: improve create scratch request button styling

- Use Button component with ghost variant and primary color
- Position button inside scrollable tab area
- Vertically center button with tabs
- Clean up unnecessary CSS properties

* fix: append scratch request dropdown to body to fix z-index issue

* refactor: improve scratch collection detection with path registration

- Add centralized scratch path tracking in backend (scratchCollectionPaths Set)
- Register scratch paths when created via renderer:mount-workspace-scratch
- Set brunoConfig.type='scratch' based on registered paths instead of string pattern
- Store scratchTempDirectory path in workspace state for frontend validation
- Update schema to accept 'scratch' as valid collection type
- Simplify frontend filtering to use brunoConfig.type or path comparison
- Remove fragile 'bruno-scratch-' string pattern matching
- Prevent scratch collections from being added to workspace.collections

* refactor: use CreateTransientRequest for scratch requests in workspace tabs

- Remove CreateScratchRequest component in favor of reusing CreateTransientRequest
- Register scratch collection temp directory via addTransientDirectory for transient request creation
- Add scratch collection item sync with workspace tabs
- Display HTTP method with color on scratch request tabs

* refactor: unify WorkspaceTabs with RequestTabs system

Remove separate WorkspaceTabs system and integrate workspace views (Overview, Environments) into the unified RequestTabs architecture using scratch collections.

Key changes:
- Remove WorkspaceTabs component and Redux slice
- Add workspaceOverview and workspaceEnvironments as special tab types
- Create WorkspaceHeader component to display workspace name in toolbar
- Make workspace tabs non-closable and always present
- Update tab creation on workspace switch to automatically add Overview and Environments tabs
- Simplify WorkspaceHome component by removing redundant header
- Update all references from WorkspaceTabs to unified tab system

Benefits:
- Single tab system for all content (collections and workspace views)
- Consistent UX with unified navigation pattern
- Reduced code complexity (~1000+ lines removed)
- Easier maintenance and feature development

* fix: enable automatic tab creation for scratch collection transient requests

- Add updateCollectionMountStatus to properly set scratch collection mount status to 'mounted'
- Create new renderer:add-collection-watcher IPC handler for explicit watcher setup
- Move workspace tab type checks before collection validation in RequestTabPanel
- Update mountScratchCollection to use explicit watcher setup instead of open-multiple-collections

This ensures the task middleware recognizes scratch collections as fully mounted,
allowing transient requests to automatically open in tabs when created.

* feat: add collection selector with breadcrumb navigation for scratch requests

Add multi-step save flow for scratch collection requests with collection selection before folder selection. Includes breadcrumb navigation showing "Collections > [Selected Collection] > [Folders...]" that allows users to navigate back to collection selector.

Refactor scratch collection detection to use workspace.scratchCollectionUid instead of persisting type to brunoConfig, providing cleaner separation of concerns without disk persistence.

Add backend support for automatic format conversion when saving from YAML scratch collections to BRU collections.

* chore: remove redundant comments and simplify code

* fix: use focusTab for home button, remove unused ScratchRequestPane

* fix: improve SaveTransientRequest collection mounting and selection flow

* refactor: use WorkspaceOverview directly, remove WorkspaceHome wrapper

* feat: add workspace management dropdown with rename, export, and close options

* refactor: extract CollectionListItem component with Redux selector for mount status

* refactor: separate scratch collection handling from openCollectionEvent

- Create dedicated openScratchCollectionEvent function for scratch collections
- Revert openCollectionEvent to clean state without scratch-specific logic
- Simplify closeTabs and closeAllCollectionTabs reducers in tabs slice
- Remove unused isScratchCollectionPath helper function

* test: add scratch requests test suite

- Add tests for creating scratch requests (HTTP, GraphQL, gRPC, WebSocket)
- Add tests for sending scratch requests and verifying response
- Add tests for saving scratch requests to a collection
- Add tests for multiple tabs and closing tabs
- Handle "Don't Save" modal in playwright fixture during app close

* refactor: address code review feedback for scratch requests feature

- Fix RequestTabPanel hooks violation by moving SSR guard after hooks
- Fix validateWorkspaceName to trim before length check
- Use stable deterministic UID in SaveTransientRequest
- Use ES6 shorthand for collectionUid in useIpcEvents
- Add JSDoc and error handling to openScratchCollectionEvent
- Fix closeAllCollectionTabs to preserve activeTabUid when not removed
- Add syncExampleUidsCache call to save-scratch-request handler
- Use getCollectionFormat for save-transient-request extension handling
- Fix Playwright modal handling with proper waitFor pattern
- Make keyboard shortcut platform-aware in scratch tests
- Remove flaky close tab test, handled by fixture cleanup
- Extract isScratchCollection utility to reduce duplication
- Memoize scratch collection derivation in RequestTabs
- Use theme color instead of Tailwind class for success icon
- Wrap resetForm and handleCancelWorkspaceRename in useCallback
- Extract FolderBreadcrumbs into separate component
- Call reset() inside useEffect in useCollectionFolderTree hook
- Defer workspace scratch state updates until mount succeeds

* feat: add unified collection header with context switcher dropdown

- Create CollectionHeader component that replaces WorkspaceHeader and CollectionToolBar
- Add dropdown to switch between workspace and mounted collections
- Display tab count badge next to collection/workspace name in header and dropdown
- Remove unused WorkspaceHeader and CollectionToolBar components
- Handle null/undefined values elegantly throughout

* chore: allow pr to comment

* refactor: improve scratch requests test cleanup with closeAllTabs helper

- Revert playwright/index.ts modal handling hack
- Add closeAllTabs helper to test utils for proper tab cleanup
- Update scratch-requests test to use closeAllTabs in afterAll
- Fix test assertion for new collection header dropdown structure

---------
Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
2026-02-13 15:34:47 +05:30
shubh-bruno
3e581675cd fix: cURL import NDJSON in request body as text (#7002)
* fix: cURL import NDJSON request body as text

* fix: cURL import NDJSON request body as text

* fix: cURL import NDJSON request body as text

* fix: resolved comments for body.text

* fix: add NDJSON content type detection

---------

Co-authored-by: shubh-bruno <shubh-bruno@shubh-bruno.local>
2026-02-12 21:10:04 +05:30
sanish chirayath
e03cf9a519 Feat/support missing env apis (#7069)
* feat: add support for new variable management functions in Bruno

- Implemented methods to retrieve and delete all environment and global variables.
- Added corresponding translations for new functions in Postman and Bruno converters.
- Updated request handling to include header deletion functionality.
- Enhanced test cases to cover new variable management features.

* feat: add new scripts for environment and global variable management

- Introduced scripts to delete all environment and global variables.
- Added functionality to retrieve all environment and global variables.
- Implemented tests to validate the behavior of new variable management features.

* feat: implement collection variable management in Bruno

- Added methods for managing collection variables: set, get, has, delete, and retrieve all.
- Updated Postman translation functions to reflect new collection variable methods.
- Enhanced tests to validate the functionality of collection variable management.
- Refactored existing code to replace environment variable references with collection variable equivalents.

* feat: enhance collection variable translations in Bruno

- Updated translation functions for collection variable management to align with Postman API.
- Added tests for new collection variable methods: set, has, delete, retrieve all, and clear.
- Refactored existing tests to ensure accurate translation of collection variable operations.

* feat: expand API hints for variable management in Bruno

* fix: test cases

* fix: remove unnecessary return in deleteEnvVar function
2026-02-12 18:38:25 +05:30
Pragadesh-45
91467f699c feat: enhance axios shim error handling and add comprehensive tests (#6349) 2026-02-12 17:37:48 +05:30
sanish chirayath
3871ca9edd feat: enhance translation capabilities for Bruno to Postman conversion (#7052)
* feat: enhance translation capabilities for Bruno to Postman conversion

- Added support for translating req.getHost(), req.getPath(), and req.getQueryString() to their Postman equivalents.
- Implemented translation for req.getPathParams() to pm.request.url.variables.
- Introduced handling for bru.visualize() to pm.visualizer.set() with various argument types.
- Added tests to validate new translation features and ensure correct behavior for URL-related methods and visualizer functionality.

* rm: duplicates

* refactor: remove bru.visualize transformation and associated tests

* feat: enhance BDD-style assertion translations in Postman converter

- Updated transformation logic to translate BDD-style assertions like pm.response.to.be.ok, pm.response.to.be.success, and others to their corresponding expect statements.
- Added comprehensive tests to validate the new translations for various response status checks.
- Improved handling of BDD assertions within test blocks to ensure accurate translation.

* fix: correct variable naming in transformation logic for Postman converter

- Updated variable names in the transformation logic to improve clarity and consistency.
- Ensured that the correct nodes are replaced and added to the transformedNodes set during processing.

* fix: improve AST mutation handling in Postman to Bruno translation

- Enhanced the processTransformations function to capture stable references before mutating the AST, ensuring correct node replacement and insertion.
- Added a defensive guard for ExpressionStatements to prevent errors when accessing undefined properties.
- Improved the logic for inserting remaining nodes after the grandparent in reverse order to maintain the correct sequence.

* fix: remove unnecessary defensive guard in AST mutation for Postman to Bruno translation
2026-02-12 17:17:39 +05:30
sanish chirayath
2517fe078f refactor: enhance gRPC methods loading with cache indication (#7022)
* refactor: enhance gRPC methods loading with cache indication

- Updated `loadMethodsFromReflection` and `loadMethodsFromProtoFile` to return a `fromCache` flag indicating whether methods were loaded from cache.
- Adjusted success toast messages to only display when methods are not loaded from cache, improving user feedback on data retrieval.

* empty commit

* empty
2026-02-12 17:13:54 +05:30
Chirag Chandrashekhar
7f047a4412 fix: multipart form-data file param export/import for Postman (#7111) 2026-02-12 16:41:25 +05:30
sanish chirayath
d30ab4d984 feat: add translations for direct cookie access methods (#7070)
* feat: add translations for direct cookie access methods

- Implement translations for pm.cookies.has, pm.cookies.get, and pm.cookies.toObject to their corresponding bru.cookies methods.
- Enhance the postman-to-bruno translator to handle these new cookie access patterns.
- Add unit tests to verify the correct conversion of cookie access methods in various scenarios.

* refactor: simplify optional member expression handling in postman-to-bruno translator

- Streamlined the code for handling optional member expressions in the translation of cookie access methods.
- Updated unit test to verify the correct output format for pm.cookies.toObject() conversion.

* refactor: enhance handling of await expressions in cookie translations

- Updated the postman-to-bruno translator to wrap await expressions in parentheses for improved clarity and consistency.
- Adjusted unit tests to reflect the new output format for cookie access methods, ensuring accurate translation of pm.cookies.get calls.

* refactor: update cookie access translations to use hasCookie method

- Modified translations for pm.cookies.has to utilize the new bru.cookies.hasCookie method for improved clarity and functionality.
- Updated related unit tests to reflect changes in expected output for cookie existence checks.
- Added new tests to validate the behavior of the hasCookie method in various scenarios.
2026-02-12 14:30:35 +05:30
Sanjai Kumar
836c2b9ace fix: graphQL variables interpolation consistency (UI and CLI) (#7049)
* feat: enhance GraphQL request handling with variable interpolation
2026-02-12 13:48:35 +05:30
Sanjai Kumar
e1827080dd chore: update swagger-ui-react (#7086) 2026-02-12 12:09:12 +05:30
lohit
ff87eb23ee fix(node-vm): scripting context and module resolution (#7033)
* fix(node-vm): scripting context and module resolution issues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(node-vm): use vm.createContext for true isolation and fix prototype mismatches

- Replace vm.compileFunction with vm.createContext + runInContext for true isolation
- Remove ECMAScript built-ins from safeGlobals (VM provides its own versions)
- This fixes prototype chain mismatches that broke libraries like @faker-js/faker
- Add sanitized process object (allows env, blocks exit/kill)
- Add global/globalThis pointing to isolated context (not host)
- Extract safe globals to constants.js for maintainability
- Remove typed-arrays mixin (VM provides TypedArrays)
- Add comprehensive isolation tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(node-vm): remove process, add Error types and TypedArrays mixin, add jose test

- Remove process object from script context (security hardening)
- Remove createSanitizedProcess function from constants.js
- Add Error types to safeGlobals for instanceof checks with host errors
- Add TypedArrays mixin for host API compatibility (TextEncoder, crypto, Buffer)
- Add jose library and test for JWT sign/verify functionality
- Update tests to reflect process removal

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(node-vm): handle circular dependencies and failed module caching

- Pre-populate module cache before execution to support circular requires
- Cache moduleObj instead of moduleObj.exports to handle module.exports reassignment
- Remove failed modules from cache to allow retry
- Add test for circular dependency handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(node-vm): spread all context properties in buildScriptContext

Instead of explicitly listing each context property, spread all
properties from the context input to support future additions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(node-vm): add filtered process object to script context

Expose a sanitized process object with only safe read-only properties
(argv, version, arch, platform, pid, features) while keeping env empty
for security.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(node-vm): add comprehensive tests for Node.js builtins

Add 18 test files for Node.js builtin APIs in developer sandbox mode:
- Buffer, URL, TextEncoder/TextDecoder, btoa/atob
- Web Crypto API and node:crypto module
- Timers (setTimeout, setInterval, setImmediate, queueMicrotask)
- Fetch API (Request, Response, Headers, FormData, Blob)
- Intl formatters, JSON, Events (Event, EventTarget, CustomEvent)
- Node modules: fs, path, os, util, stream, zlib, querystring

All tests skip in safe mode using bru.runner.skipRequest().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(node-vm): address CodeRabbit review feedback

- Block absolute paths from bypassing security by routing through loadLocalModule
- Fix process tests to expect sanitized object instead of undefined
- Fix cache test to verify module executes only once
- Add tests for absolute path handling (block outside, allow within roots)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: lint issues

* fix(node-vm): recontextualize host objects for cross-context deep equality

Objects passed from the host context into the Node VM have different
Object/Array constructors than objects created inside the VM. This breaks
deep equality checks in libraries like AJV, where fast-deep-equal fails
on `a.constructor !== b.constructor` for structurally identical objects.

Add recontextualizeScript to utils.js that wraps getter methods (res.getBody,
res.getHeaders, req.getBody, req.getHeaders, req.getPathParams, req.getTags,
bru.getVar) to JSON round-trip returned objects inside the VM, giving them
VM-native prototypes.

Add external-lib-with-bru-req-res-objects package and tests to verify
bru/req/res accessibility from npm modules. Update ajv.bru tests to
validate res.getBody() against AJV schemas with enum on nested objects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(node-vm): update spec to use saved mock refs after recontextualize

The recontextualizeScript wraps res.getBody with a JSON round-trip
function, replacing the jest mock on the context object. Save mock
references before calling runScriptInNodeVm so assertions work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(node-vm): shallow-copy mutable process properties in sandbox

process.argv, process.versions, and process.features were passed by
reference, allowing sandboxed scripts to mutate the host process.
Shallow-copy these properties to prevent leaking mutable references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(node-vm): use recursive clone in toVMNative instead of JSON round-trip

JSON.stringify converts undefined to null in arrays, breaking tests like
res.setBody([..., undefined, ...]). Replace with recursive clone that
creates new VM-native objects/arrays while preserving undefined values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(node-vm): generalize recontextualize to wrap all bru/req/res methods

Instead of hardcoding specific method names, walk the prototype chain
with Object.getOwnPropertyNames to discover and wrap all methods that
return Objects/Arrays. Async methods (sendRequest, runRequest) get their
resolved values wrapped. The res callable and res.body/res.headers are
also recontextualized for direct access and query usage.

Adds integration tests for VM-native prototype checks across res, req,
bru APIs, res() callable queries, and bru.sendRequest patterns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* revert(node-vm): remove recontextualizeScript and related tests

The recontextualize approach of wrapping all bru/req/res methods
to return VM-native objects is being reverted in favor of a
different solution to the cross-context prototype mismatch issue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(node-vm): expose full process object in developer sandbox via safeGlobals

* test(node-vm): update process tests for full process object in developer sandbox

* test(node-vm): update spec to verify process.nextTick availability

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-12 01:03:35 +05:30
Abhishek S Lal
7460078fd6 fix: enhance tag handling and validation in collection import/export (#7107)
- Added collection format handling in Tags component.
- Updated convertCollection function to accept collectionFormat parameter.
- Improved tag validation logic in TagList component based on collection format.
- Adjusted OpenAPI transformation functions to support collection format options.
- Enhanced schema validation for tags to allow spaces and underscores.
2026-02-12 00:42:16 +05:30
Bijin A B
e4b6f7a28b fix(save-all): fix save all modified requests while closing the app (#7118) 2026-02-12 00:07:23 +05:30
980 changed files with 83811 additions and 13071 deletions

View File

@@ -23,6 +23,19 @@ reviews:
drafts: false
base_branches: ['main', 'release/*']
path_instructions:
- path: '**/*'
instructions: |
Bruno is a cross-platform Electron desktop app that runs on macOS, Windows, and Linux. Ensure that all code is OS-agnostic:
- File paths must use `path.join()` or `path.resolve()` instead of hardcoded `/` or `\\` separators
- Never assume case-sensitive or case-insensitive filesystems
- Use `os.homedir()`, `app.getPath()`, or environment-appropriate APIs instead of hardcoded paths like `/home/`, `C:\\Users\\`, or `~/`
- Line endings should be handled consistently (be aware of CRLF vs LF issues)
- Use `path.sep` or `path.posix`/`path.win32` when platform-specific separators are needed
- Shell commands or child_process calls must account for platform differences (e.g., `which` vs `where`, `/bin/sh` vs `cmd.exe`)
- File permissions (e.g., `fs.chmod`, `fs.access`) should account for Windows not supporting Unix-style permission bits
- Avoid relying on Unix-only signals (e.g., `SIGKILL`) without Windows fallbacks
- Use `os.tmpdir()` instead of hardcoding `/tmp`
- Environment variable access should handle platform differences (e.g., `HOME` vs `USERPROFILE`)
- path: 'tests/**/**.*'
instructions: |
Review the following e2e test code written using the Playwright test library. Ensure that:

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno
* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno @sid-bruno

View File

@@ -0,0 +1,19 @@
name: 'Run Auth E2E Tests - Linux'
description: 'Run Auth E2E tests on Linux'
runs:
using: 'composite'
steps:
- name: Run Auth E2E tests
shell: bash
run: |
set -euo pipefail
xvfb-run npm run test:e2e:auth
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-auth-linux
path: playwright-report/
retention-days: 30

View File

@@ -0,0 +1,30 @@
name: 'Run OAuth1 CLI Tests - Linux'
description: 'Run OAuth1 CLI tests on Linux'
runs:
using: 'composite'
steps:
- name: Run BRU format CLI tests
shell: bash
run: |
set -euo pipefail
BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js"
# navigate to BRU test collection directory
cd tests/auth/oauth1/fixtures/collections/bru
echo "=== BRU Format Collection Run ==="
node $BRU_CLI run --env Local --output junit-bru.xml --format junit
- name: Run YML format CLI tests
shell: bash
run: |
set -euo pipefail
BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js"
# navigate to YML test collection directory
cd tests/auth/oauth1/fixtures/collections/yml
echo "=== YML Format Collection Run ==="
node $BRU_CLI run --env Local --output junit-yml.xml --format junit

View File

@@ -0,0 +1,15 @@
name: 'Setup Auth Feature Dependencies - Linux'
description: 'Setup feature-specific dependencies for auth tests on Linux'
runs:
using: 'composite'
steps:
- name: Install additional OS dependencies for auth tests
shell: bash
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox

View File

@@ -0,0 +1,16 @@
name: 'Start Test Server - Linux'
description: 'Start the bruno-tests mock server for OAuth1 CLI tests on Linux'
runs:
using: 'composite'
steps:
- name: Start test server
shell: bash
run: |
set -euo pipefail
cd packages/bruno-tests
echo "starting test server in background"
node src/index.js &
echo "server started with PID: $!"

View File

@@ -0,0 +1,17 @@
name: 'Run Auth E2E Tests - macOS'
description: 'Run Auth E2E tests on macOS'
runs:
using: 'composite'
steps:
- name: Run Auth E2E tests
shell: bash
run: |
npm run test:e2e:auth
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-auth-macos
path: playwright-report/
retention-days: 30

View File

@@ -0,0 +1,30 @@
name: 'Run OAuth1 CLI Tests - macOS'
description: 'Run OAuth1 CLI tests on macOS'
runs:
using: 'composite'
steps:
- name: Run BRU format CLI tests
shell: bash
run: |
set -euo pipefail
BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js"
# navigate to BRU test collection directory
cd tests/auth/oauth1/fixtures/collections/bru
echo "=== BRU Format Collection Run ==="
node $BRU_CLI run --env Local --output junit-bru.xml --format junit
- name: Run YML format CLI tests
shell: bash
run: |
set -euo pipefail
BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js"
# navigate to YML test collection directory
cd tests/auth/oauth1/fixtures/collections/yml
echo "=== YML Format Collection Run ==="
node $BRU_CLI run --env Local --output junit-yml.xml --format junit

View File

@@ -0,0 +1,16 @@
name: 'Start Test Server - macOS'
description: 'Start the bruno-tests mock server for OAuth1 CLI tests on macOS'
runs:
using: 'composite'
steps:
- name: Start test server
shell: bash
run: |
set -euo pipefail
cd packages/bruno-tests
echo "starting test server in background"
node src/index.js &
echo "server started with PID: $!"

View File

@@ -0,0 +1,17 @@
name: 'Run Auth E2E Tests - Windows'
description: 'Run Auth E2E tests on Windows'
runs:
using: 'composite'
steps:
- name: Run Auth E2E tests
shell: pwsh
run: |
npm run test:e2e:auth
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-auth-windows
path: playwright-report/
retention-days: 30

View File

@@ -0,0 +1,34 @@
name: 'Run OAuth1 CLI Tests - Windows'
description: 'Run OAuth1 CLI tests on Windows'
runs:
using: 'composite'
steps:
- name: Run BRU format CLI tests
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$BRU_CLI = "..\..\..\..\..\..\packages\bruno-cli\bin\bru.js"
# navigate to BRU test collection directory
Set-Location tests\auth\oauth1\fixtures\collections\bru
Write-Host "=== BRU Format Collection Run ==="
$process = Start-Process -FilePath "node" -ArgumentList "$BRU_CLI run --env Local --output junit-bru.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
if ($process.ExitCode -ne 0) { exit 1 }
- name: Run YML format CLI tests
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$BRU_CLI = "..\..\..\..\..\..\packages\bruno-cli\bin\bru.js"
# navigate to YML test collection directory
Set-Location tests\auth\oauth1\fixtures\collections\yml
Write-Host "=== YML Format Collection Run ==="
$process = Start-Process -FilePath "node" -ArgumentList "$BRU_CLI run --env Local --output junit-yml.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
if ($process.ExitCode -ne 0) { exit 1 }

View File

@@ -0,0 +1,14 @@
name: 'Start Test Server - Windows'
description: 'Start the bruno-tests mock server for OAuth1 CLI tests on Windows'
runs:
using: 'composite'
steps:
- name: Start test server
shell: pwsh
run: |
Set-StrictMode -Version Latest
Set-Location packages\bruno-tests
Write-Host "starting test server in background"
Start-Process -FilePath "node" -ArgumentList "src\index.js" -PassThru -WindowStyle Hidden

View File

@@ -1,5 +1,10 @@
name: 'Setup Node Dependencies'
description: 'Install Node.js and npm dependencies'
inputs:
skip-build:
description: 'Skip building libraries'
required: false
default: 'false'
runs:
using: 'composite'
steps:
@@ -9,12 +14,13 @@ runs:
node-version: v22.17.0
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install node dependencies
shell: bash
run: npm ci --legacy-peer-deps
- name: Build libraries
if: inputs.skip-build != 'true'
shell: bash
run: |
npm run build:graphql-docs

View File

@@ -0,0 +1,20 @@
name: 'Run CLI Tests'
description: 'Setup dependencies, start local testbench and run CLI tests'
runs:
using: 'composite'
steps:
- name: Run Local Testbench
shell: bash
run: |
npm start --workspace=packages/bruno-tests &
sleep 5
- name: Install Test Collection Dependencies
shell: bash
run: npm ci --prefix packages/bruno-tests/collection
- name: Run CLI Tests
shell: bash
run: |
cd packages/bruno-tests/collection
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer

View File

@@ -0,0 +1,22 @@
name: 'Run E2E Tests'
description: 'Setup dependencies, configure environment, and run Playwright E2E tests'
inputs:
os:
description: 'Operating system (ubuntu, macos, windows)'
default: 'ubuntu'
runs:
using: 'composite'
steps:
- name: Install Test Collection Dependencies
shell: bash
run: npm ci --prefix packages/bruno-tests/collection
- name: Run Playwright Tests (Ubuntu)
if: inputs.os == 'ubuntu'
shell: bash
run: xvfb-run npm run test:e2e
- name: Run Playwright Tests
if: inputs.os != 'ubuntu'
shell: bash
run: npm run test:e2e

View File

@@ -0,0 +1,48 @@
name: 'Run Unit Tests'
description: 'Setup dependencies and run unit tests for all packages'
runs:
using: 'composite'
steps:
- name: Test Package bruno-js
shell: bash
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-cli
shell: bash
run: npm run test --workspace=packages/bruno-cli
- name: Test Package bruno-query
shell: bash
run: npm run test --workspace=packages/bruno-query
- name: Test Package bruno-lang
shell: bash
run: npm run test --workspace=packages/bruno-lang
- name: Test Package bruno-schema
shell: bash
run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app
shell: bash
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-common
shell: bash
run: npm run test --workspace=packages/bruno-common
- name: Test Package bruno-converters
shell: bash
run: npm run test --workspace=packages/bruno-converters
- name: Test Package bruno-electron
shell: bash
run: npm run test --workspace=packages/bruno-electron
- name: Test Package bruno-requests
shell: bash
run: npm run test --workspace=packages/bruno-requests
- name: Test Package bruno-filestore
shell: bash
run: npm run test --workspace=packages/bruno-filestore

79
.github/workflows/auth-tests.yml vendored Normal file
View File

@@ -0,0 +1,79 @@
name: Auth Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
oauth1-tests-for-linux:
name: OAuth 1.0 Auth Tests - Linux
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup Feature Dependencies
uses: ./.github/actions/auth/oauth1/linux/setup-feature-specific-deps
- name: Run Auth E2E Tests
uses: ./.github/actions/auth/oauth1/linux/run-auth-e2e-tests
- name: Start Test Server
uses: ./.github/actions/auth/oauth1/linux/start-test-server
- name: Run OAuth1 CLI Tests
uses: ./.github/actions/auth/oauth1/linux/run-oauth1-cli-tests
oauth1-tests-for-macos:
name: OAuth 1.0 Auth Tests - macOS
timeout-minutes: 60
runs-on: macos-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Run Auth E2E Tests
uses: ./.github/actions/auth/oauth1/macos/run-auth-e2e-tests
- name: Start Test Server
uses: ./.github/actions/auth/oauth1/macos/start-test-server
- name: Run OAuth1 CLI Tests
uses: ./.github/actions/auth/oauth1/macos/run-oauth1-cli-tests
oauth1-tests-for-windows:
name: OAuth 1.0 Auth Tests - Windows
timeout-minutes: 60
runs-on: windows-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Run Auth E2E Tests
uses: ./.github/actions/auth/oauth1/windows/run-auth-e2e-tests
- name: Start Test Server
uses: ./.github/actions/auth/oauth1/windows/start-test-server
- name: Run OAuth1 CLI Tests
uses: ./.github/actions/auth/oauth1/windows/run-oauth1-cli-tests

View File

@@ -9,6 +9,7 @@ on:
permissions:
contents: read
pull-requests: write
issues: write
checks: write
jobs:
@@ -72,7 +73,7 @@ jobs:
- name: Post PR comment
if: hashFiles('pr-comment.md') != ''
uses: actions/github-script@v7
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');

26
.github/workflows/lint-checks.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Lint Checks
on:
workflow_dispatch:
push:
branches: [main, 'release/v*']
pull_request:
branches: [main, 'release/v*']
jobs:
lint:
name: Lint Check
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
with:
skip-build: 'true'
- name: Lint Check
run: npm run lint
env:
ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}

View File

@@ -43,7 +43,7 @@ jobs:
bru run --env Prod --output junit.xml --format junit --sandbox developer
- name: Publish Test Report
uses: dorny/test-reporter@v2
uses: dorny/test-reporter@v3
if: success() || failure()
with:
name: Test Report

View File

@@ -1,9 +1,10 @@
name: Tests
on:
workflow_dispatch:
push:
branches: [main]
branches: [main, 'release/v*']
pull_request:
branches: [main]
branches: [main, 'release/v*']
jobs:
unit-test:
@@ -14,52 +15,12 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install dependencies
run: npm ci --legacy-peer-deps
# build libraries
- name: Build libraries
run: |
npm run build --workspace=packages/bruno-common
npm run build --workspace=packages/bruno-query
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
npm run build --workspace=packages/bruno-schema-types
npm run build --workspace=packages/bruno-filestore
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Lint Check
run: npm run lint
env:
ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}
# tests
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-cli
run: npm run test --workspace=packages/bruno-cli
- name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query
- name: Test Package bruno-lang
run: npm run test --workspace=packages/bruno-lang
- name: Test Package bruno-schema
run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-common
run: npm run test --workspace=packages/bruno-common
- name: Test Package bruno-converters
run: npm run test --workspace=packages/bruno-converters
- name: Test Package bruno-electron
run: npm run test --workspace=packages/bruno-electron
- name: Test Package bruno-requests
run: npm run test --workspace=packages/bruno-requests
- name: Run Unit Tests
uses: ./.github/actions/tests/run-unit-tests
cli-test:
name: CLI Tests
@@ -70,35 +31,12 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Build Libraries
run: |
npm run build --workspace=packages/bruno-query
npm run build --workspace=packages/bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
npm run build --workspace=packages/bruno-schema-types
npm run build --workspace=packages/bruno-filestore
- name: Run Local Testbench
run: |
npm start --workspace=packages/bruno-tests &
sleep 5
- name: Run tests
run: |
cd packages/bruno-tests/collection
npm install
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer
- name: Run CLI Tests
uses: ./.github/actions/tests/run-cli-tests
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action@v2
@@ -107,46 +45,38 @@ jobs:
check_name: CLI Test Results
files: packages/bruno-tests/collection/junit.xml
comment_mode: always
e2e-test:
name: Playwright E2E Tests
timeout-minutes: 60
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version: v22.11.x
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb
npm ci --legacy-peer-deps
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
- uses: actions/checkout@v6
- name: Install dependencies for test collection environment
run: |
npm ci --prefix packages/bruno-tests/collection
- name: Install System Dependencies (Ubuntu)
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb
- name: Build libraries
run: |
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:schema-types
npm run build:bruno-filestore
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Run Playwright tests
run: |
xvfb-run npm run test:e2e
- uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Configure Chrome Sandbox
run: |
sudo chown root node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 node_modules/electron/dist/chrome-sandbox
- name: Run playwright Tests
uses: ./.github/actions/tests/run-e2e-tests
with:
os: ubuntu
- name: Upload Playwright Report
uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

5
.gitignore vendored
View File

@@ -49,6 +49,11 @@ bruno.iml
.idea
.vscode
.cursor
.claude
.codex
.agents
.agent
skills-lock.json
# Playwright
/blob-report/

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
min-release-age=10

View File

@@ -75,6 +75,8 @@ Remember, these rules are here to make our codebase harmonious. If something doe
- Avoid: `import * as React from "react";` then `React.useCallback(...)`
- Add `data-testid` to testable elements for Playwright
- Co-locate utilities that are truly component-specific next to the component, otherwise place shared items under a common folder
- Avoid mixed controlled and uncontrolled state in React components. A component is either controlled or uncontrolled. State needs a single source of truth instead of being computed by props and then recomputed internally.
- SHOULD: Use derived state variables instead of adding unneeded `React.useState` / `useState` hooks.
## Readability and Abstractions

View File

@@ -324,7 +324,7 @@ test('should create and execute HTTP request', async ({ page, createTmpDir }) =>
await page.getByRole('button', { name: 'Create' }).click();
// Execute request
await page.locator('#send-request').getByRole('img').nth(2).click();
await page.getByTestId('send-arrow-icon').click();
// Verify response
await expect(page.getByRole('main')).toContainText('200 OK');

View File

@@ -178,7 +178,8 @@ module.exports = runESMImports().then(() => defineConfig([
}
},
rules: {
'no-undef': 'error'
'no-undef': 'error',
'no-case-declarations': 'error'
}
},
{

11808
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"@eslint/compat": "^1.3.2",
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@opencollection/types": "~0.7.0",
"@opencollection/types": "0.9.1",
"@playwright/test": "^1.51.1",
"@rollup/plugin-json": "^6.1.0",
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
@@ -37,7 +37,7 @@
"@typescript-eslint/parser": "^8.39.0",
"concurrently": "^8.2.2",
"cross-env": "10.1.0",
"eslint": "^9.26.0",
"eslint": "^9.39.4",
"eslint-plugin-diff": "^2.0.3",
"fs-extra": "^11.1.1",
"globals": "^16.1.0",
@@ -82,6 +82,7 @@
"test:codegen": "node playwright/codegen.ts",
"test:e2e": "playwright test --project=default",
"test:e2e:ssl": "playwright test --project=ssl",
"test:e2e:auth": "playwright test --project=auth",
"lint": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint",
"lint:fix": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint --fix",
"prepare": "husky"
@@ -92,7 +93,9 @@
]
},
"overrides": {
"rollup": "3.29.5",
"axios":"1.13.6",
"rollup": "3.30.0",
"pbkdf2":"3.1.5",
"electron-store": {
"conf": {
"json-schema-typed": "8.0.1"
@@ -100,6 +103,7 @@
}
},
"dependencies": {
"ajv": "^8.17.1"
"ajv": "^8.17.1",
"git-url-parse": "^14.1.0"
}
}
}

View File

@@ -1,4 +1,4 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],
"plugins": [["styled-components", { "ssr": true }]]
}

View File

@@ -27,6 +27,7 @@
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
"cookie": "0.7.1",
"diff2html": "^3.4.47",
"dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"fast-fuzzy": "^1.12.0",
@@ -38,7 +39,7 @@
"github-markdown-css": "^5.2.0",
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"graphql-request": "4.2.0",
"hexy": "^0.3.5",
"httpsnippet": "^3.0.9",
"i18next": "24.1.2",
@@ -88,7 +89,7 @@
"shell-quote": "^1.8.3",
"strip-json-comments": "^5.0.1",
"styled-components": "^5.3.3",
"swagger-ui-react": "5.17.12",
"swagger-ui-react": "^5.31.0",
"system": "^2.0.1",
"url": "^0.11.3",
"xml-formatter": "^3.5.0",
@@ -99,9 +100,10 @@
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.22.0",
"@rsbuild/core": "^1.1.2",
"@rsbuild/plugin-babel": "^1.0.3",
"@rsbuild/plugin-node-polyfill": "^1.2.0",
"@rsbuild/plugin-node-polyfill": "1.2.0",
"@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-sass": "^1.1.0",
"@rsbuild/plugin-styled-components": "1.1.0",
@@ -130,4 +132,4 @@
"form-data": "4.0.4"
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,713 @@
:host,
:root {
--d2h-bg-color: #fff;
--d2h-border-color: #ddd;
--d2h-dim-color: rgba(0, 0, 0, 0.3);
--d2h-line-border-color: #eee;
--d2h-file-header-bg-color: #f7f7f7;
--d2h-file-header-border-color: #d8d8d8;
--d2h-empty-placeholder-bg-color: #f1f1f1;
--d2h-empty-placeholder-border-color: #e1e1e1;
--d2h-selected-color: #c8e1ff;
--d2h-ins-bg-color: #dfd;
--d2h-ins-border-color: #b4e2b4;
--d2h-ins-highlight-bg-color: #97f295;
--d2h-ins-label-color: #399839;
--d2h-del-bg-color: #fee8e9;
--d2h-del-border-color: #e9aeae;
--d2h-del-highlight-bg-color: #ffb6ba;
--d2h-del-label-color: #c33;
--d2h-change-del-color: #fdf2d0;
--d2h-change-ins-color: #ded;
--d2h-info-bg-color: #f8fafd;
--d2h-info-border-color: #d5e4f2;
--d2h-change-label-color: #d0b44c;
--d2h-moved-label-color: #3572b0;
--d2h-dark-color: #e6edf3;
--d2h-dark-bg-color: #0d1117;
--d2h-dark-border-color: #30363d;
--d2h-dark-dim-color: #6e7681;
--d2h-dark-line-border-color: #21262d;
--d2h-dark-file-header-bg-color: #161b22;
--d2h-dark-file-header-border-color: #30363d;
--d2h-dark-empty-placeholder-bg-color: hsla(215, 8%, 47%, 0.1);
--d2h-dark-empty-placeholder-border-color: #30363d;
--d2h-dark-selected-color: rgba(56, 139, 253, 0.1);
--d2h-dark-ins-bg-color: rgba(46, 160, 67, 0.15);
--d2h-dark-ins-border-color: rgba(46, 160, 67, 0.4);
--d2h-dark-ins-highlight-bg-color: rgba(46, 160, 67, 0.4);
--d2h-dark-ins-label-color: #3fb950;
--d2h-dark-del-bg-color: rgba(248, 81, 73, 0.1);
--d2h-dark-del-border-color: rgba(248, 81, 73, 0.4);
--d2h-dark-del-highlight-bg-color: rgba(248, 81, 73, 0.4);
--d2h-dark-del-label-color: #f85149;
--d2h-dark-change-del-color: rgba(210, 153, 34, 0.2);
--d2h-dark-change-ins-color: rgba(46, 160, 67, 0.25);
--d2h-dark-info-bg-color: rgba(56, 139, 253, 0.1);
--d2h-dark-info-border-color: rgba(56, 139, 253, 0.4);
--d2h-dark-change-label-color: #d29922;
--d2h-dark-moved-label-color: #3572b0;
}
.d2h-wrapper {
text-align: left;
}
.d2h-file-header {
background-color: #f7f7f7;
background-color: var(--d2h-file-header-bg-color);
border-bottom: 1px solid #d8d8d8;
border-bottom: 1px solid var(--d2h-file-header-border-color);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-family: Source Sans Pro, Helvetica Neue, Helvetica, Arial, sans-serif;
height: 35px;
padding: 5px 10px;
}
.d2h-file-header.d2h-sticky-header {
position: sticky;
top: 0;
z-index: 1;
}
.d2h-file-stats {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 14px;
margin-left: auto;
}
.d2h-lines-added {
border: 1px solid #b4e2b4;
border: 1px solid var(--d2h-ins-border-color);
border-radius: 5px 0 0 5px;
color: #399839;
color: var(--d2h-ins-label-color);
padding: 2px;
text-align: right;
vertical-align: middle;
}
.d2h-lines-deleted {
border: 1px solid #e9aeae;
border: 1px solid var(--d2h-del-border-color);
border-radius: 0 5px 5px 0;
color: #c33;
color: var(--d2h-del-label-color);
margin-left: 1px;
padding: 2px;
text-align: left;
vertical-align: middle;
}
.d2h-file-name-wrapper {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 15px;
width: 100%;
}
.d2h-file-name {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.d2h-file-wrapper {
border: 1px solid #ddd;
border: 1px solid var(--d2h-border-color);
border-radius: 3px;
margin-bottom: 1em;
}
.d2h-file-collapse {
-webkit-box-pack: end;
-ms-flex-pack: end;
cursor: pointer;
display: none;
font-size: 12px;
justify-content: flex-end;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
border: 1px solid #ddd;
border: 1px solid var(--d2h-border-color);
border-radius: 3px;
padding: 4px 8px;
}
.d2h-file-collapse.d2h-selected {
background-color: #c8e1ff;
background-color: var(--d2h-selected-color);
}
.d2h-file-collapse-input {
margin: 0 4px 0 0;
}
.d2h-diff-table {
border-collapse: collapse;
font-family: Menlo, Consolas, monospace;
font-size: 13px;
width: 100%;
}
.d2h-files-diff {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
width: 100%;
}
.d2h-file-diff {
overflow-y: hidden;
}
.d2h-file-diff.d2h-d-none,
.d2h-files-diff.d2h-d-none {
display: none;
}
.d2h-file-side-diff {
display: inline-block;
overflow-x: scroll;
overflow-y: hidden;
width: 50%;
}
.d2h-code-line {
padding: 0 8em;
width: calc(100% - 16em);
}
.d2h-code-line,
.d2h-code-side-line {
display: inline-block;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
white-space: nowrap;
}
.d2h-code-side-line {
padding: 0 4.5em;
width: calc(100% - 9em);
}
.d2h-code-line-ctn {
background: none;
display: inline-block;
padding: 0;
word-wrap: normal;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
vertical-align: middle;
white-space: pre;
width: 100%;
}
.d2h-code-line del,
.d2h-code-side-line del {
background-color: #ffb6ba;
background-color: var(--d2h-del-highlight-bg-color);
}
.d2h-code-line del,
.d2h-code-line ins,
.d2h-code-side-line del,
.d2h-code-side-line ins {
border-radius: 0.2em;
display: inline-block;
margin-top: -1px;
-webkit-text-decoration: none;
text-decoration: none;
}
.d2h-code-line ins,
.d2h-code-side-line ins {
background-color: #97f295;
background-color: var(--d2h-ins-highlight-bg-color);
text-align: left;
}
.d2h-code-line-prefix {
background: none;
display: inline;
padding: 0;
word-wrap: normal;
white-space: pre;
}
.line-num1 {
float: left;
}
.line-num1,
.line-num2 {
-webkit-box-sizing: border-box;
box-sizing: border-box;
overflow: hidden;
padding: 0 0.5em;
text-overflow: ellipsis;
width: 3.5em;
}
.line-num2 {
float: right;
}
.d2h-code-linenumber {
background-color: #fff;
background-color: var(--d2h-bg-color);
border: solid #eee;
border: solid var(--d2h-line-border-color);
border-width: 0 1px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: rgba(0, 0, 0, 0.3);
color: var(--d2h-dim-color);
cursor: pointer;
display: inline-block;
position: absolute;
text-align: right;
width: 7.5em;
}
.d2h-code-linenumber:after {
content: '\200b';
}
.d2h-code-side-linenumber {
background-color: #fff;
background-color: var(--d2h-bg-color);
border: solid #eee;
border: solid var(--d2h-line-border-color);
border-width: 0 1px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: rgba(0, 0, 0, 0.3);
color: var(--d2h-dim-color);
cursor: pointer;
display: inline-block;
overflow: hidden;
padding: 0 0.5em;
position: absolute;
text-align: right;
text-overflow: ellipsis;
width: 4em;
}
.d2h-code-side-linenumber:after {
content: '\200b';
}
.d2h-code-side-emptyplaceholder,
.d2h-emptyplaceholder {
background-color: #f1f1f1;
background-color: var(--d2h-empty-placeholder-bg-color);
border-color: #e1e1e1;
border-color: var(--d2h-empty-placeholder-border-color);
}
.d2h-code-line-prefix,
.d2h-code-linenumber,
.d2h-code-side-linenumber,
.d2h-emptyplaceholder {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.d2h-code-linenumber,
.d2h-code-side-linenumber {
direction: rtl;
}
.d2h-del {
background-color: #fee8e9;
background-color: var(--d2h-del-bg-color);
border-color: #e9aeae;
border-color: var(--d2h-del-border-color);
}
.d2h-ins {
background-color: #dfd;
background-color: var(--d2h-ins-bg-color);
border-color: #b4e2b4;
border-color: var(--d2h-ins-border-color);
}
.d2h-info {
background-color: #f8fafd;
background-color: var(--d2h-info-bg-color);
border-color: #d5e4f2;
border-color: var(--d2h-info-border-color);
color: rgba(0, 0, 0, 0.3);
color: var(--d2h-dim-color);
}
.d2h-file-diff .d2h-del.d2h-change {
background-color: #fdf2d0;
background-color: var(--d2h-change-del-color);
}
.d2h-file-diff .d2h-ins.d2h-change {
background-color: #ded;
background-color: var(--d2h-change-ins-color);
}
.d2h-file-list-wrapper {
margin-bottom: 10px;
}
.d2h-file-list-wrapper a {
-webkit-text-decoration: none;
text-decoration: none;
}
.d2h-file-list-wrapper a,
.d2h-file-list-wrapper a:visited {
color: #3572b0;
color: var(--d2h-moved-label-color);
}
.d2h-file-list-header {
text-align: left;
}
.d2h-file-list-title {
font-weight: 700;
}
.d2h-file-list-line {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
text-align: left;
}
.d2h-file-list {
display: block;
list-style: none;
margin: 0;
padding: 0;
}
.d2h-file-list > li {
border-bottom: 1px solid #ddd;
border-bottom: 1px solid var(--d2h-border-color);
margin: 0;
padding: 5px 10px;
}
.d2h-file-list > li:last-child {
border-bottom: none;
}
.d2h-file-switch {
cursor: pointer;
display: none;
font-size: 10px;
}
.d2h-icon {
margin-right: 10px;
vertical-align: middle;
fill: currentColor;
}
.d2h-deleted {
color: #c33;
color: var(--d2h-del-label-color);
}
.d2h-added {
color: #399839;
color: var(--d2h-ins-label-color);
}
.d2h-changed {
color: #d0b44c;
color: var(--d2h-change-label-color);
}
.d2h-moved {
color: #3572b0;
color: var(--d2h-moved-label-color);
}
.d2h-tag {
background-color: #fff;
background-color: var(--d2h-bg-color);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 10px;
margin-left: 5px;
padding: 0 2px;
}
.d2h-deleted-tag {
border: 1px solid #c33;
border: 1px solid var(--d2h-del-label-color);
}
.d2h-added-tag {
border: 1px solid #399839;
border: 1px solid var(--d2h-ins-label-color);
}
.d2h-changed-tag {
border: 1px solid #d0b44c;
border: 1px solid var(--d2h-change-label-color);
}
.d2h-moved-tag {
border: 1px solid #3572b0;
border: 1px solid var(--d2h-moved-label-color);
}
.d2h-dark-color-scheme {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
color: #e6edf3;
color: var(--d2h-dark-color);
}
.d2h-dark-color-scheme .d2h-file-header {
background-color: #161b22;
background-color: var(--d2h-dark-file-header-bg-color);
border-bottom: #30363d;
border-bottom: var(--d2h-dark-file-header-border-color);
}
.d2h-dark-color-scheme .d2h-lines-added {
border: 1px solid rgba(46, 160, 67, 0.4);
border: 1px solid var(--d2h-dark-ins-border-color);
color: #3fb950;
color: var(--d2h-dark-ins-label-color);
}
.d2h-dark-color-scheme .d2h-lines-deleted {
border: 1px solid rgba(248, 81, 73, 0.4);
border: 1px solid var(--d2h-dark-del-border-color);
color: #f85149;
color: var(--d2h-dark-del-label-color);
}
.d2h-dark-color-scheme .d2h-code-line del,
.d2h-dark-color-scheme .d2h-code-side-line del {
background-color: rgba(248, 81, 73, 0.4);
background-color: var(--d2h-dark-del-highlight-bg-color);
}
.d2h-dark-color-scheme .d2h-code-line ins,
.d2h-dark-color-scheme .d2h-code-side-line ins {
background-color: rgba(46, 160, 67, 0.4);
background-color: var(--d2h-dark-ins-highlight-bg-color);
}
.d2h-dark-color-scheme .d2h-diff-tbody {
border-color: #30363d;
border-color: var(--d2h-dark-border-color);
}
.d2h-dark-color-scheme .d2h-code-side-linenumber {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
border-color: #21262d;
border-color: var(--d2h-dark-line-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-dark-color-scheme .d2h-files-diff .d2h-code-side-emptyplaceholder,
.d2h-dark-color-scheme .d2h-files-diff .d2h-emptyplaceholder {
background-color: hsla(215, 8%, 47%, 0.1);
background-color: var(--d2h-dark-empty-placeholder-bg-color);
border-color: #30363d;
border-color: var(--d2h-dark-empty-placeholder-border-color);
}
.d2h-dark-color-scheme .d2h-code-linenumber {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
border-color: #21262d;
border-color: var(--d2h-dark-line-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-dark-color-scheme .d2h-del {
background-color: rgba(248, 81, 73, 0.1);
background-color: var(--d2h-dark-del-bg-color);
border-color: rgba(248, 81, 73, 0.4);
border-color: var(--d2h-dark-del-border-color);
}
.d2h-dark-color-scheme .d2h-ins {
background-color: rgba(46, 160, 67, 0.15);
background-color: var(--d2h-dark-ins-bg-color);
border-color: rgba(46, 160, 67, 0.4);
border-color: var(--d2h-dark-ins-border-color);
}
.d2h-dark-color-scheme .d2h-info {
background-color: rgba(56, 139, 253, 0.1);
background-color: var(--d2h-dark-info-bg-color);
border-color: rgba(56, 139, 253, 0.4);
border-color: var(--d2h-dark-info-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-dark-color-scheme .d2h-file-diff .d2h-del.d2h-change {
background-color: rgba(210, 153, 34, 0.2);
background-color: var(--d2h-dark-change-del-color);
}
.d2h-dark-color-scheme .d2h-file-diff .d2h-ins.d2h-change {
background-color: rgba(46, 160, 67, 0.25);
background-color: var(--d2h-dark-change-ins-color);
}
.d2h-dark-color-scheme .d2h-file-wrapper {
border: 1px solid #30363d;
border: 1px solid var(--d2h-dark-border-color);
}
.d2h-dark-color-scheme .d2h-file-collapse {
border: 1px solid #0d1117;
border: 1px solid var(--d2h-dark-bg-color);
}
.d2h-dark-color-scheme .d2h-file-collapse.d2h-selected {
background-color: rgba(56, 139, 253, 0.1);
background-color: var(--d2h-dark-selected-color);
}
.d2h-dark-color-scheme .d2h-file-list-wrapper a,
.d2h-dark-color-scheme .d2h-file-list-wrapper a:visited {
color: #3572b0;
color: var(--d2h-dark-moved-label-color);
}
.d2h-dark-color-scheme .d2h-file-list > li {
border-bottom: 1px solid #0d1117;
border-bottom: 1px solid var(--d2h-dark-bg-color);
}
.d2h-dark-color-scheme .d2h-deleted {
color: #f85149;
color: var(--d2h-dark-del-label-color);
}
.d2h-dark-color-scheme .d2h-added {
color: #3fb950;
color: var(--d2h-dark-ins-label-color);
}
.d2h-dark-color-scheme .d2h-changed {
color: #d29922;
color: var(--d2h-dark-change-label-color);
}
.d2h-dark-color-scheme .d2h-moved {
color: #3572b0;
color: var(--d2h-dark-moved-label-color);
}
.d2h-dark-color-scheme .d2h-tag {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
}
.d2h-dark-color-scheme .d2h-deleted-tag {
border: 1px solid #f85149;
border: 1px solid var(--d2h-dark-del-label-color);
}
.d2h-dark-color-scheme .d2h-added-tag {
border: 1px solid #3fb950;
border: 1px solid var(--d2h-dark-ins-label-color);
}
.d2h-dark-color-scheme .d2h-changed-tag {
border: 1px solid #d29922;
border: 1px solid var(--d2h-dark-change-label-color);
}
.d2h-dark-color-scheme .d2h-moved-tag {
border: 1px solid #3572b0;
border: 1px solid var(--d2h-dark-moved-label-color);
}
@media (prefers-color-scheme: dark) {
.d2h-auto-color-scheme {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
color: #e6edf3;
color: var(--d2h-dark-color);
}
.d2h-auto-color-scheme .d2h-file-header {
background-color: #161b22;
background-color: var(--d2h-dark-file-header-bg-color);
border-bottom: #30363d;
border-bottom: var(--d2h-dark-file-header-border-color);
}
.d2h-auto-color-scheme .d2h-lines-added {
border: 1px solid rgba(46, 160, 67, 0.4);
border: 1px solid var(--d2h-dark-ins-border-color);
color: #3fb950;
color: var(--d2h-dark-ins-label-color);
}
.d2h-auto-color-scheme .d2h-lines-deleted {
border: 1px solid rgba(248, 81, 73, 0.4);
border: 1px solid var(--d2h-dark-del-border-color);
color: #f85149;
color: var(--d2h-dark-del-label-color);
}
.d2h-auto-color-scheme .d2h-code-line del,
.d2h-auto-color-scheme .d2h-code-side-line del {
background-color: rgba(248, 81, 73, 0.4);
background-color: var(--d2h-dark-del-highlight-bg-color);
}
.d2h-auto-color-scheme .d2h-code-line ins,
.d2h-auto-color-scheme .d2h-code-side-line ins {
background-color: rgba(46, 160, 67, 0.4);
background-color: var(--d2h-dark-ins-highlight-bg-color);
}
.d2h-auto-color-scheme .d2h-diff-tbody {
border-color: #30363d;
border-color: var(--d2h-dark-border-color);
}
.d2h-auto-color-scheme .d2h-code-side-linenumber {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
border-color: #21262d;
border-color: var(--d2h-dark-line-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-auto-color-scheme .d2h-files-diff .d2h-code-side-emptyplaceholder,
.d2h-auto-color-scheme .d2h-files-diff .d2h-emptyplaceholder {
background-color: hsla(215, 8%, 47%, 0.1);
background-color: var(--d2h-dark-empty-placeholder-bg-color);
border-color: #30363d;
border-color: var(--d2h-dark-empty-placeholder-border-color);
}
.d2h-auto-color-scheme .d2h-code-linenumber {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
border-color: #21262d;
border-color: var(--d2h-dark-line-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-auto-color-scheme .d2h-del {
background-color: rgba(248, 81, 73, 0.1);
background-color: var(--d2h-dark-del-bg-color);
border-color: rgba(248, 81, 73, 0.4);
border-color: var(--d2h-dark-del-border-color);
}
.d2h-auto-color-scheme .d2h-ins {
background-color: rgba(46, 160, 67, 0.15);
background-color: var(--d2h-dark-ins-bg-color);
border-color: rgba(46, 160, 67, 0.4);
border-color: var(--d2h-dark-ins-border-color);
}
.d2h-auto-color-scheme .d2h-info {
background-color: rgba(56, 139, 253, 0.1);
background-color: var(--d2h-dark-info-bg-color);
border-color: rgba(56, 139, 253, 0.4);
border-color: var(--d2h-dark-info-border-color);
color: #6e7681;
color: var(--d2h-dark-dim-color);
}
.d2h-auto-color-scheme .d2h-file-diff .d2h-del.d2h-change {
background-color: rgba(210, 153, 34, 0.2);
background-color: var(--d2h-dark-change-del-color);
}
.d2h-auto-color-scheme .d2h-file-diff .d2h-ins.d2h-change {
background-color: rgba(46, 160, 67, 0.25);
background-color: var(--d2h-dark-change-ins-color);
}
.d2h-auto-color-scheme .d2h-file-wrapper {
border: 1px solid #30363d;
border: 1px solid var(--d2h-dark-border-color);
}
.d2h-auto-color-scheme .d2h-file-collapse {
border: 1px solid #0d1117;
border: 1px solid var(--d2h-dark-bg-color);
}
.d2h-auto-color-scheme .d2h-file-collapse.d2h-selected {
background-color: rgba(56, 139, 253, 0.1);
background-color: var(--d2h-dark-selected-color);
}
.d2h-auto-color-scheme .d2h-file-list-wrapper a,
.d2h-auto-color-scheme .d2h-file-list-wrapper a:visited {
color: #3572b0;
color: var(--d2h-dark-moved-label-color);
}
.d2h-auto-color-scheme .d2h-file-list > li {
border-bottom: 1px solid #0d1117;
border-bottom: 1px solid var(--d2h-dark-bg-color);
}
.d2h-dark-color-scheme .d2h-deleted {
color: #f85149;
color: var(--d2h-dark-del-label-color);
}
.d2h-auto-color-scheme .d2h-added {
color: #3fb950;
color: var(--d2h-dark-ins-label-color);
}
.d2h-auto-color-scheme .d2h-changed {
color: #d29922;
color: var(--d2h-dark-change-label-color);
}
.d2h-auto-color-scheme .d2h-moved {
color: #3572b0;
color: var(--d2h-dark-moved-label-color);
}
.d2h-auto-color-scheme .d2h-tag {
background-color: #0d1117;
background-color: var(--d2h-dark-bg-color);
}
.d2h-auto-color-scheme .d2h-deleted-tag {
border: 1px solid #f85149;
border: 1px solid var(--d2h-dark-del-label-color);
}
.d2h-auto-color-scheme .d2h-added-tag {
border: 1px solid #3fb950;
border: 1px solid var(--d2h-dark-ins-label-color);
}
.d2h-auto-color-scheme .d2h-changed-tag {
border: 1px solid #d29922;
border: 1px solid var(--d2h-dark-change-label-color);
}
.d2h-auto-color-scheme .d2h-moved-tag {
border: 1px solid #3572b0;
border: 1px solid var(--d2h-dark-moved-label-color);
}
}

View File

@@ -2,10 +2,11 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
height: calc(100vh - 4rem);
height: calc(100vh - 9rem);
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
font-family: ${(props) => (props.font ? props.font : 'default')};
font-size: ${(props) => props.theme.font.size.base};
line-break: anywhere;
}
@@ -60,6 +61,17 @@ const StyledWrapper = styled.div`
.cm-variable-invalid {
color: ${(props) => props.theme.codemirror.variable.invalid};
}
.CodeMirror-matchingbracket {
background: ${(props) => props.theme.status.success.background} !important;
text-decoration: unset;
}
.CodeMirror-nonmatchingbracket {
color: ${(props) => props.theme.colors.text.danger} !important;
background: ${(props) => props.theme.status.danger.background} !important;
text-decoration: unset;
}
`;
export default StyledWrapper;

View File

@@ -57,16 +57,6 @@ export default class CodeEditor extends React.Component {
scrollbarStyle: 'overlay',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
extraKeys: {
'Cmd-S': () => {
if (this.props.onSave) {
this.props.onSave();
}
},
'Ctrl-S': () => {
if (this.props.onSave) {
this.props.onSave();
}
},
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent',
'Cmd-H': 'replace',

View File

@@ -1,51 +0,0 @@
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from './CodeEditor/index';
import { IconDeviceFloppy } from '@tabler/icons';
import { saveApiSpecToFile } from 'providers/ReduxStore/slices/apiSpec';
import { useState } from 'react';
const FileEditor = ({ apiSpec }) => {
const dispatch = useDispatch();
const { displayedTheme, theme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const [content, setContent] = useState(apiSpec?.raw);
const onEdit = (value) => {
setContent(value);
};
const onSave = () => {
dispatch(saveApiSpecToFile({ uid: apiSpec?.uid, content }));
};
const hasChanges = Boolean(content != apiSpec?.raw);
const editorMode = 'yaml';
return (
<div className="flex flex-grow relative">
<CodeEditor
theme={displayedTheme}
value={content}
onEdit={onEdit}
onSave={onSave}
mode={editorMode}
font={get(preferences, 'font.codeFont', 'default')}
/>
<IconDeviceFloppy
onClick={onSave}
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
strokeWidth={1.5}
size={22}
className={`absolute right-0 top-0 m-4 ${
hasChanges ? 'cursor-pointer oapcity-100' : 'cursor-default opacity-50'
}`}
/>
</div>
);
};
export default FileEditor;

View File

@@ -2,15 +2,868 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
.swagger-root {
height: calc(100vh - 4rem);
border: solid 1px ${(props) => props.theme.codemirror.border};
height: calc(100vh - 7rem);
border-left: solid 1px ${(props) => props.theme.border.border1};
overflow-y: auto;
background: ${(props) => props.theme.bg};
padding-bottom: 20px;
&.dark {
.swagger-ui {
filter: invert(88%) hue-rotate(180deg);
/* ── Global reset ── */
.swagger-ui {
font-family: inherit;
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.text};
* {
border-color: ${(props) => props.theme.border.border1};
}
.swagger-ui .microlight {
filter: invert(100%) hue-rotate(180deg);
.auth-container {
padding: 0;
}
select {
box-shadow: none !important;
}
.wrapper {
padding: 0 20px;
max-width: none;
}
/* ── Info section ── */
.info {
margin: 16px 0 12px;
hgroup.main {
margin: 0;
}
.title {
font-size: 16px;
font-weight: 600;
color: ${(props) => props.theme.text};
small {
padding: 2px 6px !important;
font-size: 10px;
vertical-align: middle;
border-radius: 3px;
pre {
color: ${(props) => props.theme.text} !important;
font-size: 10px;
}
}
}
.base-url {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
.description {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
p, li {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
margin: 3px 0;
line-height: 1.5;
}
h1, h2, h3, h4, h5, h6 {
color: ${(props) => props.theme.text};
}
a {
color: ${(props) => props.theme.textLink};
}
}
}
/* Version / OAS badges */
.version-stamp span.version {
background: ${(props) => props.theme.border.border1} !important;
border: 1px solid ${(props) => props.theme.colors.text.muted} !important;
color: ${(props) => props.theme.text} !important;
font-size: 9px;
padding: 2px 6px;
border-radius: 3px;
}
.version-pragma {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
/* ── Tag section headings ── */
.opblock-tag-section {
.opblock-tag {
font-size: ${(props) => props.theme.font.size.md};
color: ${(props) => props.theme.text};
border-bottom: none;
padding: 0;
&:hover {
background: ${(props) => props.theme.background.mantle};
}
a {
color: ${(props) => props.theme.text} !important;
}
small {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
padding: 0 10px;
}
}
}
/* ── Operation blocks (GET, POST, PUT, DELETE, PATCH) ── */
.opblock {
margin: 0 0 8px;
border-radius: 4px;
border: 1px solid ${(props) => props.theme.border.border1} !important;
background: ${(props) => props.theme.bg} !important;
box-shadow: none !important;
.opblock-summary {
padding: 6px 10px;
border: none !important;
background: transparent !important;
.opblock-summary-method {
font-size: 10px;
font-weight: 700;
padding: 3px 8px;
min-width: 50px;
text-align: center;
border-radius: 3px;
}
.opblock-summary-path {
font-size: ${(props) => props.theme.font.size.sm};
a, span {
color: ${(props) => props.theme.text} !important;
}
}
.opblock-summary-description {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
.opblock-summary-control {
svg {
fill: ${(props) => props.theme.colors.text.muted};
width: 14px;
height: 14px;
}
}
}
.opblock-body {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.bg};
border-top: 1px solid ${(props) => props.theme.border.border1};
.opblock-description-wrapper,
.opblock-section {
p {
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.sm};
}
}
.tab-header .tab-item {
color: ${(props) => props.theme.colors.text.muted};
&.active {
color: ${(props) => props.theme.text};
}
}
select {
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 3px;
font-size: ${(props) => props.theme.font.size.xs};
padding: 2px 6px;
}
input[type="text"] {
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 3px;
font-size: ${(props) => props.theme.font.size.sm};
}
}
}
/* Method badge colors — keep them but tone down */
.opblock.opblock-get .opblock-summary-method { background: #61affe; color: #fff; }
.opblock.opblock-post .opblock-summary-method { background: #49cc90; color: #fff; }
.opblock.opblock-put .opblock-summary-method { background: #fca130; color: #fff; }
.opblock.opblock-delete .opblock-summary-method { background: #f93e3e; color: #fff; }
.opblock.opblock-patch .opblock-summary-method { background: #50e3c2; color: #000; }
/* Lock / authorization icons */
.authorization__btn {
svg {
fill: ${(props) => props.theme.colors.text.muted};
width: 14px;
height: 14px;
}
}
/* ── Tables ── */
table {
font-size: ${(props) => props.theme.font.size.sm};
thead {
tr {
th {
font-size: ${(props) => props.theme.font.size.xs} !important;
color: ${(props) => props.theme.colors.text.muted} !important;
border-bottom: 1px solid ${(props) => props.theme.border.border1} !important;
padding: 6px 0;
}
}
}
td {
padding: 6px 0;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
color: ${(props) => props.theme.text};
}
}
.parameter__name {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
&.required::after {
color: ${(props) => props.theme.colors.text.danger || '#c0392b'};
font-size: ${(props) => props.theme.font.size.xs};
}
}
.parameter__type {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
.parameter__in {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
/* ── Models / Schemas ── */
section.models {
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 4px;
background: ${(props) => props.theme.bg};
padding-bottom: 0px;
margin-bottom: 40px;
margin-top: 8px;
h4 {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
border-bottom: none;
padding: 6px 10px;
margin: 0;
svg {
fill: ${(props) => props.theme.colors.text.muted};
width: 16px;
height: 16px;
}
}
.model-container {
background: ${(props) => props.theme.bg} !important;
margin: 0;
padding: 4px 8px;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
&:last-child {
border-bottom: none;
}
.model-box {
background: ${(props) => props.theme.bg} !important;
padding: 2px 0;
}
}
}
.model {
font-size: 11px;
color: ${(props) => props.theme.text};
line-height: 1.4;
.prop-type {
color: ${(props) => props.theme.textLink};
font-size: 11px;
}
.prop-format {
color: ${(props) => props.theme.colors.text.muted};
font-size: 10px;
}
span.prop-enum {
display: block;
color: ${(props) => props.theme.colors.text.muted};
font-size: 10px;
}
}
.model-example {
.tab li {
color: ${(props) => props.theme.colors.text.muted} !important;
}
}
/* Model expand/collapse toggle */
.model-toggle {
cursor: pointer;
font-size: 10px;
color: ${(props) => props.theme.colors.text.muted};
&::after {
color: ${(props) => props.theme.colors.text.muted};
}
}
/* Model box inner styling */
.model-box {
background: ${(props) => props.theme.bg} !important;
color: ${(props) => props.theme.text};
}
/* Inner model details */
.inner-object {
color: ${(props) => props.theme.text};
}
/* Model title (schema name) */
.model-title {
color: ${(props) => props.theme.text};
font-size: 12px;
font-weight: 600;
}
/* ── JSON Schema 2020-12 (OpenAPI 3.1) schema overrides ── */
.json-schema-2020-12-accordion,
.json-schema-2020-12-expand-deep-button,
section.models h4 button,
.model-box button,
.models-control,
.opblock-summary,
.opblock-summary-control,
.opblock-tag {
outline: none !important;
box-shadow: none !important;
}
button:focus-visible,
.opblock-summary:focus-visible,
.opblock-tag:focus-visible,
.models-control:focus-visible {
outline: 2px solid ${(props) => props.theme.textLink} !important;
outline-offset: 2px;
}
.json-schema-2020-12__title {
font-size: 12px !important;
font-weight: 600;
color: ${(props) => props.theme.text} !important;
}
.json-schema-2020-12-head {
padding: 4px 8px !important;
background: ${(props) => props.theme.bg} !important;
.json-schema-2020-12-accordion {
padding: 0 !important;
color: ${(props) => props.theme.text} !important;
background: transparent !important;
}
/* chevron / arrow icon */
.json-schema-2020-12-accordion__icon {
fill: ${(props) => props.theme.colors.text.muted} !important;
}
button.json-schema-2020-12-expand-deep-button {
font-size: 10px !important;
color: ${(props) => props.theme.colors.text.muted} !important;
background: transparent !important;
padding: 0 4px !important;
}
strong.json-schema-2020-12__attribute--primary {
font-size: 11px !important;
color: ${(props) => props.theme.textLink} !important;
font-weight: normal;
}
}
.json-schema-2020-12-body {
font-size: 11px !important;
margin-left: 16px;
color: ${(props) => props.theme.text} !important;
.json-schema-2020-12-property {
margin-left: 8px;
color: ${(props) => props.theme.text} !important;
border-color: ${(props) => props.theme.border.border1} !important;
}
/* property names */
.json-schema-2020-12__title {
font-size: 11px !important;
font-weight: normal;
color: ${(props) => props.theme.text} !important;
}
/* type badges inside expanded schema */
strong.json-schema-2020-12__attribute--primary {
font-size: 10px !important;
color: ${(props) => props.theme.textLink} !important;
font-weight: normal;
}
strong.json-schema-2020-12__attribute {
font-size: 10px !important;
color: ${(props) => props.theme.colors.text.muted} !important;
font-weight: normal;
}
}
.json-schema-2020-12 {
font-size: 11px !important;
margin: 0 !important;
width: 100%;
height: 100%;
color: ${(props) => props.theme.text} !important;
background: ${(props) => props.theme.bg} !important;
}
/* JSON viewer (Examples section inside schema properties) */
.json-schema-2020-12-json-viewer {
background: transparent !important;
color: ${(props) => props.theme.text} !important;
}
.json-schema-2020-12-json-viewer__name {
color: ${(props) => props.theme.text} !important;
}
.json-schema-2020-12-json-viewer__name--secondary {
color: ${(props) => props.theme.colors.text.muted} !important;
font-weight: normal !important;
}
.json-schema-2020-12-json-viewer__value {
color: ${(props) => props.theme.text} !important;
}
.json-schema-2020-12-json-viewer__value--secondary {
color: ${(props) => props.theme.colors.text.subtext0} !important;
}
.json-schema-2020-12-json-viewer__value--string,
.json-schema-2020-12-json-viewer__value--string.json-schema-2020-12-json-viewer__value--secondary {
color: ${(props) => props.theme.colors.text.green} !important;
}
.json-schema-2020-12-json-viewer__value--number,
.json-schema-2020-12-json-viewer__value--bigint,
.json-schema-2020-12-json-viewer__value--number.json-schema-2020-12-json-viewer__value--secondary,
.json-schema-2020-12-json-viewer__value--bigint.json-schema-2020-12-json-viewer__value--secondary {
color: ${(props) => props.theme.textLink} !important;
}
.json-schema-2020-12-json-viewer__value--boolean,
.json-schema-2020-12-json-viewer__value--boolean.json-schema-2020-12-json-viewer__value--secondary {
color: ${(props) => props.theme.colors.text.warning} !important;
}
.json-schema-2020-12-json-viewer__value--null,
.json-schema-2020-12-json-viewer__value--undefined {
color: ${(props) => props.theme.colors.text.muted} !important;
}
/* enum/keyword example values container */
.json-schema-2020-12-keyword--examples,
[data-json-schema-keyword="examples"] {
color: ${(props) => props.theme.text} !important;
}
/* Model collapse/expand all link */
span.model-toggle {
color: ${(props) => props.theme.colors.text.muted};
font-size: 10px;
}
/* Brace styling in models */
.brace-open, .brace-close {
color: ${(props) => props.theme.colors.text.muted};
font-size: 11px;
}
/* ── Code / Response blocks ── */
.microlight {
background: ${(props) => props.theme.codemirror.bg} !important;
color: ${(props) => props.theme.text} !important;
font-size: ${(props) => props.theme.font.size.xs};
border-radius: 4px;
padding: 8px;
border: 1px solid ${(props) => props.theme.border.border1};
}
.highlight-code {
background: ${(props) => props.theme.codemirror.bg} !important;
> .microlight {
border: none;
}
}
pre {
color: ${(props) => props.theme.text};
font-size: ${(props) => props.theme.font.size.xs};
border-radius: 4px;
}
.response-col_status {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
}
.response-col_description {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
.responses-inner {
h4, h5 {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
}
}
/* ── Buttons ── */
.btn {
font-size: ${(props) => props.theme.font.size.xs};
border-radius: 4px;
box-shadow: none !important;
color: ${(props) => props.theme.text};
border-color: ${(props) => props.theme.border.border1};
background: transparent;
}
.btn.authorize {
color: ${(props) => props.theme.text};
border-color: ${(props) => props.theme.border.border1};
background: transparent;
svg {
fill: ${(props) => props.theme.text};
}
span {
color: ${(props) => props.theme.text};
}
}
.btn.execute {
background: ${(props) => props.theme.primary?.solid || props.theme.textLink};
color: #fff;
border-color: transparent;
}
.btn-group {
.btn {
background: ${(props) => props.theme.bg};
color: ${(props) => props.theme.text};
}
}
/* ── Links ── */
a {
color: ${(props) => props.theme.textLink};
}
/* ── Servers / Scheme container ── */
.scheme-container {
background: ${(props) => props.theme.background.mantle} !important;
border-top: 1px solid ${(props) => props.theme.border.border1};
border-bottom: 1px solid ${(props) => props.theme.border.border1};
padding: 10px;
box-shadow: none !important;
.schemes-title {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
label {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
select {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 4px;
padding: 4px 8px;
}
}
/* ── SVGs / icons ── */
svg {
fill: ${(props) => props.theme.colors.text.muted};
}
svg.arrow {
fill: ${(props) => props.theme.text};
width: 12px;
height: 12px;
margin-left: 4px;
}
.expand-operation svg {
fill: ${(props) => props.theme.colors.text.muted};
width: 14px;
height: 14px;
}
/* ── Misc / catch-all ── */
.loading-container .loading::after {
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.sm};
}
.renderedMarkdown p {
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.sm};
}
.opblock-section-header {
background: ${(props) => props.theme.background.mantle} !important;
box-shadow: none !important;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
padding: 6px 10px;
h4 {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
}
label {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
}
.copy-to-clipboard {
button {
background: ${(props) => props.theme.background.mantle};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 3px;
}
}
/* Dialog / modal overrides */
.dialog-ux {
.modal-ux {
background: ${(props) => props.theme.bg};
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 6px;
color: ${(props) => props.theme.text};
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
.modal-ux-header {
border-bottom: 1px solid ${(props) => props.theme.border.border1};
padding: 12px 0px;
h3 {
font-size: ${(props) => props.theme.font.size.md};
font-weight: 600;
color: ${(props) => props.theme.text};
}
.close-modal {
opacity: 0.6;
&:hover { opacity: 1; }
svg { fill: ${(props) => props.theme.text}; }
}
}
.modal-ux-content {
color: ${(props) => props.theme.text};
padding: 12px 16px;
p {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
/* Section headings like "api_key (apiKey)" */
h4, h5, h6 {
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 600;
color: ${(props) => props.theme.textLink};
margin: 12px 0 6px;
}
/* Labels: "Name:", "In:", "Flow:", "Value:", etc. */
label {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
> span {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
}
/* "Scopes:" heading */
.scopes h2 {
font-size: ${(props) => props.theme.font.size.sm} !important;
font-weight: 500;
color: ${(props) => props.theme.text} !important;
}
/* Scope item name + description */
.scopes .checkbox {
p.name {
font-size: ${(props) => props.theme.font.size.sm} !important;
color: ${(props) => props.theme.text} !important;
font-weight: 500;
margin: 0;
}
p.description {
font-size: ${(props) => props.theme.font.size.xs} !important;
color: ${(props) => props.theme.colors.text.muted} !important;
margin: 0;
}
}
/* Text inputs */
input[type="text"],
input[type="password"],
input[type="email"] {
background: ${(props) => props.theme.background.mantle} !important;
color: ${(props) => props.theme.text} !important;
border: 1px solid ${(props) => props.theme.border.border1} !important;
border-radius: 4px !important;
font-size: ${(props) => props.theme.font.size.sm} !important;
padding: 6px 10px !important;
outline: none !important;
box-shadow: none !important;
&:focus {
border-color: ${(props) => props.theme.textLink} !important;
outline: none !important;
box-shadow: none !important;
}
}
/* Checkboxes — custom styled to match theme */
input[type="checkbox"] {
appearance: none !important;
-webkit-appearance: none !important;
width: 14px !important;
height: 14px !important;
min-width: 14px;
border: 1px solid ${(props) => props.theme.border.border1} !important;
border-radius: 3px !important;
background: ${(props) => props.theme.background.mantle} !important;
cursor: pointer;
position: relative;
vertical-align: middle;
&:checked {
background: ${(props) => props.theme.textLink} !important;
border-color: ${(props) => props.theme.textLink} !important;
&::after {
content: '';
position: absolute;
left: 3px;
top: 1px;
width: 5px;
height: 8px;
border: 2px solid #fff;
border-top: none;
border-left: none;
transform: rotate(45deg);
}
}
}
/* "select all / select none" links */
a {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.textLink};
}
/* Dividers between auth sections */
hr {
border-color: ${(props) => props.theme.border.border1};
margin: 12px 0;
}
/* Authorize / Close buttons */
.btn-done,
.auth-btn-wrapper .btn {
font-size: ${(props) => props.theme.font.size.sm};
border-radius: 4px;
padding: 6px 16px;
border: 1px solid ${(props) => props.theme.border.border1};
background: transparent;
color: ${(props) => props.theme.text};
cursor: pointer;
outline: none !important;
box-shadow: none !important;
&:hover {
background: ${(props) => props.theme.background.mantle};
}
&.modal-btn-operation {
background: ${(props) => props.theme.textLink};
color: #fff;
border-color: transparent;
&:hover {
opacity: 0.9;
}
}
}
}
}
.backdrop-ux {
background: rgba(0, 0, 0, 0.5);
}
}
}
}

View File

@@ -1,16 +1,11 @@
import SwaggerUI from 'swagger-ui-react';
import StyledWrapper from './StyledWrapper';
import { useTheme } from 'providers/Theme';
const Swagger = ({ string }) => {
const { displayedTheme } = useTheme();
console.log('string', string);
const Swagger = ({ spec }) => {
return (
<StyledWrapper>
<div className={`swagger-root w-full overflow-y-scroll ${displayedTheme}`}>
<SwaggerUI spec={string} />
<div className="swagger-root w-full">
<SwaggerUI spec={spec} />
</div>
</StyledWrapper>
);

View File

@@ -0,0 +1,71 @@
import React, { useState, useEffect, Suspense } from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useSelector } from 'react-redux';
import { IconDeviceFloppy } from '@tabler/icons';
import CodeEditor from './FileEditor/CodeEditor/index';
import Swagger from './Renderers/Swagger';
/**
* Shared split-pane spec viewer: CodeEditor (left) + Swagger preview (right).
*
* Props:
* - content (string) The spec content (YAML/JSON string)
* - readOnly (boolean) If true, editor is not editable and save icon is hidden
* - onSave (function) Called with current editor content on save (editable mode only)
*/
const SpecViewer = ({ content, readOnly, onSave }) => {
const { displayedTheme, theme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const [editorContent, setEditorContent] = useState(content);
// Sync editor when saved content changes from outside (e.g. after save completes)
useEffect(() => {
setEditorContent(content);
}, [content]);
const hasChanges = !readOnly && editorContent !== content;
const handleSave = () => {
if (onSave) onSave(editorContent);
};
return (
<section className="main flex flex-grow pl-4 relative">
<div className="w-full grid grid-cols-2">
<div className="col-span-1">
<div className="flex flex-grow relative">
<CodeEditor
theme={displayedTheme}
value={readOnly ? content : editorContent}
readOnly={readOnly ? 'nocursor' : false}
onEdit={readOnly ? undefined : (val) => setEditorContent(val)}
onSave={readOnly ? undefined : handleSave}
mode="yaml"
font={get(preferences, 'font.codeFont', 'default')}
/>
{!readOnly && onSave && (
<IconDeviceFloppy
onClick={handleSave}
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
strokeWidth={1.5}
size={22}
className={`absolute right-0 top-0 m-4 ${
hasChanges ? 'cursor-pointer opacity-100' : 'cursor-default opacity-50'
}`}
/>
)}
</div>
</div>
<div className="col-span-1">
<Suspense fallback="">
<Swagger spec={content} />
</Suspense>
</div>
</div>
</section>
);
};
export default SpecViewer;

View File

@@ -3,13 +3,11 @@ import find from 'lodash/find';
import { useSelector, useDispatch } from 'react-redux';
import { IconFileCode, IconDots } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import FileEditor from './FileEditor';
import SpecViewer from './SpecViewer';
import Dropdown from 'components/Dropdown';
import { openApiSpec } from 'providers/ReduxStore/slices/apiSpec';
import { openApiSpec, saveApiSpecToFile } from 'providers/ReduxStore/slices/apiSpec';
import { useState } from 'react';
import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec';
import { Suspense } from 'react';
import Swagger from './Renderers/Swagger';
import toast from 'react-hot-toast';
const ApiSpecPanel = () => {
@@ -78,18 +76,10 @@ const ApiSpecPanel = () => {
</Dropdown>
</div>
</div>
<section className="main flex flex-grow px-4 relative">
<div className="w-full grid grid-cols-2">
<div className="col-span-1">
<FileEditor apiSpec={apiSpec} />
</div>
<div className="col-span-1">
<Suspense fallback="">
<Swagger string={raw} />
</Suspense>
</div>
</div>
</section>
<SpecViewer
content={raw}
onSave={(content) => dispatch(saveApiSpecToFile({ uid, content }))}
/>
</StyledWrapper>
);
};

View File

@@ -4,10 +4,12 @@ import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import { useDispatch, useSelector } from 'react-redux';
import { savePreferences, showHomePage, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
import { savePreferences, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';
import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
import { createWorkspaceWithUniqueName, openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
import { focusTab } from 'providers/ReduxStore/slices/tabs';
import get from 'lodash/get';
import Bruno from 'components/Bruno';
import MenuDropdown from 'ui/MenuDropdown';
@@ -129,7 +131,10 @@ const AppTitleBar = () => {
});
const handleHomeClick = () => {
dispatch(showHomePage());
const scratchCollectionUid = activeWorkspace?.scratchCollectionUid;
if (scratchCollectionUid) {
dispatch(focusTab({ uid: `${scratchCollectionUid}-overview` }));
}
};
const handleWorkspaceSwitch = (workspaceUid) => {
@@ -146,9 +151,19 @@ const AppTitleBar = () => {
}
};
const handleCreateWorkspace = () => {
setCreateWorkspaceModalOpen(true);
};
const handleCreateWorkspace = useCallback(async () => {
const defaultLocation = get(preferences, 'general.defaultLocation', '');
if (!defaultLocation) {
setCreateWorkspaceModalOpen(true);
return;
}
try {
await dispatch(createWorkspaceWithUniqueName(defaultLocation));
} catch (error) {
toast.error(error?.message || 'Failed to create workspace');
}
}, [preferences, dispatch]);
const handleManageWorkspaces = () => {
dispatch(showManageWorkspacePage());
@@ -236,7 +251,7 @@ const AppTitleBar = () => {
);
return items;
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace]);
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace, handleCreateWorkspace]);
return (
<StyledWrapper className={`app-titlebar ${osClass} ${isFullScreen ? 'fullscreen' : ''}`}>

View File

@@ -151,8 +151,14 @@ const StyledWrapper = styled.div`
//matching bracket fix
.CodeMirror-matchingbracket {
background: #5cc0b48c !important;
text-decoration:unset;
background: ${(props) => props.theme.status.success.background} !important;
text-decoration: unset;
}
.CodeMirror-nonmatchingbracket {
color: ${(props) => props.theme.colors.text.danger} !important;
background: ${(props) => props.theme.status.danger.background} !important;
text-decoration: unset;
}
.cm-search-line-highlight {

View File

@@ -74,26 +74,6 @@ export default class CodeEditor extends React.Component {
scrollbarStyle: 'overlay',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
extraKeys: {
'Cmd-Enter': () => {
if (this.props.onRun) {
this.props.onRun();
}
},
'Ctrl-Enter': () => {
if (this.props.onRun) {
this.props.onRun();
}
},
'Cmd-S': () => {
if (this.props.onSave) {
this.props.onSave();
}
},
'Ctrl-S': () => {
if (this.props.onSave) {
this.props.onSave();
}
},
'Cmd-F': (cm) => {
this.setState({ searchBarVisible: true }, () => {
this.searchBarRef.current?.focus();
@@ -217,6 +197,12 @@ export default class CodeEditor extends React.Component {
// Setup lint error tooltip on line number hover
this.cleanupLintErrorTooltip = setupLintErrorTooltip(editor);
// Add mousetrap class so Mousetrap captures shortcuts even when CodeMirror is focused
const cmInput = editor.getInputField();
if (cmInput) {
cmInput.classList.add('mousetrap');
}
}
}
@@ -233,17 +219,10 @@ export default class CodeEditor extends React.Component {
CodeMirror.signal(this.editor, 'change', this.editor);
}
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
// TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098
const nextValue = this.props.value ?? '';
const currentValue = this.editor.getValue();
if (this.editor.hasFocus?.() && currentValue !== nextValue) {
this.cachedValue = currentValue;
} else {
const cursor = this.editor.getCursor();
this.cachedValue = nextValue;
this.editor.setValue(nextValue);
this.editor.setCursor(cursor);
}
const cursor = this.editor.getCursor();
this.cachedValue = String(this?.props?.value ?? '');
this.editor.setValue(String(this.props.value) || '');
this.editor.setCursor(cursor);
}
if (this.editor) {

View File

@@ -8,6 +8,44 @@ function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');
}
const MAX_MATCHES = 99_999;
function findSearchMatches(editor, searchText, regex, caseSensitive, wholeWord) {
try {
let query, options = {};
if (regex) {
try {
query = new RegExp(searchText, caseSensitive ? 'g' : 'gi');
} catch (error) {
console.warn('Invalid regex provided in search!', error);
return [];
}
} else if (wholeWord) {
const escaped = escapeRegExp(searchText);
query = new RegExp(`\\b${escaped}\\b`, caseSensitive ? 'g' : 'gi');
} else {
query = searchText;
options = { caseFold: !caseSensitive };
}
const cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, options);
const out = [];
while (cursor.findNext()) {
out.push({ from: cursor.from(), to: cursor.to() });
if (out.length >= MAX_MATCHES) {
break;
}
}
return out;
} catch (e) {
console.error('Search error:', e);
return [];
}
}
function createCacheKey(editor, searchText, regex, caseSensitive, wholeWord) {
return `${editor.getValue().length}${searchText}${regex}${caseSensitive}${wholeWord}`;
}
const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
const [searchText, setSearchText] = useState('');
const [regex, setRegex] = useState(false);
@@ -19,49 +57,15 @@ const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
const searchMarks = useRef([]);
const searchLineHighlight = useRef(null);
const searchMatches = useRef([]);
const searchCacheKey = useRef('');
const inputRef = useRef(null);
const debouncedSearchText = useDebounce(searchText, 150);
const memoizedMatches = useMemo(() => {
if (!editor || !visible) return [];
if (!debouncedSearchText) return [];
try {
let query, options = {};
if (regex) {
try {
query = new RegExp(debouncedSearchText, caseSensitive ? 'g' : 'gi');
} catch {
return [];
}
} else if (wholeWord) {
const escaped = escapeRegExp(debouncedSearchText);
query = new RegExp(`\\b${escaped}\\b`, caseSensitive ? 'g' : 'gi');
} else {
query = debouncedSearchText;
options = { caseFold: !caseSensitive };
}
const cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, options);
const out = [];
while (cursor.findNext()) {
out.push({ from: cursor.from(), to: cursor.to() });
}
return out;
} catch (e) {
console.error('Search error:', e);
return [];
}
}, [editor, visible, debouncedSearchText, regex, caseSensitive, wholeWord]);
const debouncedSearchText = useDebounce(searchText, 250);
const doSearch = useCallback((newIndex = 0) => {
if (!editor) return;
if (!editor || !visible) {
return;
}
// Clear previous marks
searchMarks.current.forEach((mark) => mark.clear());
searchMarks.current = [];
// Clear previous line highlight
if (searchLineHighlight.current !== null) {
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
searchLineHighlight.current = null;
@@ -71,41 +75,89 @@ const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
setMatchCount(0);
setMatchIndex(0);
searchMatches.current = [];
searchMarks.current.forEach((mark) => mark.clear());
searchMarks.current = [];
return;
}
try {
const matches = memoizedMatches;
let matchIndex = matches.length ? Math.max(0, Math.min(newIndex, matches.length - 1)) : 0;
matches.forEach((m, i) => {
const mark = editor.markText(m.from, m.to, {
className: i === matchIndex ? 'cm-search-current' : 'cm-search-match',
clearOnEnter: true
});
searchMarks.current.push(mark);
});
const newCacheKey = createCacheKey(editor, debouncedSearchText, regex, caseSensitive, wholeWord);
const isCacheHit = newCacheKey === searchCacheKey.current;
if (matches.length) {
const currentLine = matches[matchIndex].from.line;
editor.addLineClass(currentLine, 'wrap', 'cm-search-line-highlight');
searchLineHighlight.current = currentLine;
editor.scrollIntoView(matches[matchIndex].from, 100);
editor.setSelection(matches[matchIndex].from, matches[matchIndex].to);
} else {
searchLineHighlight.current = null;
let matches = searchMatches.current;
if (!isCacheHit) {
matches = findSearchMatches(editor, debouncedSearchText, regex, caseSensitive, wholeWord);
searchMatches.current = matches;
searchCacheKey.current = newCacheKey;
setMatchCount(matches.length);
}
setMatchCount(matches.length);
if (!matches.length) {
setMatchIndex(0);
// Clear previous marks
searchMarks.current.forEach((mark) => mark.clear());
searchMarks.current = [];
return;
}
const matchIndex = Math.max(0, Math.min(newIndex, matches.length - 1));
setMatchIndex(matchIndex);
searchMatches.current = matches;
if (isCacheHit) {
// Clear only old current mark
const oldIndex = searchMarks.current.findIndex((mark) => mark.className?.includes('cm-search-current'));
if (oldIndex !== -1) {
searchMarks.current[oldIndex].clear();
searchMarks.current.splice(oldIndex, 1);
}
// Add mark to the new current and remark the previous and next
const toMark = [
// Previous
matchIndex > 0 ? matchIndex - 1 : null,
// Current
matchIndex,
// Next
matchIndex < matches.length - 1 ? matchIndex + 1 : null
].filter((i) => i !== null);
toMark.forEach((i) => {
const mark = editor.markText(matches[i].from, matches[i].to, {
className: i === matchIndex ? 'cm-search-current' : 'cm-search-match',
clearOnEnter: true
});
searchMarks.current.push(mark);
});
} else {
// Clear previous marks
searchMarks.current.forEach((mark) => mark.clear());
searchMarks.current = [];
// Mark all on new search
matches.forEach((m, i) => {
const mark = editor.markText(m.from, m.to, {
className: i === matchIndex ? 'cm-search-current' : 'cm-search-match',
clearOnEnter: true
});
searchMarks.current.push(mark);
});
}
const currentLine = matches[matchIndex].from.line;
editor.addLineClass(currentLine, 'wrap', 'cm-search-line-highlight');
searchLineHighlight.current = currentLine;
editor.scrollIntoView(matches[matchIndex].from, 100);
editor.setSelection(matches[matchIndex].from, matches[matchIndex].to);
} catch (e) {
console.error('Search error:', e);
setMatchCount(0);
setMatchIndex(0);
searchMatches.current = [];
searchCacheKey.current = '';
}
}, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, memoizedMatches]);
}, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, visible]);
useImperativeHandle(ref, () => ({
focus: () => {
@@ -116,7 +168,7 @@ const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
}));
useEffect(() => {
doSearch(0, debouncedSearchText);
doSearch(0);
}, [debouncedSearchText, doSearch]);
const handleSearchBarClose = useCallback(() => {
@@ -127,6 +179,7 @@ const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
searchLineHighlight.current = null;
}
searchMatches.current = [];
searchCacheKey.current = '';
if (onClose) onClose();
// Focus the editor after closing the search bar
if (editor) {
@@ -142,32 +195,27 @@ const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
const handleToggleRegex = () => {
setRegex((prev) => !prev);
setMatchIndex(0);
doSearch(0);
};
const handleToggleCase = () => {
setCaseSensitive((prev) => !prev);
setMatchIndex(0);
doSearch(0);
};
const handleToggleWholeWord = () => {
setWholeWord((prev) => !prev);
setMatchIndex(0);
doSearch(0);
};
const handleNext = () => {
if (!searchMatches.current || !searchMatches.current.length) return;
let next = (matchIndex + 1) % searchMatches.current.length;
setMatchIndex(next);
const next = (matchIndex + 1) % searchMatches.current.length;
doSearch(next);
};
const handlePrev = () => {
if (!searchMatches.current || !searchMatches.current.length) return;
let prev = (matchIndex - 1 + searchMatches.current.length) % searchMatches.current.length;
setMatchIndex(prev);
const prev = (matchIndex - 1 + searchMatches.current.length) % searchMatches.current.length;
doSearch(prev);
};

View File

@@ -0,0 +1,61 @@
import styled from 'styled-components';
import { rgba } from 'polished';
const StyledWrapper = styled.div`
.code-snippet {
font-family: monospace;
font-size: ${(props) => props.theme.font.size.xs};
line-height: 1.4;
overflow-x: auto;
border-radius: ${(props) => props.theme.border.radius.base};
background-color: ${(props) => props.theme.background.elevated};
border: 1px solid ${(props) => props.theme.border.border2};
}
.code-line {
display: flex;
align-items: stretch;
}
.code-line.highlighted-error {
background-color: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};
border-left: 3px solid ${(props) => props.theme.colors.text.danger};
}
.code-line.highlighted-warning {
background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.1)};
border-left: 3px solid ${(props) => props.theme.colors.text.warning};
}
.code-line:not(.highlighted-error):not(.highlighted-warning) {
border-left: 3px solid transparent;
}
.code-line-number {
min-width: 2.5rem;
text-align: right;
padding: 0 0.5rem;
color: ${(props) => props.theme.colors.text.muted};
user-select: none;
flex-shrink: 0;
}
.code-line-content {
white-space: pre;
padding: 0 0.5rem;
flex: 1;
min-width: 0;
}
.code-line-separator {
border-left: 3px solid transparent;
}
.separator-content {
color: ${(props) => props.theme.colors.text.muted};
user-select: none;
padding: 0 0.5rem;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
const renderLine = (line, highlightClass, hunkIdx) => {
const isHighlighted = line.isHighlighted || line.isError;
const key = hunkIdx != null ? `${hunkIdx}-${line.lineNumber}` : line.lineNumber;
return (
<div
key={key}
className={`code-line ${isHighlighted ? highlightClass : ''}`}
data-testid={isHighlighted ? 'code-line-error' : 'code-line'}
>
<span className="code-line-number">{line.lineNumber}</span>
<span className="code-line-content">
{isHighlighted ? '> ' : ' '}{line.content}
</span>
</div>
);
};
const CodeSnippet = ({ lines, hunks, variant = 'error' }) => {
const highlightClass = variant === 'warning' ? 'highlighted-warning' : 'highlighted-error';
if (hunks?.length) {
return (
<StyledWrapper>
<div className="code-snippet" data-testid="code-snippet">
{hunks.map((hunk, idx) => (
<React.Fragment key={idx}>
{hunk.hasSeparatorBefore && (
<div className="code-line code-line-separator">
<span className="code-line-number"></span>
<span className="code-line-content separator-content">{'\u22EE'}</span>
</div>
)}
{hunk.lines.map((line) => renderLine(line, highlightClass, idx))}
</React.Fragment>
))}
</div>
</StyledWrapper>
);
}
if (!lines?.length) return null;
return (
<StyledWrapper>
<div className="code-snippet" data-testid="code-snippet">
{lines.map((line) => renderLine(line, highlightClass))}
</div>
</StyledWrapper>
);
};
export default CodeSnippet;

View File

@@ -0,0 +1,140 @@
import '@testing-library/jest-dom';
import React from 'react';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import CodeSnippet from './index';
const theme = {
font: { size: { xs: '0.75rem' } },
background: { elevated: '#f5f5f5' },
border: { border2: '#e0e0e0', radius: { base: '4px' } },
colors: { text: { danger: '#ef4444', warning: '#f59e0b', muted: '#999' } }
};
const renderWithTheme = (component) => {
return render(
<ThemeProvider theme={theme}>
{component}
</ThemeProvider>
);
};
const sampleLines = [
{ lineNumber: 3, content: 'const a = 1;', isHighlighted: false },
{ lineNumber: 4, content: 'undefinedVar.foo();', isHighlighted: true },
{ lineNumber: 5, content: 'const b = 2;', isHighlighted: false }
];
describe('CodeSnippet', () => {
it('should render nothing when lines is empty', () => {
const { container } = renderWithTheme(<CodeSnippet lines={[]} />);
expect(container.firstChild).toBeNull();
});
it('should render nothing when lines is null', () => {
const { container } = renderWithTheme(<CodeSnippet lines={null} />);
expect(container.firstChild).toBeNull();
});
it('should render all lines with line numbers', () => {
renderWithTheme(<CodeSnippet lines={sampleLines} />);
expect(screen.getByText('3')).toBeInTheDocument();
expect(screen.getByText('4')).toBeInTheDocument();
expect(screen.getByText('5')).toBeInTheDocument();
});
it('should apply error highlight class by default', () => {
const { container } = renderWithTheme(<CodeSnippet lines={sampleLines} variant="error" />);
const highlightedLine = container.querySelector('.highlighted-error');
expect(highlightedLine).toBeInTheDocument();
});
it('should apply warning highlight class when variant is warning', () => {
const { container } = renderWithTheme(<CodeSnippet lines={sampleLines} variant="warning" />);
const highlightedLine = container.querySelector('.highlighted-warning');
expect(highlightedLine).toBeInTheDocument();
expect(container.querySelector('.highlighted-error')).not.toBeInTheDocument();
});
it('should show > prefix on highlighted line for accessibility', () => {
const { container } = renderWithTheme(<CodeSnippet lines={sampleLines} />);
const codeLineContents = container.querySelectorAll('.code-line-content');
// The highlighted line (index 1) should start with "> "
expect(codeLineContents[1].textContent).toContain('> ');
// Non-highlighted lines should not have ">"
expect(codeLineContents[0].textContent).not.toContain('>');
});
it('should also support isError property for backward compatibility', () => {
const linesWithIsError = [
{ lineNumber: 1, content: 'line 1', isError: false },
{ lineNumber: 2, content: 'error line', isError: true },
{ lineNumber: 3, content: 'line 3', isError: false }
];
const { container } = renderWithTheme(<CodeSnippet lines={linesWithIsError} />);
expect(container.querySelector('.highlighted-error')).toBeInTheDocument();
});
describe('hunks prop', () => {
const sampleHunks = [
{
hasSeparatorBefore: false,
lines: [
{ lineNumber: 1, content: 'const a = true;', isHighlighted: false },
{ lineNumber: 2, content: 'pm.vault.get();', isHighlighted: true },
{ lineNumber: 3, content: 'const b = false;', isHighlighted: false }
]
},
{
hasSeparatorBefore: true,
lines: [
{ lineNumber: 10, content: 'const x = null;', isHighlighted: false },
{ lineNumber: 11, content: 'pm.cookies.jar();', isHighlighted: true },
{ lineNumber: 12, content: 'const y = undefined;', isHighlighted: false }
]
}
];
it('should render all lines from all hunks', () => {
renderWithTheme(<CodeSnippet hunks={sampleHunks} variant="warning" />);
// line numbers
expect(screen.getByText('1')).toBeInTheDocument();
expect(screen.getByText('2')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.getByText('11')).toBeInTheDocument();
// content
expect(screen.getByText(/const a = true;/)).toBeInTheDocument();
expect(screen.getByText(/pm\.vault\.get\(\);/)).toBeInTheDocument();
expect(screen.getByText(/const x = null;/)).toBeInTheDocument();
expect(screen.getByText(/pm\.cookies\.jar\(\);/)).toBeInTheDocument();
});
it('should render separator between hunks when hasSeparatorBefore is true', () => {
const { container } = renderWithTheme(<CodeSnippet hunks={sampleHunks} variant="warning" />);
const separators = container.querySelectorAll('.code-line-separator');
expect(separators).toHaveLength(1);
// separator should appear between the two hunks, not before the first
const allRows = container.querySelectorAll('.code-line, .code-line-separator');
const separatorIndex = Array.from(allRows).findIndex((el) => el.classList.contains('code-line-separator'));
// first hunk has 3 lines (indices 0-2), separator should be at index 3
expect(separatorIndex).toBe(3);
});
it('should render the ellipsis character in separator', () => {
const { container } = renderWithTheme(<CodeSnippet hunks={sampleHunks} variant="warning" />);
const separator = container.querySelector('.separator-content');
expect(separator.textContent).toBe('\u22EE');
});
it('should apply warning highlights within hunks', () => {
const { container } = renderWithTheme(<CodeSnippet hunks={sampleHunks} variant="warning" />);
const highlighted = container.querySelectorAll('.highlighted-warning');
expect(highlighted).toHaveLength(2);
});
it('should render nothing when hunks is empty array', () => {
const { container } = renderWithTheme(<CodeSnippet hunks={[]} />);
expect(container.firstChild).toBeNull();
});
});
});

View File

@@ -51,6 +51,11 @@ const AuthMode = ({ collection }) => {
label: 'NTLM Auth',
onClick: () => onModeChange('ntlm')
},
{
id: 'oauth1',
label: 'OAuth 1.0',
onClick: () => onModeChange('oauth1')
},
{
id: 'oauth2',
label: 'OAuth 2.0',

View File

@@ -0,0 +1,26 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import OAuth1 from 'components/RequestPane/Auth/OAuth1';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
const CollectionOAuth1 = ({ collection }) => {
const dispatch = useDispatch();
const request = collection.draft?.root
? get(collection, 'draft.root.request', {})
: get(collection, 'root.request', {});
const save = () => dispatch(saveCollectionSettings(collection.uid));
return (
<OAuth1
collection={collection}
request={request}
save={save}
updateAuth={updateCollectionAuth}
/>
);
};
export default CollectionOAuth1;

View File

@@ -12,6 +12,7 @@ import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/
import StyledWrapper from './StyledWrapper';
import OAuth2 from './OAuth2';
import NTLMAuth from './NTLMAuth';
import OAuth1 from './Oauth1';
import Button from 'ui/Button';
const Auth = ({ collection }) => {
@@ -37,6 +38,9 @@ const Auth = ({ collection }) => {
case 'ntlm': {
return <NTLMAuth collection={collection} />;
}
case 'oauth1': {
return <OAuth1 collection={collection} />;
}
case 'oauth2': {
return <OAuth2 collection={collection} />;
}

View File

@@ -1,9 +1,25 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
position: relative;
height: 100%;
overflow-y: auto;
.editing-mode {
cursor: pointer;
position: sticky;
top: 0;
z-index: 10;
background: ${(props) => props.theme.bg};
padding: 6px 0;
margin-bottom: 10px;
display: flex;
justify-content: flex-end;
}
.markdown-body {
height: auto !important;
overflow-y: visible !important;
}
`;

View File

@@ -1,9 +1,10 @@
import React, { useState, useCallback } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { setCollectionHeaders } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import SingleLineEditor from 'components/SingleLineEditor';
import EditableTable from 'components/EditableTable';
import StyledWrapper from './StyledWrapper';
@@ -18,11 +19,21 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const headers = collection.draft?.root
? get(collection, 'draft.root.request.headers', [])
: get(collection, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const collectionHeadersWidths = focusedTab?.tableColumnWidths?.['collection-headers'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const toggleBulkEditMode = () => {
setIsBulkEditMode(!isBulkEditMode);
};
@@ -114,11 +125,14 @@ const Headers = ({ collection }) => {
Add request headers that will be sent with every request in this collection.
</div>
<EditableTable
tableId="collection-headers"
columns={columns}
rows={headers}
onChange={handleHeadersChange}
defaultRow={defaultRow}
getRowError={getRowError}
columnWidths={collectionHeadersWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('collection-headers', widths)}
/>
<div className="flex justify-end mt-2">
<button className="text-link select-none" onClick={toggleBulkEditMode}>

View File

@@ -1,9 +1,11 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import get from 'lodash/get';
import find from 'lodash/find';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { updateScriptPaneTab } from 'providers/ReduxStore/slices/tabs';
import { useTheme } from 'providers/Theme';
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
import StatusDot from 'components/StatusDot';
@@ -18,27 +20,24 @@ const Script = ({ collection }) => {
const requestScript = collection.draft?.root ? get(collection, 'draft.root.request.script.req', '') : get(collection, 'root.request.script.req', '');
const responseScript = collection.draft?.root ? get(collection, 'draft.root.request.script.res', '') : get(collection, 'root.request.script.res', '');
// Default to post-response if pre-request script is empty
const getInitialTab = () => {
const tabs = useSelector((state) => state.tabs.tabs);
const focusedTab = find(tabs, (t) => t.uid === collection.uid);
const scriptPaneTab = focusedTab?.scriptPaneTab;
const getDefaultTab = () => {
const hasPreRequestScript = requestScript && requestScript.trim().length > 0;
return hasPreRequestScript ? 'pre-request' : 'post-response';
};
const [activeTab, setActiveTab] = useState(getInitialTab);
const prevCollectionUidRef = useRef(collection.uid);
const activeTab = scriptPaneTab || getDefaultTab();
const setActiveTab = (tab) => {
dispatch(updateScriptPaneTab({ uid: collection.uid, scriptPaneTab: tab }));
};
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
// Update active tab only when switching to a different collection
useEffect(() => {
if (prevCollectionUidRef.current !== collection.uid) {
prevCollectionUidRef.current = collection.uid;
const hasPreRequestScript = requestScript && requestScript.trim().length > 0;
setActiveTab(hasPreRequestScript ? 'pre-request' : 'post-response');
}
}, [collection.uid, requestScript]);
// Refresh CodeMirror when tab becomes visible
useEffect(() => {
const timer = setTimeout(() => {

View File

@@ -1,6 +1,10 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.markdown-body {
height: auto !important;
overflow-y: visible !important;
}
div.tabs {
div.tab {
padding: 6px 0px;
@@ -24,7 +28,8 @@ const StyledWrapper = styled.div`
}
&.active {
font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;
font-weight: ${(props) =>
props.theme.tabs.active.fontWeight} !important;
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
@@ -45,7 +50,7 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.colors.text.muted};
}
input[type='radio'] {
input[type="radio"] {
cursor: pointer;
accent-color: ${(props) => props.theme.primary.solid};
}

View File

@@ -1,7 +1,8 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import MultiLineEditor from 'components/MultiLineEditor';
import InfoTip from 'components/InfoTip';
import EditableTable from 'components/EditableTable';
@@ -13,6 +14,16 @@ import { setCollectionVars } from 'providers/ReduxStore/slices/collections/index
const VarsTable = ({ collection, vars, varType }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const collectionVarsWidths = focusedTab?.tableColumnWidths?.['collection-vars'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const onSave = () => dispatch(saveCollectionSettings(collection.uid));
@@ -68,11 +79,14 @@ const VarsTable = ({ collection, vars, varType }) => {
return (
<StyledWrapper className="w-full">
<EditableTable
tableId="collection-vars"
columns={columns}
rows={vars}
onChange={handleVarsChange}
defaultRow={defaultRow}
getRowError={getRowError}
columnWidths={collectionVarsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('collection-vars', widths)}
/>
</StyledWrapper>
);

View File

@@ -106,42 +106,42 @@ const CollectionSettings = ({ collection }) => {
return (
<StyledWrapper className="flex flex-col h-full relative px-4 py-4 overflow-hidden">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('overview')} role="tab" onClick={() => setTab('overview')}>
<div className={getTabClassname('overview')} role="tab" data-testid="collection-settings-tab-overview" onClick={() => setTab('overview')}>
Overview
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
<div className={getTabClassname('headers')} role="tab" data-testid="collection-settings-tab-headers" onClick={() => setTab('headers')}>
Headers
{activeHeadersCount > 0 && <sup className="ml-1 font-medium">{activeHeadersCount}</sup>}
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
<div className={getTabClassname('vars')} role="tab" data-testid="collection-settings-tab-vars" onClick={() => setTab('vars')}>
Vars
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
<div className={getTabClassname('auth')} role="tab" data-testid="collection-settings-tab-auth" onClick={() => setTab('auth')}>
Auth
{authMode !== 'none' && <StatusDot />}
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
<div className={getTabClassname('script')} role="tab" data-testid="collection-settings-tab-script" onClick={() => setTab('script')}>
Script
{hasScripts && <StatusDot />}
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => setTab('tests')}>
<div className={getTabClassname('tests')} role="tab" data-testid="collection-settings-tab-tests" onClick={() => setTab('tests')}>
Tests
{hasTests && <StatusDot />}
</div>
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
<div className={getTabClassname('presets')} role="tab" data-testid="collection-settings-tab-presets" onClick={() => setTab('presets')}>
Presets
{hasPresets && <StatusDot />}
</div>
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
<div className={getTabClassname('proxy')} role="tab" data-testid="collection-settings-tab-proxy" onClick={() => setTab('proxy')}>
Proxy
{Object.keys(proxyConfig).length > 0 && proxyEnabled && <StatusDot />}
</div>
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
<div className={getTabClassname('clientCert')} role="tab" data-testid="collection-settings-tab-clientCert" onClick={() => setTab('clientCert')}>
Client Certificates
{clientCertConfig.length > 0 && <StatusDot />}
</div>
<div className={getTabClassname('protobuf')} role="tab" onClick={() => setTab('protobuf')}>
<div className={getTabClassname('protobuf')} role="tab" data-testid="collection-settings-tab-protobuf" onClick={() => setTab('protobuf')}>
Protobuf
{protobufConfig.protoFiles && protobufConfig.protoFiles.length > 0 && <StatusDot />}
</div>

View File

@@ -113,7 +113,7 @@ const SessionList = ({ sessions, activeSessionId, onSelectSession, onCloseSessio
return (
<StyledSessionList>
{sessions.map((session) => {
{sessions.map((session, idx) => {
const { name } = getSessionDisplayInfo(session);
return (
<ToolHint
@@ -125,6 +125,7 @@ const SessionList = ({ sessions, activeSessionId, onSelectSession, onCloseSessio
>
<div
className={`session-list-item ${activeSessionId === session.sessionId ? 'active' : ''}`}
data-testid={`session-list-${idx}`}
onClick={() => onSelectSession(session.sessionId)}
>
<div className="session-name">
@@ -133,6 +134,7 @@ const SessionList = ({ sessions, activeSessionId, onSelectSession, onCloseSessio
</div>
<div
className="session-close-btn"
data-testid={`session-close-${idx}`}
onClick={(e) => {
e.stopPropagation();
onCloseSession(session.sessionId);

View File

@@ -1,8 +1,20 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
height: 100%;
overflow-y: auto;
position: relative;
.editing-mode {
cursor: pointer;
position: sticky;
z-index: 10;
top: 0;
background: ${(props) => props.theme.bg};
padding-bottom: 0.5em;
}
.markdown-body {
height: auto !important;
overflow-y: visible !important;
}
`;

View File

@@ -1,6 +1,8 @@
import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import find from 'lodash/find';
import { updateRequestDocs } from 'providers/ReduxStore/slices/collections';
import { updateDocsEditing } from 'providers/ReduxStore/slices/tabs';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -12,12 +14,15 @@ import StyledWrapper from './StyledWrapper';
const Documentation = ({ item, collection }) => {
const dispatch = useDispatch();
const { displayedTheme } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const isEditing = focusedTab?.docsEditing || false;
const docs = item.draft ? get(item, 'draft.request.docs') : get(item, 'request.docs');
const preferences = useSelector((state) => state.app.preferences);
const toggleViewMode = () => {
setIsEditing((prev) => !prev);
dispatch(updateDocsEditing({ uid: activeTabUid, docsEditing: !isEditing }));
};
const onEdit = (value) => {

View File

@@ -85,6 +85,17 @@ const Wrapper = styled.div`
justify-content: center;
}
.dropdown-tab-count {
margin-left: auto;
font-size: 11px;
font-weight: 500;
padding: 1px 6px;
border-radius: 10px;
background: ${(props) => props.theme.dropdown.hoverBg};
min-width: 18px;
text-align: center;
}
&:hover:not(:disabled):not(.disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}

View File

@@ -63,7 +63,7 @@ const StyledWrapper = styled.div`
height: 100%;
cursor: col-resize;
background: transparent;
z-index: 100;
z-index: 10;
&:hover,
&.resizing {

View File

@@ -7,6 +7,7 @@ import StyledWrapper from './StyledWrapper';
const MIN_COLUMN_WIDTH = 80;
const EditableTable = ({
tableId, // Not being used kept to maintain uniqueness & pass similar in onColumnWidthsChange
columns,
rows,
onChange,
@@ -20,20 +21,20 @@ const EditableTable = ({
reorderable = false,
onReorder,
showAddRow = true,
testId = 'editable-table'
testId = 'editable-table',
columnWidths,
onColumnWidthsChange
}) => {
const tableRef = useRef(null);
const emptyRowUidRef = useRef(null);
const [hoveredRow, setHoveredRow] = useState(null);
const [resizing, setResizing] = useState(null);
const [tableHeight, setTableHeight] = useState(0);
const [columnWidths, setColumnWidths] = useState(() => {
const initialWidths = {};
columns.forEach((col) => {
initialWidths[col.key] = col.width || 'auto';
});
return initialWidths;
});
const widths = columnWidths || {};
const handleColumnWidthsChange = useCallback((newWidths) => {
onColumnWidthsChange?.(newWidths);
}, [onColumnWidthsChange]);
const handleResizeStart = useCallback((e, columnKey) => {
e.preventDefault();
@@ -59,11 +60,13 @@ const EditableTable = ({
const maxShrink = startWidth - MIN_COLUMN_WIDTH;
const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff));
setColumnWidths((prev) => ({
...prev,
const newWidths = {
...widths,
[columnKey]: `${startWidth + clampedDiff}px`,
[nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px`
}));
};
handleColumnWidthsChange(newWidths);
};
const handleMouseUp = () => {
@@ -88,7 +91,7 @@ const EditableTable = ({
});
if (Object.keys(newWidths).length > 0) {
setColumnWidths((prev) => ({ ...prev, ...newWidths }));
handleColumnWidthsChange({ ...widths, ...newWidths });
}
}
setResizing(null);
@@ -98,7 +101,7 @@ const EditableTable = ({
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}, [columns, showCheckbox]);
}, [columns, showCheckbox, widths, handleColumnWidthsChange]);
// Track table height for resize handles
useEffect(() => {
@@ -118,8 +121,8 @@ const EditableTable = ({
}, [rows.length]);
const getColumnWidth = useCallback((column) => {
return columnWidths[column.key] || column.width || 'auto';
}, [columnWidths]);
return widths[column.key] || column.width || 'auto';
}, [widths]);
const createEmptyRow = useCallback(() => {
const newUid = uuid();

View File

@@ -96,6 +96,18 @@ const Wrapper = styled.div`
max-width: 200px !important;
}
.name-cell-wrapper {
position: relative;
width: 100%;
}
.no-results {
padding: 24px;
text-align: center;
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
input[type='text'] {
width: 100%;
border: 1px solid transparent;

View File

@@ -3,7 +3,8 @@ import { TableVirtuoso } from 'react-virtuoso';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useSelector } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import MultiLineEditor from 'components/MultiLineEditor/index';
import StyledWrapper from './StyledWrapper';
import { uuid } from 'utils/common';
@@ -44,12 +45,41 @@ const EnvironmentVariablesTable = ({
}) => {
const { storedTheme } = useTheme();
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
const activeWorkspace = useSelector((state) => {
const uid = state.workspaces?.activeWorkspaceUid;
return state.workspaces?.workspaces?.find((w) => w.uid === uid);
});
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const hasDraftForThisEnv = draft?.environmentUid === environment.uid;
const [tableHeight, setTableHeight] = useState(MIN_H);
const [columnWidths, setColumnWidths] = useState({ name: '30%', value: 'auto' });
// Use environment UID as part of tableId so each environment has its own column widths
const tableId = `env-vars-table-${environment.uid}`;
// Get column widths from Redux - derived value (not state)
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const storedColumnWidths = focusedTab?.tableColumnWidths?.[tableId];
// Local state initialized from Redux (computed once on mount/environment change via key)
const [columnWidths, setColumnWidths] = useState(() => {
return storedColumnWidths || { name: '30%', value: 'auto' };
});
const [resizing, setResizing] = useState(null);
const [pinnedData, setPinnedData] = useState({ query: '', uids: new Set() });
const handleColumnWidthsChange = (id, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId: id, widths }));
};
// Store column widths in ref for access in event handlers
const columnWidthsRef = useRef(columnWidths);
columnWidthsRef.current = columnWidths;
const handleResizeStart = useCallback((e, columnKey) => {
e.preventDefault();
@@ -72,27 +102,38 @@ const EnvironmentVariablesTable = ({
const maxShrink = startWidth - MIN_COLUMN_WIDTH;
const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff));
setColumnWidths({
const newWidths = {
[columnKey]: `${startWidth + clampedDiff}px`,
[nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px`
});
};
setColumnWidths(newWidths);
};
const handleMouseUp = () => {
setResizing(null);
// Save to Redux after resize ends using ref for latest values
handleColumnWidthsChange(tableId, columnWidthsRef.current);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}, []);
}, [handleColumnWidthsChange]);
const handleTotalHeightChanged = useCallback((h) => {
setTableHeight(h);
}, []);
const handleRowFocus = useCallback((uid) => {
setPinnedData((prev) => ({
query: searchQuery,
uids: prev.query === searchQuery ? new Set([...prev.uids, uid]) : new Set([uid])
}));
}, [searchQuery]);
const prevEnvUidRef = useRef(null);
const prevEnvVariablesRef = useRef(environment.variables);
const mountedRef = useRef(false);
let _collection = collection ? cloneDeep(collection) : {};
@@ -101,6 +142,12 @@ const EnvironmentVariablesTable = ({
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
}
// When collection is null (global/workspace environments), populate process env
// variables from the active workspace so that {{process.env.X}} can resolve
if (!collection && activeWorkspace?.processEnvVariables) {
_collection.workspaceProcessEnvVariables = activeWorkspace.processEnvVariables;
}
const initialValues = useMemo(() => {
const vars = environment.variables || [];
return [
@@ -168,11 +215,13 @@ const EnvironmentVariablesTable = ({
useEffect(() => {
const isMount = !mountedRef.current;
const envChanged = prevEnvUidRef.current !== null && prevEnvUidRef.current !== environment.uid;
const variablesReloaded = !isMount && !envChanged && prevEnvVariablesRef.current !== environment.variables;
prevEnvUidRef.current = environment.uid;
prevEnvVariablesRef.current = environment.variables;
mountedRef.current = true;
if ((isMount || envChanged) && hasDraftForThisEnv && draft?.variables) {
if ((isMount || envChanged || variablesReloaded) && hasDraftForThisEnv && draft?.variables) {
formik.setValues([
...draft.variables,
{
@@ -185,12 +234,16 @@ const EnvironmentVariablesTable = ({
}
]);
}
}, [environment.uid, hasDraftForThisEnv, draft?.variables]);
}, [environment.uid, environment.variables, hasDraftForThisEnv, draft?.variables]);
const savedValuesJson = useMemo(() => {
return JSON.stringify((environment.variables || []).map(stripEnvVarUid));
}, [environment.variables]);
useEffect(() => {
setPinnedData({ query: '', uids: new Set() });
}, [savedValuesJson]);
// Sync modified state
useEffect(() => {
const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');
@@ -344,6 +397,7 @@ const EnvironmentVariablesTable = ({
onSave(cloneDeep(variablesToSave))
.then(() => {
toast.success('Changes saved successfully');
onDraftClear();
const newValues = [
...variablesToSave,
{
@@ -362,7 +416,7 @@ const EnvironmentVariablesTable = ({
console.error(error);
toast.error('An error occurred while saving the changes');
});
}, [formik.values, environment.variables, onSave, setIsModified]);
}, [formik.values, environment.variables, onSave, onDraftClear, setIsModified]);
const handleReset = useCallback(() => {
const originalVars = environment.variables || [];
@@ -404,132 +458,157 @@ const EnvironmentVariablesTable = ({
const query = searchQuery.toLowerCase().trim();
return allVariables.filter(({ variable, index }) => {
const isLastRow = index === formik.values.length - 1;
const isEmptyRow = !variable.name || variable.name.trim() === '';
if (isLastRow && isEmptyRow) {
return true;
}
const effectivePins = pinnedData.query === searchQuery ? pinnedData.uids : new Set();
return allVariables.filter(({ variable }) => {
if (effectivePins.has(variable.uid)) return true;
const nameMatch = variable.name ? variable.name.toLowerCase().includes(query) : false;
const valueMatch = typeof variable.value === 'string' ? variable.value.toLowerCase().includes(query) : false;
const valueText
= typeof variable.value === 'string'
? variable.value
: typeof variable.value === 'number' || typeof variable.value === 'boolean'
? String(variable.value)
: '';
const valueMatch = valueText.toLowerCase().includes(query);
return !!(nameMatch || valueMatch);
});
}, [formik.values, searchQuery]);
}, [formik.values, searchQuery, pinnedData]);
const isSearchActive = !!searchQuery?.trim();
return (
<StyledWrapper className={resizing ? 'is-resizing' : ''}>
<TableVirtuoso
className="table-container"
style={{ height: tableHeight }}
components={{ TableRow }}
data={filteredVariables}
totalListHeightChanged={handleTotalHeightChanged}
fixedHeaderContent={() => (
<tr>
<td className="text-center"></td>
<td style={{ width: columnWidths.name }}>
Name
<div
className={`resize-handle ${resizing === 'name' ? 'resizing' : ''}`}
style={{ height: tableHeight > 0 ? `${tableHeight}px` : undefined }}
onMouseDown={(e) => handleResizeStart(e, 'name')}
/>
</td>
<td style={{ width: columnWidths.value }}>Value</td>
<td className="text-center">Secret</td>
<td></td>
</tr>
)}
fixedItemHeight={35}
computeItemKey={(virtualIndex, item) => `${environment.uid}-${item.index}`}
itemContent={(virtualIndex, { variable, index: actualIndex }) => {
const isLastRow = actualIndex === formik.values.length - 1;
const isEmptyRow = !variable.name || variable.name.trim() === '';
const isLastEmptyRow = isLastRow && isEmptyRow;
return (
<>
<td className="text-center">
{!isLastEmptyRow && (
<input
type="checkbox"
className="mousetrap"
name={`${actualIndex}.enabled`}
checked={variable.enabled}
onChange={formik.handleChange}
/>
)}
</td>
{isSearchActive && filteredVariables.length === 0 ? (
<div className="no-results">No results found for &ldquo;{searchQuery.trim()}&rdquo;</div>
) : (
<TableVirtuoso
className="table-container"
style={{ height: tableHeight }}
components={{ TableRow }}
data={filteredVariables}
totalListHeightChanged={handleTotalHeightChanged}
fixedHeaderContent={() => (
<tr>
<td className="text-center"></td>
<td style={{ width: columnWidths.name }}>
<div className="flex items-center">
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className="mousetrap"
id={`${actualIndex}.name`}
name={`${actualIndex}.name`}
value={variable.name}
placeholder={!variable.value || (typeof variable.value === 'string' && variable.value.trim() === '') ? 'Value' : ''}
onChange={(e) => handleNameChange(actualIndex, e)}
onBlur={() => handleNameBlur(actualIndex)}
onKeyDown={(e) => handleNameKeyDown(actualIndex, e)}
/>
<ErrorMessage name={`${actualIndex}.name`} index={actualIndex} />
</div>
Name
<div
className={`resize-handle ${resizing === 'name' ? 'resizing' : ''}`}
style={{ height: tableHeight > 0 ? `${tableHeight}px` : undefined }}
onMouseDown={(e) => handleResizeStart(e, 'name')}
/>
</td>
<td className="flex flex-row flex-nowrap items-center" style={{ width: columnWidths.value }}>
<div className="overflow-hidden grow w-full relative">
<MultiLineEditor
theme={storedTheme}
collection={_collection}
name={`${actualIndex}.value`}
value={variable.value}
placeholder={isLastEmptyRow ? 'Value' : ''}
isSecret={variable.secret}
readOnly={typeof variable.value !== 'string'}
onChange={(newValue) => formik.setFieldValue(`${actualIndex}.value`, newValue, true)}
onSave={handleSave}
/>
</div>
{typeof variable.value !== 'string' && (
<span className="ml-2 flex items-center">
<IconInfoCircle id={`${variable.uid}-disabled-info-icon`} className="text-muted" size={16} />
<Tooltip
anchorId={`${variable.uid}-disabled-info-icon`}
content="Non-string values set via scripts are read-only and can only be updated through scripts."
place="top"
<td style={{ width: columnWidths.value }}>Value</td>
<td className="text-center">Secret</td>
<td></td>
</tr>
)}
fixedItemHeight={35}
computeItemKey={(virtualIndex, item) => `${environment.uid}-${item.index}`}
itemContent={(virtualIndex, { variable, index: actualIndex }) => {
const isLastRow = actualIndex === formik.values.length - 1;
const isEmptyRow = !variable.name || variable.name.trim() === '';
const isLastEmptyRow = isLastRow && isEmptyRow;
return (
<>
<td className="text-center">
{!isLastEmptyRow && (
<input
type="checkbox"
className="mousetrap"
name={`${actualIndex}.enabled`}
checked={variable.enabled}
onChange={formik.handleChange}
/>
</span>
)}
{renderExtraValueContent && renderExtraValueContent(variable)}
</td>
<td className="text-center">
{!isLastEmptyRow && (
<input
type="checkbox"
className="mousetrap"
name={`${actualIndex}.secret`}
checked={variable.secret}
onChange={formik.handleChange}
/>
)}
</td>
<td>
{!isLastEmptyRow && (
<button onClick={() => handleRemoveVar(variable.uid)}>
<IconTrash strokeWidth={1.5} size={18} />
</button>
)}
</td>
</>
);
}}
/>
)}
</td>
<td style={{ width: columnWidths.name }}>
<div className="flex items-center">
<div className="name-cell-wrapper">
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className="mousetrap"
id={`${actualIndex}.name`}
name={`${actualIndex}.name`}
value={variable.name}
placeholder={!variable.value || (typeof variable.value === 'string' && variable.value.trim() === '') ? 'Name' : ''}
onChange={(e) => handleNameChange(actualIndex, e)}
onFocus={() => handleRowFocus(variable.uid)}
onBlur={() => {
handleNameBlur(actualIndex);
}}
onKeyDown={(e) => handleNameKeyDown(actualIndex, e)}
/>
</div>
<ErrorMessage name={`${actualIndex}.name`} index={actualIndex} />
</div>
</td>
<td
className="flex flex-row flex-nowrap items-center"
style={{ width: columnWidths.value }}
>
<div
className="overflow-hidden grow w-full relative"
onFocus={() => handleRowFocus(variable.uid)}
>
<MultiLineEditor
theme={storedTheme}
collection={_collection}
name={`${actualIndex}.value`}
value={variable.value}
placeholder={isLastEmptyRow ? 'Value' : ''}
isSecret={variable.secret}
readOnly={typeof variable.value !== 'string'}
onChange={(newValue) => {
formik.setFieldValue(`${actualIndex}.value`, newValue, true);
// Clear ephemeral metadata when user manually edits the value
if (variable.ephemeral) {
formik.setFieldValue(`${actualIndex}.ephemeral`, undefined, false);
formik.setFieldValue(`${actualIndex}.persistedValue`, undefined, false);
}
}}
onSave={handleSave}
/>
</div>
{typeof variable.value !== 'string' && (
<span className="ml-2 flex items-center">
<IconInfoCircle id={`${variable.uid}-disabled-info-icon`} className="text-muted" size={16} />
<Tooltip
anchorId={`${variable.uid}-disabled-info-icon`}
content="Non-string values set via scripts are read-only and can only be updated through scripts."
place="top"
/>
</span>
)}
{renderExtraValueContent && renderExtraValueContent(variable)}
</td>
<td className="text-center">
{!isLastEmptyRow && (
<input
type="checkbox"
className="mousetrap"
name={`${actualIndex}.secret`}
checked={variable.secret}
onChange={formik.handleChange}
/>
)}
</td>
<td>
{!isLastEmptyRow && (
<button onClick={() => handleRemoveVar(variable.uid)}>
<IconTrash strokeWidth={1.5} size={18} />
</button>
)}
</td>
</>
);
}}
/>
)}
<div className="button-container">
<div className="flex items-center">

View File

@@ -55,6 +55,7 @@ const StyledWrapper = styled.div`
}
.section-title {
padding-right: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;

View File

@@ -8,11 +8,12 @@ const CollapsibleSection = ({
onToggle,
badge,
actions,
children
children,
testId
}) => {
return (
<StyledWrapper className={expanded ? 'expanded' : 'collapsed'}>
<div className="section-header" onClick={onToggle}>
<div className="section-header" onClick={onToggle} data-testid={testId}>
<div className="section-title-wrapper">
<IconChevronRight
size={14}

View File

@@ -44,6 +44,7 @@ const DotEnvFileDetails = ({
className={`toggle-btn ${viewMode === 'raw' ? 'active' : ''}`}
onClick={() => onViewModeChange?.('raw')}
aria-pressed={viewMode === 'raw'}
data-testid="dotenv-view-raw"
>
Raw
</button>

View File

@@ -13,7 +13,7 @@ const DotEnvRawView = ({
}) => {
return (
<>
<div className="raw-editor-container">
<div className="raw-editor-container" data-testid="dotenv-raw-editor">
<CodeEditor
collection={collection}
item={item}

View File

@@ -4,6 +4,7 @@ import { uuid } from 'utils/common';
import { useFormik } from 'formik';
import { variableNameRegex } from 'utils/common/regex';
import toast from 'react-hot-toast';
import useDeferredLoading from 'hooks/useDeferredLoading';
import StyledWrapper from './StyledWrapper';
import DotEnvTableView from './DotEnvTableView';
@@ -31,6 +32,7 @@ const DotEnvFileEditor = ({
const [rawValue, setRawValue] = useState(initialRawValue);
const [prevViewMode, setPrevViewMode] = useState(viewMode);
const [isSaving, setIsSaving] = useState(false);
const showSaving = useDeferredLoading(isSaving, 200);
const formikRef = useRef(null);
@@ -311,7 +313,7 @@ const DotEnvFileEditor = ({
onChange={handleRawChange}
onSave={handleSaveRaw}
onReset={handleReset}
isSaving={isSaving}
isSaving={showSaving}
/>
</StyledWrapper>
);
@@ -335,7 +337,7 @@ const DotEnvFileEditor = ({
onRemoveVar={handleRemoveVar}
onSave={handleSave}
onReset={handleReset}
isSaving={isSaving}
isSaving={showSaving}
/>
</StyledWrapper>
);

View File

@@ -1,17 +1,8 @@
import { uuid } from 'utils/common';
import { utils } from '@usebruno/common';
export const variablesToRaw = (variables) => {
return variables
.filter((v) => v.name && v.name.trim() !== '')
.map((v) => {
const value = v.value || '';
if (value.includes('\n') || value.includes('"') || value.includes('\'')) {
const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
return `${v.name}="${escapedValue}"`;
}
return `${v.name}=${value}`;
})
.join('\n');
return utils.jsonToDotenv(variables);
};
export const rawToVariables = (rawContent) => {
@@ -37,9 +28,16 @@ export const rawToVariables = (rawContent) => {
const name = trimmedLine.substring(0, equalIndex).trim();
let value = trimmedLine.substring(equalIndex + 1);
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
if (value.startsWith('\'') && value.endsWith('\'')) {
// Single-quoted values are fully literal in dotenv — no unescaping
value = value.slice(1, -1);
value = value.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
} else if (value.startsWith('`') && value.endsWith('`')) {
// Backtick-quoted values are fully literal in dotenv — no unescaping
value = value.slice(1, -1);
} else if (value.startsWith('"') && value.endsWith('"')) {
// Double-quoted values: unescape \", \n, and \r (the escapes we produce)
value = value.slice(1, -1);
value = value.replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\r/g, '\r');
}
if (name) {

View File

@@ -46,7 +46,7 @@ const EnvironmentListContent = ({
</div>
</ToolHint>
<div className="dropdown-item configure-button">
<button onClick={onSettingsClick} id="configure-env">
<button onClick={onSettingsClick} id="configure-env" data-testid="configure-env">
<IconSettings size={16} strokeWidth={1.5} />
<span>Configure</span>
</button>

View File

@@ -9,6 +9,7 @@ const Wrapper = styled.div`
border: 1px solid ${(props) => props.theme.app.collection.toolbar.environmentSelector.border};
line-height: 1rem;
transition: all 0.15s ease;
height: 24px;
&:hover {
border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBorder};
@@ -73,7 +74,7 @@ const Wrapper = styled.div`
border-top: 0.0625rem solid ${(props) => props.theme.dropdown.separator};
z-index: 10;
margin: 0;
&:hover {
background-color: ${(props) => props.theme.dropdown.bg + ' !important'};
}
@@ -119,7 +120,7 @@ const Wrapper = styled.div`
.environment-list {
flex: 1;
overflow-y: auto;
max-height: calc(75vh - 8rem);
max-height: calc(75vh - 8rem);
padding-bottom: 2.625rem;
}

View File

@@ -103,6 +103,7 @@ const EnvironmentVariables = ({ environment, setIsModified, collection, searchQu
return (
<EnvironmentVariablesTable
key={environment?.uid}
environment={environment}
collection={collection}
onSave={handleSave}

View File

@@ -12,7 +12,7 @@ const StyledWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 8px 20px;
padding: 9px 20px 8px 20px;
flex-shrink: 0;
.title {

View File

@@ -1,7 +1,6 @@
import { IconCopy, IconEdit, IconTrash, IconCheck, IconX, IconSearch } from '@tabler/icons';
import { useState, useRef } from 'react';
import { useDispatch } from 'react-redux';
import useDebounce from 'hooks/useDebounce';
import { renameEnvironment, updateEnvironmentColor } from 'providers/ReduxStore/slices/collections/actions';
import { validateName, validateNameError } from 'utils/common/regex';
import toast from 'react-hot-toast';
@@ -11,7 +10,7 @@ import EnvironmentVariables from './EnvironmentVariables';
import ColorPicker from 'components/ColorPicker';
import StyledWrapper from './StyledWrapper';
const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
const EnvironmentDetails = ({ environment, setIsModified, collection, searchQuery, setSearchQuery, isSearchExpanded, setIsSearchExpanded, debouncedSearchQuery, searchInputRef }) => {
const dispatch = useDispatch();
const environments = collection?.environments || [];
@@ -20,11 +19,7 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
const [isRenaming, setIsRenaming] = useState(false);
const [newName, setNewName] = useState('');
const [nameError, setNameError] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
const debouncedSearchQuery = useDebounce(searchQuery, 300);
const inputRef = useRef(null);
const searchInputRef = useRef(null);
const validateEnvironmentName = (name) => {
if (!name || name.trim() === '') {

View File

@@ -32,19 +32,6 @@ const StyledWrapper = styled.div`
flex-direction: column;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 16px 12px 16px;
.title {
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
color: ${(props) => props.theme.text};
margin: 0;
}
.btn-action {
display: flex;
align-items: center;
@@ -66,35 +53,54 @@ const StyledWrapper = styled.div`
}
}
.search-container {
.env-list-search {
position: relative;
padding: 0 12px 12px 12px;
.search-icon {
display: flex;
align-items: center;
margin: 0 4px 6px 4px;
.env-list-search-icon {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-100%);
left: 8px;
color: ${(props) => props.theme.colors.text.muted};
pointer-events: none;
}
.search-input {
.env-list-search-input {
width: 100%;
padding: 6px 8px 6px 28px;
padding: 5px 24px 5px 26px;
font-size: 12px;
background: transparent;
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: 5px;
border-radius: 6px;
color: ${(props) => props.theme.text};
transition: all 0.15s ease;
transition: border-color 0.15s ease;
&::placeholder {
color: ${(props) => props.theme.colors.text.muted};
}
&:focus {
outline: none;
border-color: ${(props) => props.theme.colors.accent};
}
}
.env-list-search-clear {
position: absolute;
right: 4px;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
background: transparent;
border: none;
cursor: pointer;
color: ${(props) => props.theme.colors.text.muted};
border-radius: 3px;
&:hover {
color: ${(props) => props.theme.text};
}
}
}
@@ -104,7 +110,15 @@ const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0 8px;
padding: 8px;
}
.section-header {
margin-inline: 4px !important;
padding-left: 6px !important;
border-radius: 6px ;
padding-right: 3px !important;
padding-block: 4px !important;
}
.environments-list {
@@ -130,6 +144,10 @@ const StyledWrapper = styled.div`
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
color: ${(props) => props.theme.text};
}
&.active {
color: ${(props) => props.theme.colors.accent};
}
}
.environment-item {
@@ -143,7 +161,7 @@ const StyledWrapper = styled.div`
font-size: 13px;
color: ${(props) => props.theme.text};
cursor: pointer;
border-radius: 5px;
border-radius: 6px;
transition: background 0.15s ease;
.environment-name {

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState, useRef, useCallback } from 'react';
import usePrevious from 'hooks/usePrevious';
import useOnClickOutside from 'hooks/useOnClickOutside';
import useDebounce from 'hooks/useDebounce';
import EnvironmentDetails from './EnvironmentDetails';
import { IconDownload, IconUpload, IconSearch, IconPlus, IconCheck, IconX, IconFileAlert } from '@tabler/icons';
import Button from 'ui/Button';
@@ -23,6 +24,7 @@ import {
deleteDotEnvFile
} from 'providers/ReduxStore/slices/collections/actions';
import { setEnvironmentsDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';
import { setEnvVarSearchQuery, setEnvVarSearchExpanded } from 'providers/ReduxStore/slices/app';
import { validateName, validateNameError } from 'utils/common/regex';
import toast from 'react-hot-toast';
import classnames from 'classnames';
@@ -40,9 +42,14 @@ const EnvironmentList = ({
setShowExportModal
}) => {
const dispatch = useDispatch();
const envSearchQuery = useSelector((state) => state.app.envVarSearch?.collection?.query ?? '');
const isEnvSearchExpanded = useSelector((state) => state.app.envVarSearch?.collection?.expanded ?? false);
const setEnvSearchQuery = (q) => dispatch(setEnvVarSearchQuery({ context: 'collection', query: q }));
const setIsEnvSearchExpanded = (v) => dispatch(setEnvVarSearchExpanded({ context: 'collection', expanded: v }));
const [openImportModal, setOpenImportModal] = useState(false);
const [searchText, setSearchText] = useState('');
const envListSearchInputRef = useRef(null);
const [isCreatingInline, setIsCreatingInline] = useState(false);
const [renamingEnvUid, setRenamingEnvUid] = useState(null);
const [newEnvName, setNewEnvName] = useState('');
@@ -65,6 +72,9 @@ const EnvironmentList = ({
const dotEnvInputRef = useRef(null);
const dotEnvCreateContainerRef = useRef(null);
const debouncedEnvSearchQuery = useDebounce(envSearchQuery, 300);
const envSearchInputRef = useRef(null);
const dotEnvFiles = useSelector((state) => {
const coll = state.collections.collections.find((c) => c.uid === collection?.uid);
return coll?.dotEnvFiles || EMPTY_ARRAY;
@@ -73,6 +83,8 @@ const EnvironmentList = ({
const envUids = environments ? environments.map((env) => env.uid) : [];
const prevEnvUids = usePrevious(envUids);
const environmentsDraftUid = collection?.environmentsDraft?.environmentUid;
const handleDotEnvModifiedChange = useCallback((modified) => {
setIsDotEnvModified(modified);
if (modified) {
@@ -81,10 +93,10 @@ const EnvironmentList = ({
environmentUid: `dotenv:${selectedDotEnvFile}`,
variables: []
}));
} else {
} else if (environmentsDraftUid?.startsWith('dotenv:')) {
dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));
}
}, [dispatch, collection.uid, selectedDotEnvFile]);
}, [dispatch, collection.uid, selectedDotEnvFile, environmentsDraftUid]);
useEffect(() => {
if (dotEnvFiles.length === 0) {
@@ -497,6 +509,12 @@ const EnvironmentList = ({
setIsModified={setIsModified}
originalEnvironmentVariables={originalEnvironmentVariables}
collection={collection}
searchQuery={envSearchQuery}
setSearchQuery={setEnvSearchQuery}
isSearchExpanded={isEnvSearchExpanded}
setIsSearchExpanded={setIsEnvSearchExpanded}
debouncedSearchQuery={debouncedEnvSearchQuery}
searchInputRef={envSearchInputRef}
/>
);
}
@@ -531,20 +549,6 @@ const EnvironmentList = ({
)}
<div className="sidebar">
<div className="sidebar-header">
<h2 className="title">Variables</h2>
</div>
<div className="search-container">
<IconSearch size={14} strokeWidth={1.5} className="search-icon" />
<input
type="text"
placeholder="Search..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="search-input"
/>
</div>
<div className="sections-container">
<CollapsibleSection
@@ -553,18 +557,67 @@ const EnvironmentList = ({
onToggle={() => setEnvironmentsExpanded(!environmentsExpanded)}
actions={(
<>
<button type="button" className="btn-action" onClick={() => handleCreateEnvClick()} title="Create environment">
<button
type="button"
className="btn-action"
onClick={() => {
if (!environmentsExpanded) setEnvironmentsExpanded(true);
handleCreateEnvClick();
}}
title="Create environment"
>
<IconPlus size={14} strokeWidth={1.5} />
</button>
<button type="button" className="btn-action" onClick={() => handleImportClick()} title="Import environment">
<button
type="button"
className="btn-action"
onClick={() => {
if (!environmentsExpanded) setEnvironmentsExpanded(true);
handleImportClick();
}}
title="Import environment"
>
<IconDownload size={14} strokeWidth={1.5} />
</button>
<button type="button" className="btn-action" onClick={() => handleExportClick()} title="Export environment">
<button
type="button"
className="btn-action"
onClick={() => {
if (!environmentsExpanded) setEnvironmentsExpanded(true);
handleExportClick();
}}
title="Export environment"
>
<IconUpload size={14} strokeWidth={1.5} />
</button>
</>
)}
>
<div className="env-list-search">
<IconSearch size={13} strokeWidth={1.5} className="env-list-search-icon" />
<input
ref={envListSearchInputRef}
type="text"
placeholder="Search environments..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="env-list-search-input"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
/>
{searchText && (
<button
className="env-list-search-clear"
title="Clear search"
onClick={() => setSearchText('')}
onMouseDown={(e) => e.preventDefault()}
>
<IconX size={12} strokeWidth={1.5} />
</button>
)}
</div>
<div className="environments-list">
{filteredEnvironments.map((env) => (
<div
@@ -683,6 +736,7 @@ const EnvironmentList = ({
<CollapsibleSection
title=".env Files"
testId="dotenv-files-section"
expanded={dotEnvExpanded}
onToggle={() => setDotEnvExpanded(!dotEnvExpanded)}
badge={dotEnvFiles.length}
@@ -691,6 +745,7 @@ const EnvironmentList = ({
className="btn-action"
onClick={handleCreateDotEnvInlineClick}
title="Create .env file"
data-testid="create-dotenv-file"
>
<IconPlus size={14} strokeWidth={1.5} />
</button>
@@ -715,6 +770,7 @@ const EnvironmentList = ({
ref={dotEnvInputRef}
type="text"
className="environment-name-input"
data-testid="dotenv-name-input"
value={newDotEnvName}
onChange={handleDotEnvNameChange}
onKeyDown={handleDotEnvNameKeyDown}

View File

@@ -34,23 +34,36 @@ class ErrorBoundary extends Component {
const serializeArgs = (args) => {
return args.map((arg) => {
const seen = new WeakSet();
const replacer = (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
if (value instanceof Error || Object.prototype.toString.call(value) === '[object Error]' || (typeof value.message === 'string' && typeof value.stack === 'string')) {
const error = {};
Object.getOwnPropertyNames(value).forEach((prop) => {
error[prop] = value[prop];
});
return error;
}
}
return value;
};
try {
if (arg === null) return 'null';
if (arg === undefined) return 'undefined';
if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') {
return arg;
}
if (arg instanceof Error) {
return {
__type: 'Error',
name: arg.name,
message: arg.message,
stack: arg.stack
};
}
if (typeof arg === 'object') {
try {
return JSON.parse(JSON.stringify(arg));
return JSON.parse(JSON.stringify(arg, replacer));
} catch {
return String(arg);
}

View File

@@ -0,0 +1,10 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
color: ${(props) => props.theme.colors.danger};
pre {
color: ${(props) => props.theme.colors.danger};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import { useState } from 'react';
import StyledWrapper from './StyledWrapper';
const IpcErrorModal = ({ error }) => {
const [showModal, setShowModal] = useState(true);
return (
<>
{showModal ? (
<StyledWrapper>
<Portal>
<Modal
size="sm"
title="Error"
hideFooter={true}
hideCancel={true}
handleCancel={() => {
setShowModal(false);
}}
disableCloseOnOutsideClick={true}
disableEscapeKey={true}
>
<pre className="w-full flex flex-wrap whitespace-pre-wrap">{error}</pre>
</Modal>
</Portal>
</StyledWrapper>
) : null}
</>
);
};
export default IpcErrorModal;

View File

@@ -14,6 +14,7 @@ import BasicAuth from 'components/RequestPane/Auth/BasicAuth';
import BearerAuth from 'components/RequestPane/Auth/BearerAuth';
import DigestAuth from 'components/RequestPane/Auth/DigestAuth';
import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
import OAuth1 from 'components/RequestPane/Auth/OAuth1';
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
@@ -143,6 +144,17 @@ const Auth = ({ collection, folder }) => {
/>
);
}
case 'oauth1': {
return (
<OAuth1
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'wsse': {
return (
<WsseAuth

View File

@@ -47,6 +47,11 @@ const AuthMode = ({ collection, folder }) => {
label: 'NTLM Auth',
onClick: () => onModeChange('ntlm')
},
{
id: 'oauth1',
label: 'OAuth 1.0',
onClick: () => onModeChange('oauth1')
},
{
id: 'oauth2',
label: 'OAuth 2.0',

View File

@@ -1,9 +1,18 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
height: 100%;
overflow-y: auto;
position: relative;
.editing-mode {
cursor: pointer;
color: ${(props) => props.theme.colors.text.yellow};
position: sticky;
top: 0;
z-index: 10;
background: ${(props) => props.theme.bg};
padding-bottom: 0.5em;
}
`;

View File

@@ -1,9 +1,10 @@
import React, { useState, useCallback } from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { setFolderHeaders } from 'providers/ReduxStore/slices/collections';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import SingleLineEditor from 'components/SingleLineEditor';
import EditableTable from 'components/EditableTable';
import StyledWrapper from './StyledWrapper';
@@ -18,11 +19,21 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection, folder }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const headers = folder.draft
? get(folder, 'draft.request.headers', [])
: get(folder, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const folderHeadersWidths = focusedTab?.tableColumnWidths?.['folder-headers'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const toggleBulkEditMode = () => {
setIsBulkEditMode(!isBulkEditMode);
};
@@ -119,11 +130,14 @@ const Headers = ({ collection, folder }) => {
Request headers that will be sent with every request inside this folder.
</div>
<EditableTable
tableId="folder-headers"
columns={columns}
rows={headers}
onChange={handleHeadersChange}
defaultRow={defaultRow}
getRowError={getRowError}
columnWidths={folderHeadersWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('folder-headers', widths)}
/>
<div className="flex justify-end mt-2">
<button className="text-link select-none" onClick={toggleBulkEditMode}>

View File

@@ -1,9 +1,11 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import get from 'lodash/get';
import find from 'lodash/find';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateFolderRequestScript, updateFolderResponseScript } from 'providers/ReduxStore/slices/collections';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import { updateScriptPaneTab } from 'providers/ReduxStore/slices/tabs';
import { useTheme } from 'providers/Theme';
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
import StatusDot from 'components/StatusDot';
@@ -18,27 +20,25 @@ const Script = ({ collection, folder }) => {
const requestScript = folder.draft ? get(folder, 'draft.request.script.req', '') : get(folder, 'root.request.script.req', '');
const responseScript = folder.draft ? get(folder, 'draft.request.script.res', '') : get(folder, 'root.request.script.res', '');
// Default to post-response if pre-request script is empty
const getInitialTab = () => {
const tabs = useSelector((state) => state.tabs.tabs);
const focusedTab = find(tabs, (t) => t.uid === folder.uid);
const scriptPaneTab = focusedTab?.scriptPaneTab;
// Default to post-response if pre-request script is empty (only when scriptPaneTab is null/undefined)
const getDefaultTab = () => {
const hasPreRequestScript = requestScript && requestScript.trim().length > 0;
return hasPreRequestScript ? 'pre-request' : 'post-response';
};
const [activeTab, setActiveTab] = useState(getInitialTab);
const prevFolderUidRef = useRef(folder.uid);
const activeTab = scriptPaneTab || getDefaultTab();
const setActiveTab = (tab) => {
dispatch(updateScriptPaneTab({ uid: folder.uid, scriptPaneTab: tab }));
};
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
// Update active tab only when switching to a different folder
useEffect(() => {
if (prevFolderUidRef.current !== folder.uid) {
prevFolderUidRef.current = folder.uid;
const hasPreRequestScript = requestScript && requestScript.trim().length > 0;
setActiveTab(hasPreRequestScript ? 'pre-request' : 'post-response');
}
}, [folder.uid, requestScript]);
// Refresh CodeMirror when tab becomes visible
useEffect(() => {
const timer = setTimeout(() => {

View File

@@ -2,6 +2,12 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
position: relative;
.markdown-body {
height: auto !important;
overflow-y: visible !important;
}
div.tabs {
div.tab {

View File

@@ -1,7 +1,8 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import MultiLineEditor from 'components/MultiLineEditor';
import InfoTip from 'components/InfoTip';
import EditableTable from 'components/EditableTable';
@@ -13,6 +14,16 @@ import { setFolderVars } from 'providers/ReduxStore/slices/collections/index';
const VarsTable = ({ folder, collection, vars, varType }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
// Get column widths from Redux
const focusedTab = tabs?.find((t) => t.uid === activeTabUid);
const folderVarsWidths = focusedTab?.tableColumnWidths?.['folder-vars'] || {};
const handleColumnWidthsChange = (tableId, widths) => {
dispatch(updateTableColumnWidths({ uid: activeTabUid, tableId, widths }));
};
const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
@@ -74,11 +85,14 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
return (
<StyledWrapper className="w-full">
<EditableTable
tableId="folder-vars"
columns={columns}
rows={vars}
onChange={handleVarsChange}
defaultRow={defaultRow}
getRowError={getRowError}
columnWidths={folderVarsWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('folder-vars', widths)}
/>
</StyledWrapper>
);

View File

@@ -77,27 +77,27 @@ const FolderSettings = ({ collection, folder }) => {
<StyledWrapper className="flex flex-col h-full overflow-auto">
<div className="flex flex-col h-full relative px-4 py-4">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
<div className={getTabClassname('headers')} role="tab" data-testid="folder-settings-tab-headers" onClick={() => setTab('headers')}>
Headers
{activeHeadersCount > 0 && <sup className="ml-1 font-medium">{activeHeadersCount}</sup>}
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
<div className={getTabClassname('script')} role="tab" data-testid="folder-settings-tab-script" onClick={() => setTab('script')}>
Script
{hasScripts && <StatusDot />}
</div>
<div className={getTabClassname('test')} role="tab" onClick={() => setTab('test')}>
<div className={getTabClassname('test')} role="tab" data-testid="folder-settings-tab-test" onClick={() => setTab('test')}>
Test
{hasTests && <StatusDot />}
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
<div className={getTabClassname('vars')} role="tab" data-testid="folder-settings-tab-vars" onClick={() => setTab('vars')}>
Vars
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
<div className={getTabClassname('auth')} role="tab" data-testid="folder-settings-tab-auth" onClick={() => setTab('auth')}>
Auth
{hasAuth && <StatusDot />}
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
<div className={getTabClassname('docs')} role="tab" data-testid="folder-settings-tab-docs" onClick={() => setTab('docs')}>
Docs
</div>
</div>

View File

@@ -0,0 +1,62 @@
import React from 'react';
import Modal from 'components/Modal/index';
import Portal from 'components/Portal/index';
const getOSName = () => {
const platform = window.navigator.userAgentData?.platform || '';
if (platform.startsWith('Win')) {
return 'Windows';
} else if (platform.startsWith('Mac')) {
return 'macOS';
} else if (platform.startsWith('Linux')) {
return 'Linux';
} else {
return 'your OS';
}
};
const getDownloadUrl = (os) => {
switch (os) {
case 'Windows':
return 'https://git-scm.com/download/win';
case 'macOS':
return 'https://git-scm.com/download/mac';
case 'Linux':
return 'https://git-scm.com/download/linux';
default:
return 'https://git-scm.com/download';
}
};
const GitNotFoundModal = ({ onClose }) => {
const osName = getOSName();
const downloadUrl = getDownloadUrl(osName);
return (
<Portal>
<Modal
size="sm"
title="Git Not Found"
handleCancel={onClose}
hideFooter={true}
>
<div>
<p>Git was not detected on your system. You need to install Git to proceed.</p>
<p className="mt-2">
You can download Git for <strong>{osName}</strong> here:
</p>
<p>
<span
className="text-blue-600 cursor-pointer border-b border-blue-600"
onClick={() => window.open(downloadUrl, '_blank')}
>
Download Git for {osName}
</span>
</p>
</div>
</Modal>
</Portal>
);
};
export default GitNotFoundModal;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { IconChevronDown, IconChevronRight } from '@tabler/icons';
const CollapsibleDiffRow = ({ title, isCollapsed, onToggle, oldContent, newContent, hasOldContent, hasNewContent }) => {
if (!hasOldContent && !hasNewContent) {
return null;
}
return (
<div className="diff-row">
<div className="diff-row-header" onClick={onToggle}>
<span className="collapse-toggle">
{isCollapsed ? (
<IconChevronRight size={14} strokeWidth={2} />
) : (
<IconChevronDown size={14} strokeWidth={2} />
)}
</span>
<span className="diff-row-title">{title}</span>
</div>
{!isCollapsed && (
<div className="diff-row-content">
<div className="diff-row-pane old">
{hasOldContent ? oldContent : <div className="empty-placeholder" />}
</div>
<div className="diff-row-pane new">
{hasNewContent ? newContent : <div className="empty-placeholder" />}
</div>
</div>
)}
</div>
);
};
export default CollapsibleDiffRow;

View File

@@ -0,0 +1,199 @@
import React, { useMemo } from 'react';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
const AUTH_TYPE_LABELS = {
awsv4: 'AWS Signature v4',
basic: 'Basic Auth',
bearer: 'Bearer Token',
digest: 'Digest Auth',
ntlm: 'NTLM',
oauth2: 'OAuth 2.0',
wsse: 'WSSE',
apikey: 'API Key'
};
const AUTH_FIELD_LABELS = {
// AWS v4
accessKeyId: 'Access Key ID',
secretAccessKey: 'Secret Access Key',
sessionToken: 'Session Token',
service: 'Service',
region: 'Region',
profileName: 'Profile Name',
// Basic/Digest/NTLM/WSSE
username: 'Username',
password: 'Password',
domain: 'Domain',
// Bearer
token: 'Token',
// API Key
key: 'Key',
value: 'Value',
placement: 'Placement',
// OAuth2
grantType: 'Grant Type',
callbackUrl: 'Callback URL',
authorizationUrl: 'Authorization URL',
accessTokenUrl: 'Access Token URL',
refreshTokenUrl: 'Refresh Token URL',
clientId: 'Client ID',
clientSecret: 'Client Secret',
scope: 'Scope',
state: 'State',
pkce: 'PKCE',
credentialsPlacement: 'Credentials Placement',
credentialsId: 'Credentials ID',
tokenPlacement: 'Token Placement',
tokenHeaderPrefix: 'Token Header Prefix',
tokenQueryKey: 'Token Query Key',
autoFetchToken: 'Auto Fetch Token',
autoRefreshToken: 'Auto Refresh Token'
};
const VisualDiffAuth = ({ oldData, newData, showSide }) => {
const oldAuth = get(oldData, 'request.auth', {});
const newAuth = get(newData, 'request.auth', {});
const currentAuth = showSide === 'old' ? oldAuth : newAuth;
const otherAuth = showSide === 'old' ? newAuth : oldAuth;
const authTypes = useMemo(() => {
const types = new Set([...Object.keys(currentAuth), ...Object.keys(otherAuth)]);
types.delete('mode');
return Array.from(types);
}, [currentAuth, otherAuth]);
const authSections = useMemo(() => {
return authTypes.map((authType) => {
const rawCurrentConfig = currentAuth[authType];
const rawOtherConfig = otherAuth[authType];
const currentConfig = (typeof rawCurrentConfig === 'object' && rawCurrentConfig !== null) ? rawCurrentConfig : {};
const otherConfig = (typeof rawOtherConfig === 'object' && rawOtherConfig !== null) ? rawOtherConfig : {};
if (Object.keys(currentConfig).length === 0 && showSide === 'old') {
return null;
}
if (Object.keys(currentConfig).length === 0 && showSide === 'new') {
return null;
}
let sectionStatus = 'unchanged';
if (Object.keys(otherConfig).length === 0) {
sectionStatus = showSide === 'old' ? 'deleted' : 'added';
} else if (!isEqual(currentConfig, otherConfig)) {
sectionStatus = 'modified';
}
const allFields = new Set([...Object.keys(currentConfig), ...Object.keys(otherConfig)]);
const fields = Array.from(allFields).map((field) => {
const currentValue = currentConfig[field];
const otherValue = otherConfig[field];
let status = 'unchanged';
if (otherValue === undefined) {
status = showSide === 'old' ? 'deleted' : 'added';
} else if (currentValue !== otherValue) {
status = 'modified';
}
let displayValue = currentValue;
if (typeof displayValue === 'boolean') {
displayValue = displayValue ? 'true' : 'false';
} else if (displayValue === undefined || displayValue === null) {
displayValue = '';
}
return {
key: AUTH_FIELD_LABELS[field] || field,
value: String(displayValue),
status
};
});
return {
type: authType,
label: AUTH_TYPE_LABELS[authType] || authType,
status: sectionStatus,
fields
};
}).filter(Boolean);
}, [authTypes, currentAuth, otherAuth, showSide]);
const currentMode = currentAuth.mode;
const otherMode = otherAuth.mode;
const modeStatus = currentMode !== otherMode ? (otherMode === undefined ? (showSide === 'old' ? 'deleted' : 'added') : 'modified') : 'unchanged';
if (authSections.length === 0 && !currentMode) {
return null;
}
return (
<>
{currentMode && (
<div className="diff-section">
<table className="diff-table">
<thead>
<tr>
<th style={{ width: '30px' }}></th>
<th style={{ width: '40%' }}>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr className={modeStatus}>
<td>
{modeStatus !== 'unchanged' && (
<span className={`status-badge ${modeStatus}`}>
{modeStatus === 'added' ? 'A' : modeStatus === 'deleted' ? 'D' : 'M'}
</span>
)}
</td>
<td className="key-cell">Auth Mode</td>
<td className="value-cell">{AUTH_TYPE_LABELS[currentMode] || currentMode}</td>
</tr>
</tbody>
</table>
</div>
)}
{authSections.map((section) => (
<div key={section.type} className="diff-section">
<div className="diff-section-header">
<span>{section.label}</span>
{section.status !== 'unchanged' && (
<span className={`status-badge ${section.status}`}>
{section.status === 'added' ? 'A' : section.status === 'deleted' ? 'D' : 'M'}
</span>
)}
</div>
<table className="diff-table">
<thead>
<tr>
<th style={{ width: '30px' }}></th>
<th style={{ width: '40%' }}>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{section.fields.map((field, index) => (
<tr key={index} className={field.status}>
<td>
{field.status !== 'unchanged' && (
<span className={`status-badge ${field.status}`}>
{field.status === 'added' ? 'A' : field.status === 'deleted' ? 'D' : 'M'}
</span>
)}
</td>
<td className="key-cell">{field.key}</td>
<td className="value-cell">{field.value}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</>
);
};
export default VisualDiffAuth;

View File

@@ -0,0 +1,353 @@
import React, { useMemo } from 'react';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import { computeLineDiffForOld, computeLineDiffForNew } from './utils/diffUtils';
const BODY_TYPE_LABELS = {
json: 'JSON',
text: 'Text',
xml: 'XML',
sparql: 'SPARQL',
graphql: 'GraphQL',
formUrlEncoded: 'Form URL Encoded',
multipartForm: 'Multipart Form',
file: 'File',
grpc: 'gRPC',
ws: 'WebSocket'
};
const TEXT_BODY_TYPES = ['json', 'text', 'xml', 'sparql'];
const FORM_BODY_TYPES = ['formUrlEncoded', 'multipartForm'];
const ALL_BODY_TYPES = Object.keys(BODY_TYPE_LABELS);
const VisualDiffBody = ({ oldData, newData, showSide }) => {
const oldBody = get(oldData, 'request.body', {});
const newBody = get(newData, 'request.body', {});
const currentBody = showSide === 'old' ? oldBody : newBody;
const otherBody = showSide === 'old' ? newBody : oldBody;
const bodyTypes = useMemo(() => {
const currentMode = currentBody.mode;
const otherMode = otherBody.mode;
// Collect body types that match either side's active mode
const relevantTypes = new Set();
if (currentMode && currentMode !== 'none') {
relevantTypes.add(currentMode);
}
if (otherMode && otherMode !== 'none') {
relevantTypes.add(otherMode);
}
// If neither side has a mode (legacy data), fall back to showing all defined types
if (relevantTypes.size === 0) {
return ALL_BODY_TYPES.filter((type) => {
const currentVal = currentBody[type];
const otherVal = otherBody[type];
return currentVal !== undefined || otherVal !== undefined;
});
}
// Only show body types that match the active mode on either side
return ALL_BODY_TYPES.filter((type) => {
if (!relevantTypes.has(type)) return false;
const currentVal = currentBody[type];
const otherVal = otherBody[type];
return currentVal !== undefined || otherVal !== undefined;
});
}, [currentBody, otherBody]);
const renderLineDiff = (segments) => {
return segments.map((segment, index) => (
<div key={index} className={`diff-line ${segment.status}`}>
{segment.text || '\u00A0'}
</div>
));
};
const renderFormData = (items, otherItems) => {
if (!items || items.length === 0) return null;
const otherItemMap = new Map();
(otherItems || []).forEach((item) => {
otherItemMap.set(item.name, item);
});
return (
<table className="diff-table">
<thead>
<tr>
<th style={{ width: '30px' }}></th>
<th className="checkbox-cell"></th>
<th style={{ width: '40%' }}>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
const otherItem = otherItemMap.get(item.name);
let status = 'unchanged';
if (!otherItem) {
status = showSide === 'old' ? 'deleted' : 'added';
} else if (item.value !== otherItem.value || item.enabled !== otherItem.enabled) {
status = 'modified';
}
return (
<tr key={`${item.name}-${index}`} className={status}>
<td>
{status !== 'unchanged' && (
<span className={`status-badge ${status}`}>
{status === 'added' ? 'A' : status === 'deleted' ? 'D' : 'M'}
</span>
)}
</td>
<td className="checkbox-cell">
<input
type="checkbox"
checked={item.enabled !== false}
readOnly
disabled
/>
</td>
<td className="key-cell">{item.name}</td>
<td className="value-cell">{item.value}</td>
</tr>
);
})}
</tbody>
</table>
);
};
const renderFileBody = (files, otherFiles) => {
if (!files || files.length === 0) return null;
const otherFileMap = new Map();
(otherFiles || []).forEach((f, idx) => {
otherFileMap.set(f.filePath || idx, f);
});
return (
<table className="diff-table">
<thead>
<tr>
<th style={{ width: '30px' }}></th>
<th className="checkbox-cell"></th>
<th>File Path</th>
<th style={{ width: '100px' }}>Content Type</th>
</tr>
</thead>
<tbody>
{files.map((file, index) => {
const otherFile = otherFileMap.get(file.filePath || index);
let status = 'unchanged';
if (!otherFile) {
status = showSide === 'old' ? 'deleted' : 'added';
} else if (file.filePath !== otherFile.filePath || file.contentType !== otherFile.contentType) {
status = 'modified';
}
return (
<tr key={index} className={status}>
<td>
{status !== 'unchanged' && (
<span className={`status-badge ${status}`}>
{status === 'added' ? 'A' : status === 'deleted' ? 'D' : 'M'}
</span>
)}
</td>
<td className="checkbox-cell">
<input type="checkbox" checked={file.selected !== false} readOnly disabled />
</td>
<td className="value-cell">{file.filePath}</td>
<td className="value-cell">{file.contentType || '-'}</td>
</tr>
);
})}
</tbody>
</table>
);
};
const renderMessageBody = (messages, otherMessages, typeLabel) => {
if (!messages || messages.length === 0) return null;
return messages.map((msg, index) => {
const otherMsg = (otherMessages || [])[index];
const contentDiff = showSide === 'old'
? computeLineDiffForOld(msg.content || '', otherMsg?.content || '')
: computeLineDiffForNew(otherMsg?.content || '', msg.content || '');
let msgStatus = 'unchanged';
if (!otherMsg) {
msgStatus = showSide === 'old' ? 'deleted' : 'added';
} else if (msg.name !== otherMsg.name || msg.type !== otherMsg.type) {
msgStatus = 'modified';
}
return (
<div key={index}>
<div className="diff-section-header">
<span>{typeLabel}: {msg.name || `Message ${index + 1}`}{msg.type ? ` (${msg.type})` : ''}</span>
{msgStatus !== 'unchanged' && (
<span className={`status-badge ${msgStatus}`}>
{msgStatus === 'added' ? 'A' : msgStatus === 'deleted' ? 'D' : 'M'}
</span>
)}
</div>
<div className="code-diff-content">{renderLineDiff(contentDiff)}</div>
</div>
);
});
};
const renderGraphqlBody = (graphql, otherGraphql) => {
const currentQuery = graphql?.query || '';
const otherQuery = otherGraphql?.query || '';
const currentVariables = graphql?.variables || '';
const otherVariables = otherGraphql?.variables || '';
const queryDiff = showSide === 'old'
? computeLineDiffForOld(currentQuery, otherQuery)
: computeLineDiffForNew(otherQuery, currentQuery);
const variablesDiff = showSide === 'old'
? computeLineDiffForOld(currentVariables, otherVariables)
: computeLineDiffForNew(otherVariables, currentVariables);
return (
<>
{(currentQuery || otherQuery) && (
<div>
<div className="diff-section-header">Query</div>
<div className="code-diff-content">{renderLineDiff(queryDiff)}</div>
</div>
)}
{(currentVariables || otherVariables) && (
<div>
<div className="diff-section-header">Variables</div>
<div className="code-diff-content">{renderLineDiff(variablesDiff)}</div>
</div>
)}
</>
);
};
const renderTextBody = (currentContent, otherContent) => {
const diffSegments = showSide === 'old'
? computeLineDiffForOld(currentContent || '', otherContent || '')
: computeLineDiffForNew(otherContent || '', currentContent || '');
return (
<div className="code-diff-content">
{renderLineDiff(diffSegments)}
</div>
);
};
const renderBodyType = (type) => {
const currentVal = currentBody[type];
const otherVal = otherBody[type];
if (currentVal === undefined && otherVal === undefined) return null;
// For text-based body types
if (TEXT_BODY_TYPES.includes(type)) {
if (!currentVal) return null;
return renderTextBody(currentVal, otherVal);
}
// For form data types
if (FORM_BODY_TYPES.includes(type)) {
return renderFormData(currentVal, otherVal);
}
// GraphQL
if (type === 'graphql') {
return renderGraphqlBody(currentVal, otherVal);
}
// File
if (type === 'file') {
return renderFileBody(currentVal, otherVal);
}
// gRPC
if (type === 'grpc') {
return renderMessageBody(currentVal, otherVal, 'gRPC');
}
// WebSocket
if (type === 'ws') {
return renderMessageBody(currentVal, otherVal, 'WebSocket');
}
return null;
};
// Show body mode if present
const currentMode = currentBody.mode;
const otherMode = otherBody.mode;
const modeStatus = currentMode !== otherMode ? (otherMode === undefined ? (showSide === 'old' ? 'deleted' : 'added') : 'modified') : 'unchanged';
if (bodyTypes.length === 0 && !currentMode) {
return null;
}
return (
<>
{currentMode && (
<div className="diff-section">
<table className="diff-table">
<thead>
<tr>
<th style={{ width: '30px' }}></th>
<th style={{ width: '40%' }}>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr className={modeStatus}>
<td>
{modeStatus !== 'unchanged' && (
<span className={`status-badge ${modeStatus}`}>
{modeStatus === 'added' ? 'A' : modeStatus === 'deleted' ? 'D' : 'M'}
</span>
)}
</td>
<td className="key-cell">Body Mode</td>
<td className="value-cell">{BODY_TYPE_LABELS[currentMode] || currentMode}</td>
</tr>
</tbody>
</table>
</div>
)}
{bodyTypes.map((type) => {
const content = renderBodyType(type);
if (!content) return null;
const currentVal = currentBody[type];
const otherVal = otherBody[type];
const hasChanges = !isEqual(currentVal, otherVal);
return (
<div key={type} className="diff-section">
<div className="diff-section-header">
<span>{BODY_TYPE_LABELS[type] || type}</span>
{hasChanges && (
<span className={`status-badge ${otherVal === undefined ? (showSide === 'old' ? 'deleted' : 'added') : 'modified'}`}>
{otherVal === undefined ? (showSide === 'old' ? 'D' : 'A') : 'M'}
</span>
)}
</div>
{content}
</div>
);
})}
</>
);
};
export default VisualDiffBody;

View File

@@ -0,0 +1,443 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
.visual-diff-content {
flex: 1;
overflow: auto;
}
.diff-header-row {
display: flex;
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: ${(props) => props.theme.border.radius.base};
margin-bottom: 1rem;
}
.diff-header-pane {
flex: 1;
padding: 0.5rem 0.75rem;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 600;
color: ${(props) => props.theme.colors.text.muted};
text-transform: uppercase;
&.old {
border-right: 1px solid ${(props) => props.theme.border.border1};
}
}
.diff-sections {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.diff-row {
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: ${(props) => props.theme.border.radius.base};
overflow: hidden;
}
.diff-row-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: ${(props) => props.theme.sidebar.bg};
cursor: pointer;
user-select: none;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
&:hover {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
}
}
.collapse-toggle {
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => props.theme.colors.text.muted};
}
.diff-row-title {
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
color: ${(props) => props.theme.text};
}
.diff-row-content {
display: flex;
gap: 1rem;
padding: 0.75rem;
background: ${(props) => props.theme.background.base};
}
.diff-row-pane {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
&.old {
border-left: 2px solid ${(props) => props.theme.colors.text.danger}20;
padding-left: 0.5rem;
}
&.new {
border-left: 2px solid ${(props) => props.theme.colors.text.green}20;
padding-left: 0.5rem;
}
}
.empty-placeholder {
flex: 1;
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: ${(props) => props.theme.sidebar.bg};
border: 1px dashed ${(props) => props.theme.border.border1};
border-radius: ${(props) => props.theme.border.radius.sm};
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.xs};
}
.empty-placeholder::after {
content: 'No content';
}
.diff-section {
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: ${(props) => props.theme.border.radius.sm};
overflow: hidden;
&.added {
border-color: ${(props) => props.theme.colors.text.green};
}
&.deleted {
border-color: ${(props) => props.theme.colors.text.danger};
}
}
.diff-section-header {
padding: 0.375rem 0.5rem;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 500;
background: ${(props) => props.theme.sidebar.bg};
border-bottom: 1px solid ${(props) => props.theme.border.border1};
display: flex;
align-items: center;
justify-content: space-between;
}
.diff-section-content {
padding: 0.5rem;
}
.url-bar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.5rem;
.method {
font-weight: 600;
font-size: ${(props) => props.theme.font.size.xs};
text-transform: uppercase;
padding: 0.125rem 0.375rem;
border-radius: ${(props) => props.theme.border.radius.sm};
background: ${(props) => props.theme.brand}15;
color: ${(props) => props.theme.brand};
}
.url {
flex: 1;
font-family: 'Fira Code', monospace;
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.text};
word-break: break-all;
&.changed {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 20%, transparent);
padding: 0.125rem 0.25rem;
border-radius: ${(props) => props.theme.border.radius.sm};
}
}
.method.changed {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 30%, transparent);
color: ${(props) => props.theme.colors.text.warning};
}
}
.diff-inline {
padding: 0.125rem 0.25rem;
border-radius: 2px;
&.added {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 25%, transparent);
color: ${(props) => props.theme.colors.text.green};
}
&.deleted {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 25%, transparent);
color: ${(props) => props.theme.colors.text.danger};
text-decoration: line-through;
}
&.modified {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 25%, transparent);
color: ${(props) => props.theme.colors.text.warning};
}
}
.diff-table {
width: 100%;
border-collapse: collapse;
font-size: ${(props) => props.theme.font.size.xs};
th, td {
padding: 0.375rem 0.5rem;
text-align: left;
border-bottom: 1px solid ${(props) => props.theme.border.border1};
}
th {
font-weight: 500;
color: ${(props) => props.theme.colors.text.muted};
background: ${(props) => props.theme.sidebar.bg};
}
tr.added {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 10%, transparent);
}
tr.deleted {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 10%, transparent);
}
tr.modified {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 10%, transparent);
}
.checkbox-cell {
width: 24px;
text-align: center;
input[type='checkbox'] {
cursor: default;
width: 12px;
height: 12px;
accent-color: ${(props) => props.theme.colors.accent};
vertical-align: middle;
margin: 0;
}
}
.key-cell {
font-family: 'Fira Code', monospace;
color: ${(props) => props.theme.text};
}
.value-cell {
font-family: 'Fira Code', monospace;
color: ${(props) => props.theme.colors.text.muted};
word-break: break-all;
}
.status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 0.875rem;
height: 0.875rem;
border-radius: 2px;
font-size: 8px;
font-weight: 600;
&.added {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 13%, transparent);
color: ${(props) => props.theme.colors.text.green};
}
&.deleted {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 13%, transparent);
color: ${(props) => props.theme.colors.text.danger};
}
&.modified {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 13%, transparent);
color: ${(props) => props.theme.colors.text.warning};
}
}
}
.code-diff-content {
max-height: 250px;
overflow: auto;
font-family: 'Fira Code', monospace;
font-size: ${(props) => props.theme.font.size.xs};
line-height: 1.5;
.diff-line {
padding: 0 0.5rem;
white-space: pre-wrap;
word-break: break-word;
&.unchanged {
color: ${(props) => props.theme.text};
}
&.added {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 15%, transparent);
color: ${(props) => props.theme.colors.text.green};
}
&.deleted {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 15%, transparent);
color: ${(props) => props.theme.colors.text.danger};
text-decoration: line-through;
}
}
}
.example-content {
padding: 0.5rem;
}
.example-block {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
.example-block-header {
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 600;
color: ${(props) => props.theme.colors.text.muted};
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.25rem 0.5rem;
background: ${(props) => props.theme.sidebar.bg};
border-radius: ${(props) => props.theme.border.radius.sm};
margin-bottom: 0.375rem;
}
.example-subsection {
margin-bottom: 0.375rem;
&:last-child {
margin-bottom: 0;
}
}
.example-subsection-title {
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 500;
color: ${(props) => props.theme.colors.text.muted};
padding: 0.25rem 0.5rem;
margin-bottom: 0.25rem;
}
.example-description {
font-weight: 400;
color: ${(props) => props.theme.colors.text.muted};
font-style: italic;
}
.status-display {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.5rem;
font-family: 'Fira Code', monospace;
font-size: ${(props) => props.theme.font.size.xs};
.status-code {
font-weight: 600;
padding: 0.125rem 0.375rem;
border-radius: ${(props) => props.theme.border.radius.sm};
background: ${(props) => props.theme.sidebar.bg};
&.changed {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 20%, transparent);
color: ${(props) => props.theme.colors.text.warning};
}
}
.status-text {
color: ${(props) => props.theme.colors.text.muted};
&.changed {
color: ${(props) => props.theme.colors.text.warning};
}
}
}
.example-subsection .diff-table {
margin: 0;
}
.example-subsection .code-diff-content {
max-height: 150px;
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: ${(props) => props.theme.border.radius.sm};
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.sm};
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.tag-badge {
display: inline-block;
padding: 0.125rem 0.375rem;
font-size: ${(props) => props.theme.font.size.xs};
font-family: 'Fira Code', monospace;
border-radius: ${(props) => props.theme.border.radius.sm};
background: ${(props) => props.theme.sidebar.bg};
border: 1px solid ${(props) => props.theme.border.border1};
&.added {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 15%, transparent);
border-color: ${(props) => props.theme.colors.text.green};
color: ${(props) => props.theme.colors.text.green};
}
&.deleted {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 15%, transparent);
border-color: ${(props) => props.theme.colors.text.danger};
color: ${(props) => props.theme.colors.text.danger};
text-decoration: line-through;
}
&.modified {
background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 15%, transparent);
border-color: ${(props) => props.theme.colors.text.warning};
color: ${(props) => props.theme.colors.text.warning};
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,109 @@
import React, { useState, useEffect } from 'react';
import CollapsibleDiffRow from '../CollapsibleDiffRow';
import StyledWrapper from './StyledWrapper';
/**
* VisualDiffContent - Presentational component for rendering visual diffs
*
* This is a reusable component that renders the visual diff UI.
* It can be used by:
* - Git VisualDiffViewer (for git diffs)
* - OpenAPI ChangeSection (for spec diffs)
*
* Props:
* - oldData: The "before" data
* - newData: The "after" data
* - sections: Array of section configs { key, title, Component, hasContent }
* - sectionHasChanges: Function (sectionKey, oldData, newData) => boolean
* - oldLabel: Label for the left/old pane (default: "Before")
* - newLabel: Label for the right/new pane (default: "After")
* - hideUnchanged: Hide sections without changes entirely (default: false)
*/
const VisualDiffContent = ({
oldData,
newData,
sections,
sectionHasChanges,
oldLabel = 'Before',
newLabel = 'After',
hideUnchanged = false
}) => {
const [collapsedSections, setCollapsedSections] = useState({});
const toggleSection = (sectionKey) => {
setCollapsedSections((prev) => ({
...prev,
[sectionKey]: !prev[sectionKey]
}));
};
// Auto-collapse unchanged sections (collapsed but still visible)
useEffect(() => {
if (!sectionHasChanges || (!oldData && !newData)) return;
const initialCollapsed = {};
sections.forEach(({ key }) => {
const hasChanges = sectionHasChanges(key, oldData, newData);
initialCollapsed[key] = !hasChanges;
});
setCollapsedSections(initialCollapsed);
}, [oldData, newData, sections, sectionHasChanges]);
if (!oldData && !newData) {
return (
<StyledWrapper>
<div className="empty-state">
No content to display
</div>
</StyledWrapper>
);
}
return (
<StyledWrapper>
<div className="visual-diff-content">
<div className="diff-header-row">
<div className="diff-header-pane old">{oldLabel}</div>
<div className="diff-header-pane new">{newLabel}</div>
</div>
<div className="diff-sections">
{sections.map(({ key, title, Component, hasContent: checkContent }) => {
const hasOld = oldData && checkContent(oldData);
const hasNew = newData && checkContent(newData);
if (!hasOld && !hasNew) {
return null;
}
// Hide sections without changes entirely when hideUnchanged is enabled
if (hideUnchanged && sectionHasChanges && !sectionHasChanges(key, oldData, newData)) {
return null;
}
return (
<CollapsibleDiffRow
key={key}
title={title}
isCollapsed={collapsedSections[key] || false}
onToggle={() => toggleSection(key)}
hasOldContent={hasOld}
hasNewContent={hasNew}
oldContent={
<Component oldData={oldData} newData={newData} showSide="old" />
}
newContent={
<Component oldData={oldData} newData={newData} showSide="new" />
}
/>
);
})}
</div>
</div>
</StyledWrapper>
);
};
export default VisualDiffContent;

View File

@@ -0,0 +1,74 @@
import React, { useMemo } from 'react';
import get from 'lodash/get';
const VisualDiffHeaders = ({ oldData, newData, showSide }) => {
const oldHeaders = get(oldData, 'request.headers', []);
const newHeaders = get(newData, 'request.headers', []);
const currentHeaders = showSide === 'old' ? oldHeaders : newHeaders;
const otherHeaders = showSide === 'old' ? newHeaders : oldHeaders;
const headersWithStatus = useMemo(() => {
const otherHeaderMap = new Map();
otherHeaders.forEach((h) => {
otherHeaderMap.set(h.name, h);
});
return currentHeaders.map((header) => {
const otherHeader = otherHeaderMap.get(header.name);
let status = 'unchanged';
if (!otherHeader) {
status = showSide === 'old' ? 'deleted' : 'added';
} else if (header.value !== otherHeader.value || header.enabled !== otherHeader.enabled) {
status = 'modified';
}
return { ...header, status };
});
}, [currentHeaders, otherHeaders, showSide]);
if (headersWithStatus.length === 0) {
return null;
}
return (
<div className="diff-section">
<table className="diff-table">
<thead>
<tr>
<th style={{ width: '30px' }}></th>
<th className="checkbox-cell"></th>
<th style={{ width: '40%' }}>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{headersWithStatus.map((header, index) => (
<tr key={`${header.name}-${index}`} className={header.status}>
<td>
{header.status !== 'unchanged' && (
<span className={`status-badge ${header.status}`}>
{header.status === 'added' ? 'A' : header.status === 'deleted' ? 'D' : 'M'}
</span>
)}
</td>
<td className="checkbox-cell">
<input
type="checkbox"
checked={header.enabled !== false}
readOnly
disabled
/>
</td>
<td className="key-cell">{header.name}</td>
<td className="value-cell">{header.value}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default VisualDiffHeaders;

View File

@@ -0,0 +1,89 @@
import React, { useMemo } from 'react';
import get from 'lodash/get';
const VisualDiffParams = ({ oldData, newData, showSide }) => {
const oldParams = get(oldData, 'request.params', []);
const newParams = get(newData, 'request.params', []);
const currentParams = showSide === 'old' ? oldParams : newParams;
const otherParams = showSide === 'old' ? newParams : oldParams;
const paramsWithStatus = useMemo(() => {
const otherParamMap = new Map();
otherParams.forEach((p) => {
otherParamMap.set(p.name, p);
});
return currentParams.map((param) => {
const otherParam = otherParamMap.get(param.name);
let status = 'unchanged';
if (!otherParam) {
status = showSide === 'old' ? 'deleted' : 'added';
} else if (param.value !== otherParam.value || param.enabled !== otherParam.enabled) {
status = 'modified';
}
return { ...param, status };
});
}, [currentParams, otherParams, showSide]);
const queryParams = paramsWithStatus.filter((p) => p.type === 'query');
const pathParams = paramsWithStatus.filter((p) => p.type === 'path');
if (queryParams.length === 0 && pathParams.length === 0) {
return null;
}
const renderTable = (params, title) => {
if (params.length === 0) return null;
return (
<div className="diff-section">
<div className="diff-section-header">{title}</div>
<table className="diff-table">
<thead>
<tr>
<th style={{ width: '30px' }}></th>
<th className="checkbox-cell"></th>
<th style={{ width: '40%' }}>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{params.map((param, index) => (
<tr key={`${param.name}-${index}`} className={param.status}>
<td>
{param.status !== 'unchanged' && (
<span className={`status-badge ${param.status}`}>
{param.status === 'added' ? 'A' : param.status === 'deleted' ? 'D' : 'M'}
</span>
)}
</td>
<td className="checkbox-cell">
<input
type="checkbox"
checked={param.enabled !== false}
readOnly
disabled
/>
</td>
<td className="key-cell">{param.name}</td>
<td className="value-cell">{param.value}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
return (
<>
{renderTable(queryParams, 'Query Parameters')}
{renderTable(pathParams, 'Path Parameters')}
</>
);
};
export default VisualDiffParams;

View File

@@ -0,0 +1,55 @@
import React, { useMemo } from 'react';
import { computeWordDiffForOld, computeWordDiffForNew } from './utils/diffUtils';
import { getMethod, getUrl } from './utils/bruUtils';
const VisualDiffUrlBar = ({ oldData, newData, showSide }) => {
const oldMethod = getMethod(oldData);
const newMethod = getMethod(newData);
const oldUrl = getUrl(oldData);
const newUrl = getUrl(newData);
const currentMethod = showSide === 'old' ? oldMethod : newMethod;
const urlDiffSegments = useMemo(() => {
if (showSide === 'old') {
return computeWordDiffForOld(oldUrl, newUrl);
} else {
return computeWordDiffForNew(oldUrl, newUrl);
}
}, [oldUrl, newUrl, showSide]);
const methodChanged = oldMethod !== newMethod;
const methodStatus = useMemo(() => {
if (!methodChanged) return 'unchanged';
if (showSide === 'old') return 'deleted';
return 'added';
}, [methodChanged, showSide]);
const renderDiffSegments = (segments) => {
return segments.map((segment, index) => {
if (segment.status === 'unchanged') {
return <span key={index}>{segment.text}</span>;
}
return (
<span key={index} className={`diff-inline ${segment.status}`}>
{segment.text}
</span>
);
});
};
return (
<div className="diff-section">
<div className="url-bar">
<span className={`method ${methodStatus !== 'unchanged' ? `diff-inline ${methodStatus}` : ''}`}>
{currentMethod?.toUpperCase() || 'GET'}
</span>
<span className="url">
{renderDiffSegments(urlDiffSegments)}
</span>
</div>
</div>
);
};
export default VisualDiffUrlBar;

View File

@@ -0,0 +1,53 @@
import get from 'lodash/get';
export const DIFF_STATUS = Object.freeze({
ADDED: 'added',
DELETED: 'deleted',
MODIFIED: 'modified',
UNCHANGED: 'unchanged'
});
export const getBodyContent = (body) => {
if (!body) return '';
if (body.json) return body.json;
if (body.text) return body.text;
if (body.xml) return body.xml;
if (body.sparql) return body.sparql;
if (body.graphql?.query) return body.graphql.query;
if (body.content) return body.content;
return '';
};
export const getBodyMode = (body) => {
if (!body) return 'none';
if (body.json !== undefined) return 'json';
if (body.text !== undefined) return 'text';
if (body.xml !== undefined) return 'xml';
if (body.sparql !== undefined) return 'sparql';
if (body.graphql) return 'graphql';
if (body.formUrlEncoded) return 'formUrlEncoded';
if (body.multipartForm) return 'multipartForm';
if (body.file) return 'file';
if (body.grpc) return 'grpc';
if (body.ws) return 'ws';
if (body.mode === 'none') return 'none';
return 'none';
};
export const getMethod = (data) => {
return get(data, 'request.method', 'GET');
};
export const getUrl = (data) => {
return get(data, 'request.url', '');
};
export const computeItemDiffStatus = (currentItem, otherItem, showSide) => {
if (!otherItem) {
return showSide === 'old' ? DIFF_STATUS.DELETED : DIFF_STATUS.ADDED;
}
if (currentItem.value !== otherItem.value || currentItem.enabled !== otherItem.enabled) {
return DIFF_STATUS.MODIFIED;
}
return DIFF_STATUS.UNCHANGED;
};

View File

@@ -0,0 +1,194 @@
const { describe, it, expect } = require('@jest/globals');
import {
getBodyContent,
getBodyMode,
getMethod,
getUrl,
computeItemDiffStatus
} from './bruUtils';
describe('bruUtils', () => {
describe('getBodyContent', () => {
it('should return empty string for null or undefined body', () => {
expect(getBodyContent(null)).toBe('');
expect(getBodyContent(undefined)).toBe('');
});
it('should return empty string for empty body', () => {
expect(getBodyContent({})).toBe('');
});
it('should return json content', () => {
expect(getBodyContent({ json: '{"key": "value"}' })).toBe('{"key": "value"}');
});
it('should return text content', () => {
expect(getBodyContent({ text: 'plain text content' })).toBe('plain text content');
});
it('should return xml content', () => {
expect(getBodyContent({ xml: '<root><item>value</item></root>' })).toBe('<root><item>value</item></root>');
});
it('should return sparql content', () => {
expect(getBodyContent({ sparql: 'SELECT * WHERE { ?s ?p ?o }' })).toBe('SELECT * WHERE { ?s ?p ?o }');
});
it('should return graphql query content', () => {
expect(getBodyContent({ graphql: { query: 'query { users { id } }' } })).toBe('query { users { id } }');
});
it('should return generic content', () => {
expect(getBodyContent({ content: 'generic content' })).toBe('generic content');
});
it('should return empty string for graphql without query', () => {
expect(getBodyContent({ graphql: {} })).toBe('');
expect(getBodyContent({ graphql: { variables: '{}' } })).toBe('');
});
it('should prioritize json over other types', () => {
expect(getBodyContent({ json: '{"a":1}', text: 'text' })).toBe('{"a":1}');
});
});
describe('getBodyMode', () => {
it('should return none for null or undefined body', () => {
expect(getBodyMode(null)).toBe('none');
expect(getBodyMode(undefined)).toBe('none');
});
it('should return none for empty body', () => {
expect(getBodyMode({})).toBe('none');
});
it('should return json mode', () => {
expect(getBodyMode({ json: '{}' })).toBe('json');
expect(getBodyMode({ json: '' })).toBe('json');
});
it('should return text mode', () => {
expect(getBodyMode({ text: 'content' })).toBe('text');
expect(getBodyMode({ text: '' })).toBe('text');
});
it('should return xml mode', () => {
expect(getBodyMode({ xml: '<root/>' })).toBe('xml');
});
it('should return sparql mode', () => {
expect(getBodyMode({ sparql: 'SELECT *' })).toBe('sparql');
});
it('should return graphql mode', () => {
expect(getBodyMode({ graphql: { query: '' } })).toBe('graphql');
});
it('should return formUrlEncoded mode', () => {
expect(getBodyMode({ formUrlEncoded: [] })).toBe('formUrlEncoded');
expect(getBodyMode({ formUrlEncoded: [{ name: 'key', value: 'val' }] })).toBe('formUrlEncoded');
});
it('should return multipartForm mode', () => {
expect(getBodyMode({ multipartForm: [] })).toBe('multipartForm');
});
it('should return file mode', () => {
expect(getBodyMode({ file: [] })).toBe('file');
});
it('should return grpc mode', () => {
expect(getBodyMode({ grpc: [] })).toBe('grpc');
});
it('should return ws mode', () => {
expect(getBodyMode({ ws: [] })).toBe('ws');
});
it('should return none for explicit none mode', () => {
expect(getBodyMode({ mode: 'none' })).toBe('none');
});
it('should prioritize json over other modes', () => {
expect(getBodyMode({ json: '{}', text: 'text' })).toBe('json');
});
});
describe('getMethod', () => {
it('should return GET as default', () => {
expect(getMethod(null)).toBe('GET');
expect(getMethod(undefined)).toBe('GET');
expect(getMethod({})).toBe('GET');
});
it('should return request method', () => {
expect(getMethod({ request: { method: 'POST' } })).toBe('POST');
expect(getMethod({ request: { method: 'PUT' } })).toBe('PUT');
expect(getMethod({ request: { method: 'DELETE' } })).toBe('DELETE');
});
it('should return GET when request exists but method is missing', () => {
expect(getMethod({ request: {} })).toBe('GET');
});
});
describe('getUrl', () => {
it('should return empty string as default', () => {
expect(getUrl(null)).toBe('');
expect(getUrl(undefined)).toBe('');
expect(getUrl({})).toBe('');
});
it('should return request url', () => {
expect(getUrl({ request: { url: 'https://api.example.com/users' } })).toBe('https://api.example.com/users');
});
it('should return empty string when request exists but url is missing', () => {
expect(getUrl({ request: {} })).toBe('');
});
it('should return url with different protocols', () => {
expect(getUrl({ request: { url: 'http://localhost:3000' } })).toBe('http://localhost:3000');
expect(getUrl({ request: { url: 'ws://localhost:8080' } })).toBe('ws://localhost:8080');
expect(getUrl({ request: { url: 'grpc://localhost:50051' } })).toBe('grpc://localhost:50051');
});
});
describe('computeItemDiffStatus', () => {
it('should return deleted when other item is missing and showing old side', () => {
expect(computeItemDiffStatus({ name: 'key', value: 'val' }, null, 'old')).toBe('deleted');
expect(computeItemDiffStatus({ name: 'key', value: 'val' }, undefined, 'old')).toBe('deleted');
});
it('should return added when other item is missing and showing new side', () => {
expect(computeItemDiffStatus({ name: 'key', value: 'val' }, null, 'new')).toBe('added');
expect(computeItemDiffStatus({ name: 'key', value: 'val' }, undefined, 'new')).toBe('added');
});
it('should return unchanged when items are equal', () => {
const item = { name: 'key', value: 'val', enabled: true };
const otherItem = { name: 'key', value: 'val', enabled: true };
expect(computeItemDiffStatus(item, otherItem, 'old')).toBe('unchanged');
expect(computeItemDiffStatus(item, otherItem, 'new')).toBe('unchanged');
});
it('should return modified when values differ', () => {
const item = { name: 'key', value: 'val1', enabled: true };
const otherItem = { name: 'key', value: 'val2', enabled: true };
expect(computeItemDiffStatus(item, otherItem, 'old')).toBe('modified');
});
it('should return modified when enabled status differs', () => {
const item = { name: 'key', value: 'val', enabled: true };
const otherItem = { name: 'key', value: 'val', enabled: false };
expect(computeItemDiffStatus(item, otherItem, 'old')).toBe('modified');
});
it('should handle undefined enabled as different from explicit false', () => {
const item = { name: 'key', value: 'val' };
const otherItem = { name: 'key', value: 'val', enabled: false };
expect(computeItemDiffStatus(item, otherItem, 'old')).toBe('modified');
});
});
});

View File

@@ -0,0 +1,202 @@
// Matches word-boundary separators: whitespace, slashes, query/path delimiters (?&=), dots, hyphens, underscores, colons, @
const WORD_SEPARATOR = /[\s\/\?\&\=\.\-\_\:\@]/;
const splitWithSeparators = (str) => {
const result = [];
let current = '';
for (const char of str) {
if (WORD_SEPARATOR.test(char)) {
if (current) {
result.push(current);
current = '';
}
result.push(char);
} else {
current += char;
}
}
if (current) {
result.push(current);
}
return result;
};
export const computeWordDiffForOld = (oldStr, newStr) => {
if (oldStr === newStr) {
return [{ text: oldStr, status: 'unchanged' }];
}
if (!oldStr) {
return [];
}
if (!newStr) {
return [{ text: oldStr, status: 'deleted' }];
}
const oldWords = splitWithSeparators(oldStr);
const newWords = splitWithSeparators(newStr);
const lcs = computeLCS(oldWords, newWords);
const segments = [];
let oldIdx = 0;
let lcsIdx = 0;
while (oldIdx < oldWords.length) {
if (lcsIdx < lcs.length && oldIdx === lcs[lcsIdx].oldIndex) {
segments.push({ text: oldWords[oldIdx], status: 'unchanged' });
lcsIdx++;
} else {
segments.push({ text: oldWords[oldIdx], status: 'deleted' });
}
oldIdx++;
}
return mergeSegments(segments);
};
export const computeWordDiffForNew = (oldStr, newStr) => {
if (oldStr === newStr) {
return [{ text: newStr, status: 'unchanged' }];
}
if (!newStr) {
return [];
}
if (!oldStr) {
return [{ text: newStr, status: 'added' }];
}
const oldWords = splitWithSeparators(oldStr);
const newWords = splitWithSeparators(newStr);
const lcs = computeLCS(oldWords, newWords);
const segments = [];
let newIdx = 0;
let lcsIdx = 0;
while (newIdx < newWords.length) {
if (lcsIdx < lcs.length && newIdx === lcs[lcsIdx].newIndex) {
segments.push({ text: newWords[newIdx], status: 'unchanged' });
lcsIdx++;
} else {
segments.push({ text: newWords[newIdx], status: 'added' });
}
newIdx++;
}
return mergeSegments(segments);
};
const mergeSegments = (segments) => {
const merged = [];
for (const segment of segments) {
if (merged.length > 0 && merged[merged.length - 1].status === segment.status) {
merged[merged.length - 1].text += segment.text;
} else {
merged.push({ ...segment });
}
}
return merged;
};
const computeLCS = (arr1, arr2) => {
const m = arr1.length;
const n = arr2.length;
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (arr1[i - 1] === arr2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
const lcs = [];
let i = m, j = n;
while (i > 0 && j > 0) {
if (arr1[i - 1] === arr2[j - 1]) {
lcs.unshift({ value: arr1[i - 1], oldIndex: i - 1, newIndex: j - 1 });
i--;
j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return lcs;
};
export const computeLineDiffForOld = (oldStr, newStr) => {
if (oldStr === newStr) {
return (oldStr || '').split('\n').map((line) => ({ text: line, status: 'unchanged' }));
}
if (!oldStr) {
return [];
}
if (!newStr) {
return oldStr.split('\n').map((line) => ({ text: line, status: 'deleted' }));
}
const oldLines = oldStr.split('\n');
const newLines = newStr.split('\n');
const lcs = computeLCS(oldLines, newLines);
const segments = [];
let oldIdx = 0;
let lcsIdx = 0;
while (oldIdx < oldLines.length) {
if (lcsIdx < lcs.length && oldIdx === lcs[lcsIdx].oldIndex) {
segments.push({ text: oldLines[oldIdx], status: 'unchanged' });
lcsIdx++;
} else {
segments.push({ text: oldLines[oldIdx], status: 'deleted' });
}
oldIdx++;
}
return segments;
};
export const computeLineDiffForNew = (oldStr, newStr) => {
if (oldStr === newStr) {
return (newStr || '').split('\n').map((line) => ({ text: line, status: 'unchanged' }));
}
if (!newStr) {
return [];
}
if (!oldStr) {
return newStr.split('\n').map((line) => ({ text: line, status: 'added' }));
}
const oldLines = oldStr.split('\n');
const newLines = newStr.split('\n');
const lcs = computeLCS(oldLines, newLines);
const segments = [];
let newIdx = 0;
let lcsIdx = 0;
while (newIdx < newLines.length) {
if (lcsIdx < lcs.length && newIdx === lcs[lcsIdx].newIndex) {
segments.push({ text: newLines[newIdx], status: 'unchanged' });
lcsIdx++;
} else {
segments.push({ text: newLines[newIdx], status: 'added' });
}
newIdx++;
}
return segments;
};

View File

@@ -0,0 +1,198 @@
const { describe, it, expect } = require('@jest/globals');
import {
computeWordDiffForOld,
computeWordDiffForNew,
computeLineDiffForOld,
computeLineDiffForNew
} from './diffUtils';
describe('diffUtils', () => {
describe('computeWordDiffForOld', () => {
it('should return unchanged for identical strings', () => {
expect(computeWordDiffForOld('hello world', 'hello world')).toEqual([
{ text: 'hello world', status: 'unchanged' }
]);
});
it('should return empty array for empty old string', () => {
expect(computeWordDiffForOld('', 'new text')).toEqual([]);
expect(computeWordDiffForOld(null, 'new text')).toEqual([]);
expect(computeWordDiffForOld(undefined, 'new text')).toEqual([]);
});
it('should return deleted for entire old string when new is empty', () => {
expect(computeWordDiffForOld('old text', '')).toEqual([
{ text: 'old text', status: 'deleted' }
]);
expect(computeWordDiffForOld('old text', null)).toEqual([
{ text: 'old text', status: 'deleted' }
]);
});
it('should detect deleted words', () => {
const result = computeWordDiffForOld('hello world', 'hello');
expect(result).toContainEqual({ text: 'hello', status: 'unchanged' });
expect(result.some((s) => s.status === 'deleted' && s.text.includes('world'))).toBe(true);
});
it('should handle URL paths', () => {
const result = computeWordDiffForOld(
'https://api.example.com/users/123',
'https://api.example.com/users/456'
);
expect(result.some((s) => s.status === 'unchanged')).toBe(true);
expect(result.some((s) => s.status === 'deleted')).toBe(true);
});
it('should preserve separators', () => {
const result = computeWordDiffForOld('a/b/c', 'a/b/c');
expect(result).toEqual([{ text: 'a/b/c', status: 'unchanged' }]);
});
});
describe('computeWordDiffForNew', () => {
it('should return unchanged for identical strings', () => {
expect(computeWordDiffForNew('hello world', 'hello world')).toEqual([
{ text: 'hello world', status: 'unchanged' }
]);
});
it('should return empty array for empty new string', () => {
expect(computeWordDiffForNew('old text', '')).toEqual([]);
expect(computeWordDiffForNew('old text', null)).toEqual([]);
expect(computeWordDiffForNew('old text', undefined)).toEqual([]);
});
it('should return added for entire new string when old is empty', () => {
expect(computeWordDiffForNew('', 'new text')).toEqual([
{ text: 'new text', status: 'added' }
]);
expect(computeWordDiffForNew(null, 'new text')).toEqual([
{ text: 'new text', status: 'added' }
]);
});
it('should detect added words', () => {
const result = computeWordDiffForNew('hello', 'hello world');
expect(result).toContainEqual({ text: 'hello', status: 'unchanged' });
expect(result.some((s) => s.status === 'added' && s.text.includes('world'))).toBe(true);
});
it('should handle URL paths', () => {
const result = computeWordDiffForNew(
'https://api.example.com/users/123',
'https://api.example.com/users/456'
);
expect(result.some((s) => s.status === 'unchanged')).toBe(true);
expect(result.some((s) => s.status === 'added')).toBe(true);
});
});
describe('computeLineDiffForOld', () => {
it('should return unchanged for identical multiline strings', () => {
const text = 'line1\nline2\nline3';
expect(computeLineDiffForOld(text, text)).toEqual([
{ text: 'line1', status: 'unchanged' },
{ text: 'line2', status: 'unchanged' },
{ text: 'line3', status: 'unchanged' }
]);
});
it('should return empty array for empty old string', () => {
expect(computeLineDiffForOld('', 'new\ntext')).toEqual([]);
expect(computeLineDiffForOld(null, 'new\ntext')).toEqual([]);
});
it('should return deleted for all lines when new is empty', () => {
expect(computeLineDiffForOld('line1\nline2', '')).toEqual([
{ text: 'line1', status: 'deleted' },
{ text: 'line2', status: 'deleted' }
]);
});
it('should detect deleted lines', () => {
const result = computeLineDiffForOld('line1\nline2\nline3', 'line1\nline3');
expect(result).toContainEqual({ text: 'line1', status: 'unchanged' });
expect(result).toContainEqual({ text: 'line2', status: 'deleted' });
expect(result).toContainEqual({ text: 'line3', status: 'unchanged' });
});
it('should handle single line strings', () => {
expect(computeLineDiffForOld('single line', 'single line')).toEqual([
{ text: 'single line', status: 'unchanged' }
]);
});
it('should handle code blocks', () => {
const oldCode = 'function foo() {\n return 1;\n}';
const newCode = 'function foo() {\n return 2;\n}';
const result = computeLineDiffForOld(oldCode, newCode);
expect(result).toContainEqual({ text: 'function foo() {', status: 'unchanged' });
expect(result).toContainEqual({ text: ' return 1;', status: 'deleted' });
expect(result).toContainEqual({ text: '}', status: 'unchanged' });
});
});
describe('computeLineDiffForNew', () => {
it('should return unchanged for identical multiline strings', () => {
const text = 'line1\nline2\nline3';
expect(computeLineDiffForNew(text, text)).toEqual([
{ text: 'line1', status: 'unchanged' },
{ text: 'line2', status: 'unchanged' },
{ text: 'line3', status: 'unchanged' }
]);
});
it('should return empty array for empty new string', () => {
expect(computeLineDiffForNew('old\ntext', '')).toEqual([]);
expect(computeLineDiffForNew('old\ntext', null)).toEqual([]);
});
it('should return added for all lines when old is empty', () => {
expect(computeLineDiffForNew('', 'line1\nline2')).toEqual([
{ text: 'line1', status: 'added' },
{ text: 'line2', status: 'added' }
]);
});
it('should detect added lines', () => {
const result = computeLineDiffForNew('line1\nline3', 'line1\nline2\nline3');
expect(result).toContainEqual({ text: 'line1', status: 'unchanged' });
expect(result).toContainEqual({ text: 'line2', status: 'added' });
expect(result).toContainEqual({ text: 'line3', status: 'unchanged' });
});
it('should handle code blocks', () => {
const oldCode = 'function foo() {\n return 1;\n}';
const newCode = 'function foo() {\n return 2;\n}';
const result = computeLineDiffForNew(oldCode, newCode);
expect(result).toContainEqual({ text: 'function foo() {', status: 'unchanged' });
expect(result).toContainEqual({ text: ' return 2;', status: 'added' });
expect(result).toContainEqual({ text: '}', status: 'unchanged' });
});
});
describe('edge cases', () => {
it('should handle empty strings', () => {
expect(computeWordDiffForOld('', '')).toEqual([{ text: '', status: 'unchanged' }]);
expect(computeWordDiffForNew('', '')).toEqual([{ text: '', status: 'unchanged' }]);
});
it('should handle strings with only whitespace', () => {
const result = computeWordDiffForOld(' ', ' ');
expect(result).toEqual([{ text: ' ', status: 'unchanged' }]);
});
it('should handle special characters in URLs', () => {
const url = 'https://api.example.com/users?id=123&name=test';
expect(computeWordDiffForOld(url, url)).toEqual([{ text: url, status: 'unchanged' }]);
});
it('should handle JSON-like content', () => {
const json = '{"key": "value", "number": 123}';
const result = computeLineDiffForOld(json, json);
expect(result).toEqual([{ text: json, status: 'unchanged' }]);
});
});
});

Some files were not shown because too many files have changed in this diff Show More