Compare commits
1 Commits
feat/fix-e
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e959f788c |
@@ -1,79 +0,0 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
|
||||
language: 'en-US'
|
||||
early_access: false
|
||||
tone_instructions: 'You are an expert code reviewer in TypeScript, JavaScript, NodeJS, and ElectronJS. You work in an enterprise software developer team, providing concise and clear code review advice. You only elaborate or provide detailed explanations when requested.'
|
||||
|
||||
knowledge_base:
|
||||
opt_out: false
|
||||
code_guidelines:
|
||||
enabled: true
|
||||
filePatterns:
|
||||
- '**/CODING_STANDARDS.md'
|
||||
|
||||
reviews:
|
||||
profile: 'chill'
|
||||
request_changes_workflow: false
|
||||
high_level_summary: true
|
||||
poem: true
|
||||
review_status: true
|
||||
collapse_walkthrough: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
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:
|
||||
- Follow best practices for Playwright code and e2e automation
|
||||
- Try to reduce usage of `page.waitForTimeout();` in code unless absolutely necessary and the locator cannot be found using existing `expect()` playwright calls
|
||||
- Avoid using `page.pause()` in code
|
||||
- Use locator variables for locators
|
||||
- Avoid using test.only
|
||||
- Use multiple assertions
|
||||
- Promote the use of `test.step` as much as possible so the generated reports are easier to read
|
||||
- Ensure that the `fixtures` like the collections are nested inside the `fixtures` folder
|
||||
|
||||
|
||||
|
||||
**Fixture Example***: Here's an example of possible fixture and test pair
|
||||
```
|
||||
.
|
||||
├── fixtures
|
||||
│ └── collection
|
||||
│ ├── base.bru
|
||||
│ ├── bruno.json
|
||||
│ ├── collection.bru
|
||||
│ ├── ws-test-request-with-headers.bru
|
||||
│ ├── ws-test-request-with-subproto.bru
|
||||
│ └── ws-test-request.bru
|
||||
├── connection.spec.ts # <- Depends on the collection in ./fixtures/collection
|
||||
├── headers.spec.ts
|
||||
├── persistence.spec.ts
|
||||
├── variable-interpolation
|
||||
│ ├── fixtures
|
||||
│ │ └── collection
|
||||
│ │ ├── environments
|
||||
│ │ ├── bruno.json
|
||||
│ │ └── ws-interpolation-test.bru
|
||||
│ ├── init-user-data
|
||||
│ └── variable-interpolation.spec.ts # <- Depends on the collection in ./variable-interpolation/fixtures/collection
|
||||
└── subproto.spec.ts
|
||||
```
|
||||
|
||||
chat:
|
||||
auto_reply: true
|
||||
2
.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
# Force LF line endings for all text files
|
||||
* text=auto eol=lf
|
||||
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno @sid-bruno @vijayh-bruno
|
||||
* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno
|
||||
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,10 +1,9 @@
|
||||
### Description
|
||||
# Description
|
||||
|
||||
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
|
||||
|
||||
#### Contribution Checklist:
|
||||
### Contribution Checklist:
|
||||
|
||||
- [ ] **I've used AI significantly to create this pull request**
|
||||
- [ ] **The pull request only addresses one issue or adds one feature.**
|
||||
- [ ] **The pull request does not introduce any breaking changes**
|
||||
- [ ] **I have added screenshots or gifs to help explain the change if applicable.**
|
||||
@@ -13,6 +12,6 @@
|
||||
|
||||
Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.
|
||||
|
||||
#### Publishing to New Package Managers
|
||||
### Publishing to New Package Managers
|
||||
|
||||
Please see [here](../publishing.md) for more information.
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,15 +0,0 @@
|
||||
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
|
||||
@@ -1,16 +0,0 @@
|
||||
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: $!"
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,16 +0,0 @@
|
||||
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: $!"
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
@@ -1,34 +0,0 @@
|
||||
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 }
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -1,14 +1,5 @@
|
||||
name: 'Setup Node Dependencies'
|
||||
description: 'Install Node.js and npm dependencies'
|
||||
inputs:
|
||||
skip-build:
|
||||
description: 'Skip building libraries'
|
||||
required: false
|
||||
default: 'false'
|
||||
shell:
|
||||
description: 'Shell to use (bash, pwsh)'
|
||||
required: false
|
||||
default: 'bash'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@@ -18,14 +9,13 @@ runs:
|
||||
node-version: v22.17.0
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './package-lock.json'
|
||||
|
||||
|
||||
- name: Install node dependencies
|
||||
shell: ${{ inputs.shell }}
|
||||
shell: bash
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
|
||||
- name: Build libraries
|
||||
if: inputs.skip-build != 'true'
|
||||
shell: ${{ inputs.shell }}
|
||||
shell: bash
|
||||
run: |
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
@@ -33,5 +23,4 @@ runs:
|
||||
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
|
||||
|
||||
@@ -7,13 +7,13 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
xvfb-run npm run test:e2e:ssl
|
||||
|
||||
- name: Upload Playwright Report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-linux-ssl
|
||||
name: playwright-report-linux
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
@@ -12,6 +12,6 @@ runs:
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-macos-ssl
|
||||
name: playwright-report-macos
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
@@ -12,6 +12,6 @@ runs:
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-windows-ssl
|
||||
name: playwright-report-windows
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
name: 'Run Benchmark Tests'
|
||||
description: 'Run Playwright benchmark tests and compare against baseline'
|
||||
inputs:
|
||||
os:
|
||||
description: 'Operating system (ubuntu, macos, windows)'
|
||||
default: 'ubuntu'
|
||||
update-baseline:
|
||||
description: 'Update baseline instead of comparing'
|
||||
default: 'false'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Run Benchmark Tests (Ubuntu)
|
||||
if: inputs.os == 'ubuntu'
|
||||
shell: bash
|
||||
run: xvfb-run npm run test:benchmark
|
||||
|
||||
- name: Run Benchmark Tests
|
||||
if: inputs.os != 'ubuntu'
|
||||
shell: bash
|
||||
run: npm run test:benchmark
|
||||
|
||||
- name: Update Baseline
|
||||
if: inputs.update-baseline == 'true'
|
||||
shell: bash
|
||||
run: >-
|
||||
node tests/benchmarks/utils/compare.js
|
||||
--results tests/benchmarks/results/mounting.json
|
||||
--baseline tests/benchmarks/mounting/baseline.${{ inputs.os }}.json
|
||||
--update-baseline
|
||||
|
||||
- name: Compare Against Baseline
|
||||
if: inputs.update-baseline != 'true'
|
||||
shell: bash
|
||||
run: >-
|
||||
node tests/benchmarks/utils/compare.js
|
||||
--results tests/benchmarks/results/mounting.json
|
||||
--baseline tests/benchmarks/mounting/baseline.${{ inputs.os }}.json
|
||||
41
.github/actions/tests/run-cli-tests/action.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: 'Run CLI Tests'
|
||||
description: 'Setup dependencies, start local testbench and run CLI tests'
|
||||
inputs:
|
||||
shell:
|
||||
description: 'Shell to use (bash, pwsh)'
|
||||
required: false
|
||||
default: 'bash'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install Test Collection Dependencies
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm ci --prefix packages/bruno-tests/collection
|
||||
|
||||
- name: Run Local Testbench and CLI Tests
|
||||
if: inputs.shell != 'pwsh'
|
||||
shell: ${{ inputs.shell }}
|
||||
run: |
|
||||
npm start --workspace=packages/bruno-tests &
|
||||
sleep 5
|
||||
cd packages/bruno-tests/collection
|
||||
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer
|
||||
|
||||
- name: Run Local Testbench and CLI Tests - Windows
|
||||
if: inputs.shell == 'pwsh'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$process = Start-Process "npm.cmd" `
|
||||
-ArgumentList "start","--workspace=packages/bruno-tests" `
|
||||
-NoNewWindow `
|
||||
-PassThru
|
||||
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
if ($process.HasExited) {
|
||||
Write-Error "Server exited early"
|
||||
exit 1
|
||||
}
|
||||
|
||||
cd packages/bruno-tests/collection
|
||||
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer
|
||||
26
.github/actions/tests/run-e2e-tests/action.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: 'Run E2E Tests'
|
||||
description: 'Setup dependencies, configure environment, and run Playwright E2E tests'
|
||||
inputs:
|
||||
os:
|
||||
description: 'Operating system (ubuntu, macos, windows)'
|
||||
default: 'ubuntu'
|
||||
shell:
|
||||
description: 'Shell to use (bash, pwsh)'
|
||||
required: false
|
||||
default: 'bash'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install Test Collection Dependencies
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm ci --prefix packages/bruno-tests/collection
|
||||
|
||||
- name: Run Playwright Tests (Ubuntu)
|
||||
if: inputs.os == 'ubuntu'
|
||||
shell: bash
|
||||
run: xvfb-run dbus-run-session -- npm run test:e2e
|
||||
|
||||
- name: Run Playwright Tests
|
||||
if: inputs.os != 'ubuntu'
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test:e2e
|
||||
53
.github/actions/tests/run-unit-tests/action.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: 'Run Unit Tests'
|
||||
description: 'Setup dependencies and run unit tests for all packages'
|
||||
inputs:
|
||||
shell:
|
||||
description: 'Shell to use (bash, pwsh)'
|
||||
required: false
|
||||
default: 'bash'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Test Package bruno-js
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test:ci --workspace=packages/bruno-js
|
||||
|
||||
- name: Test Package bruno-cli
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test:ci --workspace=packages/bruno-cli
|
||||
|
||||
- name: Test Package bruno-query
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test --workspace=packages/bruno-query
|
||||
|
||||
- name: Test Package bruno-lang
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test --workspace=packages/bruno-lang
|
||||
|
||||
- name: Test Package bruno-schema
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test --workspace=packages/bruno-schema
|
||||
|
||||
- name: Test Package bruno-app
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
|
||||
- name: Test Package bruno-common
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test --workspace=packages/bruno-common
|
||||
|
||||
- name: Test Package bruno-converters
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test:ci --workspace=packages/bruno-converters
|
||||
|
||||
- name: Test Package bruno-electron
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test:ci --workspace=packages/bruno-electron
|
||||
|
||||
- name: Test Package bruno-requests
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test --workspace=packages/bruno-requests
|
||||
|
||||
- name: Test Package bruno-filestore
|
||||
shell: ${{ inputs.shell }}
|
||||
run: npm run test --workspace=packages/bruno-filestore
|
||||
70
.github/scripts/comment-on-flaky-tests.js
vendored
@@ -1,70 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Check if flaky-tests.json exists
|
||||
if (!fs.existsSync('flaky-tests.json')) {
|
||||
console.log('No flaky-tests.json found');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Get changed files in PR
|
||||
let changedFiles = [];
|
||||
try {
|
||||
changedFiles = execSync('git diff --name-only origin/main...HEAD')
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter(f => f.endsWith('.spec.ts'));
|
||||
} catch (error) {
|
||||
console.log('Could not determine changed files:', error.message);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (changedFiles.length === 0) {
|
||||
console.log('No test files were modified in this PR');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Read flaky tests
|
||||
const flakyTests = JSON.parse(fs.readFileSync('flaky-tests.json', 'utf8'));
|
||||
|
||||
if (flakyTests.length === 0) {
|
||||
console.log('No flaky/failed tests found');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Find modified flaky tests
|
||||
const modifiedFlakyTests = flakyTests.filter(test =>
|
||||
changedFiles.some(file => test.file.includes(file))
|
||||
);
|
||||
|
||||
if (modifiedFlakyTests.length === 0) {
|
||||
console.log('No modified test files are flaky');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Generate comment markdown
|
||||
let comment = '## ⚠️ Warning: You modified flaky/failed test files\n\n';
|
||||
comment += 'The following test files you modified have reliability issues:\n\n';
|
||||
|
||||
modifiedFlakyTests.forEach(test => {
|
||||
const testType = test.status === 'failed' ? '❌ Failed' : '⚠️ Flaky';
|
||||
comment += `### ${testType}: \`${test.file}\`\n`;
|
||||
comment += `**Test:** ${test.testTitle}\n`;
|
||||
comment += `**Status:** ${test.status}\n`;
|
||||
if (test.retryAttempt > 0) {
|
||||
comment += `**Retry Attempt:** ${test.retryAttempt}\n`;
|
||||
}
|
||||
comment += '\n**To debug locally, run:**\n';
|
||||
comment += '```bash\n';
|
||||
comment += `npx playwright test ${test.file} --repeat-each=5 --workers=1\n`;
|
||||
comment += '```\n\n';
|
||||
});
|
||||
|
||||
comment += '---\n';
|
||||
comment += '**Note:** Flaky tests passed after retrying, failed tests did not pass. ';
|
||||
comment += 'Please investigate and fix the root cause before merging.\n';
|
||||
|
||||
// Save comment to file for GitHub Action to post
|
||||
fs.writeFileSync('pr-comment.md', comment);
|
||||
|
||||
console.log(`Found ${modifiedFlakyTests.length} modified flaky tests`);
|
||||
78
.github/scripts/detect-flaky-tests.js
vendored
@@ -1,78 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
|
||||
// Read Playwright JSON report
|
||||
const resultsPath = 'playwright-report/results.json';
|
||||
|
||||
if (!fs.existsSync(resultsPath)) {
|
||||
console.log('No Playwright results found at', resultsPath);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const results = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
|
||||
|
||||
// Extract flaky tests
|
||||
// A test is flaky if: status === "passed" AND retry > 0
|
||||
// A test is failed if: status === "failed"
|
||||
// This means it failed initially but passed on retry OR failed completely
|
||||
const flakyTests = [];
|
||||
|
||||
function traverseSuites(suites) {
|
||||
for (const suite of suites) {
|
||||
// Process specs in this suite
|
||||
for (const spec of suite.specs || []) {
|
||||
for (const test of spec.tests || []) {
|
||||
// Check each test result
|
||||
for (const result of test.results || []) {
|
||||
// Track two types of problematic tests:
|
||||
// 1. Flaky: passed on a retry attempt (retry > 0)
|
||||
// 2. Failed: failed on all attempts
|
||||
if ((result.status === 'passed' && result.retry > 0) || result.status === 'failed') {
|
||||
flakyTests.push({
|
||||
file: spec.file,
|
||||
title: spec.title,
|
||||
testTitle: spec.title,
|
||||
line: spec.line,
|
||||
status: result.status,
|
||||
retryAttempt: result.retry
|
||||
});
|
||||
break; // Only record once per test
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process nested suites
|
||||
if (suite.suites && suite.suites.length > 0) {
|
||||
traverseSuites(suite.suites);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseSuites(results.suites || []);
|
||||
|
||||
// Save flaky tests to JSON
|
||||
fs.writeFileSync('flaky-tests.json', JSON.stringify(flakyTests, null, 2));
|
||||
|
||||
// Generate markdown report
|
||||
let markdown = '## ⚠️ Flaky/Failed Tests Detected\n\n';
|
||||
markdown += 'The following tests are problematic:\n\n';
|
||||
|
||||
flakyTests.forEach(test => {
|
||||
const testType = test.status === 'failed' ? '❌ Failed' : '⚠️ Flaky';
|
||||
markdown += `### ${testType}: \`${test.file}\`\n`;
|
||||
markdown += `- **Test:** ${test.testTitle}\n`;
|
||||
markdown += `- **Status:** ${test.status}\n`;
|
||||
if (test.retryAttempt > 0) {
|
||||
markdown += `- **Retry Attempt:** ${test.retryAttempt}\n`;
|
||||
}
|
||||
markdown += `- **Debug command:**\n`;
|
||||
markdown += '```bash\n';
|
||||
markdown += `npx playwright test ${test.file} --repeat-each=5 --workers=1\n`;
|
||||
markdown += '```\n\n';
|
||||
});
|
||||
|
||||
fs.writeFileSync('flaky-report.md', markdown);
|
||||
|
||||
console.log(`Found ${flakyTests.length} flaky/failed tests`);
|
||||
process.exit(flakyTests.length > 0 ? 1 : 0);
|
||||
88
.github/workflows/benchmarks.yml
vendored
@@ -1,88 +0,0 @@
|
||||
name: Benchmarks
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
update-baseline:
|
||||
description: 'Update baseline with current results instead of comparing'
|
||||
type: boolean
|
||||
default: false
|
||||
pull_request:
|
||||
branches: [main, 'release/v*']
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
name: Performance Benchmarks (${{ matrix.os }})
|
||||
timeout-minutes: 60
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-24.04, macos-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-24.04
|
||||
os-name: ubuntu
|
||||
- os: macos-latest
|
||||
os-name: macos
|
||||
- os: windows-latest
|
||||
os-name: windows
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install System Dependencies (Ubuntu)
|
||||
if: matrix.os-name == '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: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- name: Configure Chrome Sandbox
|
||||
if: matrix.os-name == 'ubuntu'
|
||||
run: |
|
||||
sudo chown root node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 node_modules/electron/dist/chrome-sandbox
|
||||
|
||||
- name: Run Benchmark Tests
|
||||
uses: ./.github/actions/tests/run-benchmark-tests
|
||||
with:
|
||||
os: ${{ matrix.os-name }}
|
||||
update-baseline: ${{ github.event.inputs.update-baseline || 'false' }}
|
||||
|
||||
- name: Upload Benchmark Results
|
||||
uses: actions/upload-artifact@v6
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: benchmark-results-${{ matrix.os-name }}
|
||||
path: |
|
||||
tests/benchmarks/results/
|
||||
benchmark-report/
|
||||
retention-days: 30
|
||||
|
||||
- name: Commit Updated Baseline
|
||||
if: github.event.inputs.update-baseline == 'true'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add tests/benchmarks/mounting/baseline.${{ matrix.os-name }}.json
|
||||
git diff --staged --quiet || git commit -m "chore: update ${{ matrix.os-name }} benchmark baseline" && git push
|
||||
|
||||
- name: Comment Benchmark Results on PR
|
||||
if: github.event_name == 'pull_request' && !cancelled()
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const run = require('./tests/benchmarks/utils/pr-comment.js');
|
||||
await run({
|
||||
github,
|
||||
context,
|
||||
resultsPath: 'tests/benchmarks/results/mounting.json',
|
||||
baselinePath: 'tests/benchmarks/mounting/baseline.${{ matrix.os-name }}.json',
|
||||
title: 'Benchmark Results — Collection Mount (${{ matrix.os-name }})'
|
||||
});
|
||||
120
.github/workflows/flaky-test-detector.yml
vendored
@@ -1,120 +0,0 @@
|
||||
name: Flaky Test Detector
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'tests/**/*.spec.ts'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
detect-flaky-tests:
|
||||
name: Detect Flaky Tests
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # Need full history to compare with main
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install system 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
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: |
|
||||
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
|
||||
|
||||
- name: Install test collection dependencies
|
||||
run: npm ci --prefix packages/bruno-tests/collection
|
||||
|
||||
- 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: Run Playwright tests
|
||||
run: xvfb-run npm run test:e2e
|
||||
continue-on-error: true # Continue even if tests fail
|
||||
|
||||
- name: Detect flaky tests
|
||||
id: detect
|
||||
run: node .github/scripts/detect-flaky-tests.js
|
||||
continue-on-error: true # Don't fail workflow if flaky tests found
|
||||
|
||||
- name: Check modified flaky tests
|
||||
id: check-modified
|
||||
run: node .github/scripts/comment-on-flaky-tests.js
|
||||
continue-on-error: true
|
||||
|
||||
- name: Post PR comment
|
||||
if: hashFiles('pr-comment.md') != ''
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const comment = fs.readFileSync('pr-comment.md', 'utf8');
|
||||
|
||||
// Check if we already commented
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
|
||||
const botComment = comments.find(c =>
|
||||
c.user.type === 'Bot' && c.body.includes('Warning: You modified flaky/failed test files')
|
||||
);
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: comment
|
||||
});
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
}
|
||||
|
||||
- name: Upload flaky test artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: flaky-test-results
|
||||
path: |
|
||||
flaky-tests.json
|
||||
flaky-report.md
|
||||
playwright-report/
|
||||
retention-days: 30
|
||||
26
.github/workflows/lint-checks.yml
vendored
@@ -1,26 +0,0 @@
|
||||
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' }}
|
||||
8
.github/workflows/npm-bru-cli.yml
vendored
@@ -25,8 +25,8 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -40,10 +40,10 @@ jobs:
|
||||
run: |
|
||||
cd packages/bruno-tests/collection
|
||||
npm install
|
||||
bru run --env Prod --output junit.xml --format junit --sandbox developer
|
||||
bru run --env Prod --output junit.xml --format junit
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: dorny/test-reporter@v3
|
||||
uses: dorny/test-reporter@v2
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: Test Report
|
||||
|
||||
91
.github/workflows/ssl-tests.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: SSL Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
tests-for-linux:
|
||||
name: SSL Tests - Linux
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- name: Setup Feature Dependencies
|
||||
uses: ./.github/actions/ssl/linux/setup-feature-specific-deps
|
||||
|
||||
- name: Setup CA Certificates
|
||||
uses: ./.github/actions/ssl/linux/setup-ca-certs
|
||||
|
||||
- name: Run Basic SSL CLI Tests
|
||||
uses: ./.github/actions/ssl/linux/run-basic-ssl-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs CLI Tests
|
||||
uses: ./.github/actions/ssl/linux/run-custom-ca-certs-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs E2E Tests
|
||||
uses: ./.github/actions/ssl/linux/run-ssl-e2e-tests
|
||||
|
||||
tests-for-macos:
|
||||
name: SSL Tests - macOS
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- name: Setup Feature Dependencies
|
||||
uses: ./.github/actions/ssl/macos/setup-feature-specific-deps
|
||||
|
||||
- name: Setup CA Certificates
|
||||
uses: ./.github/actions/ssl/macos/setup-ca-certs
|
||||
|
||||
- name: Run Basic SSL CLI Tests
|
||||
uses: ./.github/actions/ssl/macos/run-basic-ssl-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs CLI Tests
|
||||
uses: ./.github/actions/ssl/macos/run-custom-ca-certs-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs E2E Tests
|
||||
uses: ./.github/actions/ssl/macos/run-ssl-e2e-tests
|
||||
|
||||
tests-for-windows:
|
||||
name: SSL Tests - Windows
|
||||
timeout-minutes: 60
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- name: Setup CA Certificates
|
||||
uses: ./.github/actions/ssl/windows/setup-ca-certs
|
||||
|
||||
- name: Run Basic SSL CLI Tests
|
||||
uses: ./.github/actions/ssl/windows/run-basic-ssl-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs CLI Tests
|
||||
uses: ./.github/actions/ssl/windows/run-custom-ca-certs-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs E2E Tests
|
||||
uses: ./.github/actions/ssl/windows/run-ssl-e2e-tests
|
||||
139
.github/workflows/tests-linux.yml
vendored
@@ -1,139 +0,0 @@
|
||||
name: Linux Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, 'release/v*']
|
||||
pull_request:
|
||||
branches: [main, 'release/v*']
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: Unit Tests (Linux)
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- name: Run Unit Tests
|
||||
uses: ./.github/actions/tests/run-unit-tests
|
||||
|
||||
cli-test:
|
||||
name: CLI Tests (Linux)
|
||||
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: Run CLI Tests
|
||||
uses: ./.github/actions/tests/run-cli-tests
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
check_name: CLI Test Results (Linux)
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: always
|
||||
check_run: false
|
||||
|
||||
e2e-test:
|
||||
name: Playwright E2E Tests (Linux)
|
||||
timeout-minutes: 240
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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 \
|
||||
gsettings-desktop-schemas dbus-x11
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- 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 E2E 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-linux
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
ssl-test:
|
||||
name: SSL 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/ssl/linux/setup-feature-specific-deps
|
||||
|
||||
- name: Setup CA Certificates
|
||||
uses: ./.github/actions/ssl/linux/setup-ca-certs
|
||||
|
||||
- name: Run Basic SSL CLI Tests
|
||||
uses: ./.github/actions/ssl/linux/run-basic-ssl-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs CLI Tests
|
||||
uses: ./.github/actions/ssl/linux/run-custom-ca-certs-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs E2E Tests
|
||||
uses: ./.github/actions/ssl/linux/run-ssl-e2e-tests
|
||||
|
||||
oauth1-tests:
|
||||
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
|
||||
123
.github/workflows/tests-macos.yml
vendored
@@ -1,123 +0,0 @@
|
||||
name: macOS Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, 'release/v*']
|
||||
pull_request:
|
||||
branches: [main, 'release/v*']
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: Unit Tests (macOS)
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- name: Run Unit Tests
|
||||
uses: ./.github/actions/tests/run-unit-tests
|
||||
|
||||
cli-test:
|
||||
name: CLI Tests (macOS)
|
||||
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 CLI Tests
|
||||
uses: ./.github/actions/tests/run-cli-tests
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: EnricoMi/publish-unit-test-result-action/macos@v2
|
||||
if: always()
|
||||
with:
|
||||
check_name: CLI Test Results (macOS)
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: off
|
||||
check_run: false
|
||||
|
||||
e2e-test:
|
||||
name: Playwright E2E Tests (macOS)
|
||||
timeout-minutes: 240
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- name: Run E2E Tests
|
||||
uses: ./.github/actions/tests/run-e2e-tests
|
||||
with:
|
||||
os: macos
|
||||
|
||||
- name: Upload Playwright Report
|
||||
uses: actions/upload-artifact@v6
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report-macos
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
ssl-test:
|
||||
name: SSL 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: Setup Feature Dependencies
|
||||
uses: ./.github/actions/ssl/macos/setup-feature-specific-deps
|
||||
|
||||
- name: Setup CA Certificates
|
||||
uses: ./.github/actions/ssl/macos/setup-ca-certs
|
||||
|
||||
- name: Run Basic SSL CLI Tests
|
||||
uses: ./.github/actions/ssl/macos/run-basic-ssl-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs CLI Tests
|
||||
uses: ./.github/actions/ssl/macos/run-custom-ca-certs-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs E2E Tests
|
||||
uses: ./.github/actions/ssl/macos/run-ssl-e2e-tests
|
||||
|
||||
oauth1-tests:
|
||||
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
|
||||
134
.github/workflows/tests-windows.yml
vendored
@@ -1,134 +0,0 @@
|
||||
name: Windows Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, 'release/v*']
|
||||
pull_request:
|
||||
branches: [main, 'release/v*']
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: Unit Tests (Windows)
|
||||
if: false # @TODO: Temporarily disabled. Remove this once the tests are fixed.
|
||||
timeout-minutes: 60
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
with:
|
||||
shell: pwsh
|
||||
|
||||
- name: Run Unit Tests
|
||||
uses: ./.github/actions/tests/run-unit-tests
|
||||
with:
|
||||
shell: pwsh
|
||||
|
||||
cli-test:
|
||||
name: CLI Tests (Windows)
|
||||
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
|
||||
with:
|
||||
shell: pwsh
|
||||
|
||||
- name: Run CLI Tests
|
||||
uses: ./.github/actions/tests/run-cli-tests
|
||||
with:
|
||||
shell: pwsh
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: EnricoMi/publish-unit-test-result-action/windows@v2
|
||||
if: always()
|
||||
with:
|
||||
check_name: CLI Test Results (Windows)
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: off
|
||||
check_run: false
|
||||
|
||||
e2e-test:
|
||||
name: Playwright E2E Tests (Windows)
|
||||
timeout-minutes: 240
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
with:
|
||||
shell: pwsh
|
||||
|
||||
- name: Run E2E Tests
|
||||
uses: ./.github/actions/tests/run-e2e-tests
|
||||
with:
|
||||
os: windows
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload Playwright Report
|
||||
uses: actions/upload-artifact@v6
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report-windows
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
ssl-test:
|
||||
name: SSL 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
|
||||
with:
|
||||
shell: pwsh
|
||||
|
||||
- name: Setup CA Certificates
|
||||
uses: ./.github/actions/ssl/windows/setup-ca-certs
|
||||
|
||||
- name: Run Basic SSL CLI Tests
|
||||
uses: ./.github/actions/ssl/windows/run-basic-ssl-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs CLI Tests
|
||||
uses: ./.github/actions/ssl/windows/run-custom-ca-certs-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs E2E Tests
|
||||
uses: ./.github/actions/ssl/windows/run-ssl-e2e-tests
|
||||
|
||||
oauth1-tests:
|
||||
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
|
||||
147
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
name: Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: Unit Tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v6
|
||||
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-filestore
|
||||
|
||||
- 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
|
||||
|
||||
cli-test:
|
||||
name: CLI Tests
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-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-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
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
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@v5
|
||||
- uses: actions/setup-node@v6
|
||||
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
|
||||
|
||||
- name: Install dependencies for test collection environment
|
||||
run: |
|
||||
npm ci --prefix packages/bruno-tests/collection
|
||||
|
||||
- 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:bruno-filestore
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
xvfb-run npm run test:e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
21
.gitignore
vendored
@@ -48,27 +48,6 @@ yarn-error.log*
|
||||
bruno.iml
|
||||
.idea
|
||||
.vscode
|
||||
.cursor
|
||||
.claude
|
||||
.codex
|
||||
.agents
|
||||
.agent
|
||||
skills-lock.json
|
||||
|
||||
# Playwright
|
||||
/blob-report/
|
||||
|
||||
# Benchmark results (generated at runtime)
|
||||
tests/benchmarks/results/
|
||||
/benchmark-report/
|
||||
|
||||
# Development plan files
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
*.plan.md
|
||||
|
||||
# packages dist
|
||||
packages/bruno-filestore/dist
|
||||
packages/bruno-requests/dist
|
||||
packages/bruno-schema-types/dist
|
||||
packages/bruno-converters/dist
|
||||
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
# Bruno Coding Standards
|
||||
|
||||
- No diffs unless an actual change is made, the code changes need to be as minimal as possible, avoid making un-necessary whitespace diffs. This is already handled by eslint but make sure you check your code changes before commiting and raising a PR.
|
||||
|
||||
## General Style Rules
|
||||
|
||||
- Use 2 spaces for indentation. No tabs, just spaces – keeps everything neat and uniform.
|
||||
|
||||
- Stick to single quotes for strings. For JSX/TSX attributes, use double quotes (e.g., <svg xmlns="..." viewBox="...">) to follow React conventions.
|
||||
|
||||
- Always add semicolons at the end of statements. It's like putting a period at the end of a sentence – clarity matters.
|
||||
|
||||
- JSX is enabled, so feel free to use it where it makes sense.
|
||||
|
||||
## Punctuation and Spacing
|
||||
|
||||
- No trailing commas. Keep it clean, no extra commas hanging around.
|
||||
|
||||
- Always use parentheses around parameters in arrow functions. Even for single params – consistency is key.
|
||||
|
||||
- For multiline constructs, put opening braces on the same line, and ensure consistency. Minimum 2 elements for multiline.
|
||||
|
||||
- No newlines inside function parentheses. Keep 'em tight.
|
||||
|
||||
- Space before and after the arrow in arrow functions. `() => {}` is good.
|
||||
|
||||
- No space between function name and parentheses. `func()` not `func ()`.
|
||||
|
||||
- Semicolons go at the end of the line, not on a new line.
|
||||
|
||||
- No strict max length – write readable code, not cramped lines.
|
||||
|
||||
- Multiple expressions per line in JSX are fine – flexibility is nice.
|
||||
|
||||
Remember, these rules are here to make our codebase harmonious. If something doesn't fit perfectly, let's chat about it. Happy coding! 🚀
|
||||
|
||||
|
||||
## Tests
|
||||
|
||||
- Add tests for any new functionality or meaningful changes. If code is added, removed, or significantly modified, corresponding tests should be updated or created.
|
||||
|
||||
- Prioritise high-value tests over maximum coverage. Focus on testing behaviour that is critical, complex, or likely to break—don’t chase coverage numbers for their own sake.
|
||||
|
||||
- Write behaviour-driven tests, not implementation-driven ones. Tests should validate real expected output and observable behaviour, not internal details or mocked-out logic unless absolutely necessary.
|
||||
|
||||
- Minimise mocking unless it meaningfully increases clarity or isolates external dependencies. Prefer real flows where practical; only mock external services, slow systems, or non-deterministic behaviour.
|
||||
|
||||
- Keep tests readable and maintainable. Optimise for clarity over cleverness. Name tests descriptively, keep setup minimal, and avoid unnecessary abstraction.
|
||||
|
||||
- Aim for tests that fail usefully. When a test fails, it should clearly indicate what behaviour broke and why.
|
||||
|
||||
- Cover both the “happy path” and the realistically problematic paths. Validate expected success behaviour, but also validate error handling, edge cases, and degraded-mode behaviour when appropriate.
|
||||
|
||||
- Ensure tests are deterministic and reproducible. No randomness, timing dependencies, or environment-specific assumptions without explicit control.
|
||||
|
||||
- Avoid overfitting tests to current behaviour if future flexibility matters. Only assert what needs to be true, not incidental details.
|
||||
|
||||
- Use consistent patterns and helper utilities where they improve clarity. Prefer shared test utilities over copy-pasted setup code, but only when it actually reduces complexity.
|
||||
|
||||
- Tests should be fast enough to run continuously. Avoid long-running operations unless absolutely necessary; prefer lightweight fixtures and isolated units.
|
||||
|
||||
### E2E Tests
|
||||
|
||||
When reviewing Electron-specific Playwright tests, treat `<project-root>/tests/**` as the canonical location for specs, typically matching `<project-root>/tests/**/*.spec.{ts,js}`. For broader Playwright workflow guidance, also refer to `docs/playwright-testing-guide.md`.
|
||||
|
||||
Goal: rewrite or critique the tests so they are genuinely behavioural, maintainable, and safely parallelizable.
|
||||
|
||||
Rules:
|
||||
1. Tests must verify user-visible behaviour, not implementation details.
|
||||
- Prefer assertions on UI state, persisted data, windows, dialogs, filesystem effects, and app-level outcomes.
|
||||
- Avoid hardcoded waits, brittle selectors, fake internal state checks, and “click then expect mock called” tests unless the user behaviour is the point.
|
||||
|
||||
2. Tests must be Electron-aware.
|
||||
- Use Electron app launch patterns correctly.
|
||||
- Handle main window, secondary windows, dialogs, menus, native prompts, clipboard, file pickers, and IPC-driven UI behaviour through observable outcomes.
|
||||
- Do not reach into app internals unless absolutely necessary for setup or controlled test fixtures.
|
||||
|
||||
3. Tests must be parallel-safe.
|
||||
- No shared user data directories.
|
||||
- No shared ports, files, DBs, caches, clipboard assumptions, or global app state.
|
||||
- Each test gets isolated temp paths, unique workspace/project names, and deterministic cleanup.
|
||||
- Avoid test ordering assumptions.
|
||||
|
||||
4. No hardcoded mess.
|
||||
- Replace magic timeouts with event-driven waits.
|
||||
- Replace brittle text/index selectors with role, label, test id, or stable user-facing selectors.
|
||||
- Replace duplicated setup with fixtures.
|
||||
- Replace hardcoded absolute paths with temp dirs.
|
||||
- Replace random sleeps with waiting for actual app signals.
|
||||
|
||||
5. Every test should follow this shape:
|
||||
- Arrange: create isolated fixture state.
|
||||
- Act: perform real user actions.
|
||||
- Assert: verify observable behavioural outcome.
|
||||
- Cleanup: remove isolated resources.
|
||||
|
||||
For each test file:
|
||||
- Identify behavioural vs non-behavioural tests.
|
||||
- Flag brittle selectors, hardcoded waits, shared state, serial dependencies, and fake assertions.
|
||||
- Rewrite the tests using Playwright best practices for Electron.
|
||||
- Make them parallel-ready.
|
||||
- Explain briefly why each rewrite is better.
|
||||
|
||||
## UI Specific instructions
|
||||
|
||||
### React
|
||||
|
||||
- Use styled component's theme prop to manage CSS colors and not CSS variables when in the context of a styled component or any react component using the styled component
|
||||
- Styled Components are used as wrappers to define both self and children components style, tailwind classes are used specifically for layout based styles.
|
||||
- Styled Component CSS might also change layout but tailwind classes shouldn't define colors.
|
||||
- MUST: Prefer custom hooks for business logic, data fetching, and side-effects.
|
||||
- MUST: Avoid `useEffect` unless absolutely needed. Prefer derived state, event handlers.
|
||||
- SHOULD: Memoize only when necessary (`useMemo`/`useCallback`), and prefer moving logic into hooks first.
|
||||
- MUST: Do not use namespace access for hooks in app code (e.g., `React.useCallback`, `React.useMemo`, `React.useState`). Import hooks directly.
|
||||
- Correct: `import { useCallback, useMemo, useState } from "react";`
|
||||
- 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
|
||||
|
||||
- Avoid abstractions unless the exact same code is being used in more than 3 places.
|
||||
- Names for functions need to be concise and descriptive.
|
||||
- Add in JSDoc comments to add more details to the abstractions if needed.
|
||||
- Follow functional programming but just enough to be readable, we don't need to go as deep as ADTs and Monads, we want to keep the code pipeline obvious and easy for everyone to read and contribute to.
|
||||
- Avoid single line abstractions where all that's being done is increasing the call stack with one additional function.
|
||||
- Add in meaningful comments instead of obvious ones where complex code flow is explained properly.
|
||||
|
Before Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 347 KiB |
|
Before Width: | Height: | Size: 584 KiB After Width: | Height: | Size: 813 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 615 KiB After Width: | Height: | Size: 153 KiB |
@@ -16,7 +16,6 @@
|
||||
| [日本語](docs/contributing/contributing_ja.md)
|
||||
| [हिंदी](docs/contributing/contributing_hi.md)
|
||||
| [Dutch](docs/contributing/contributing_nl.md)
|
||||
| [فارسی](docs/contributing/contributing_fa.md)
|
||||
|
||||
## Let's make Bruno better, together!!
|
||||
|
||||
@@ -70,13 +69,11 @@ npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
npm run build:schema-types
|
||||
npm run build:bruno-filestore
|
||||
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
|
||||
```bash
|
||||
@@ -97,22 +94,18 @@ npm run dev:electron
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
|
||||
```bash
|
||||
# run electron and react app concurrently
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### Customize Electron `userData` path
|
||||
|
||||
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
|
||||
|
||||
e.g.
|
||||
|
||||
```sh
|
||||
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
|
||||
```
|
||||
|
||||
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
[English](../../contributing.md)
|
||||
|
||||
## با هم، Bruno را بهتر میکنیم!
|
||||
|
||||
خوشحالم که قصد دارید Bruno را بهبود ببخشید. در ادامه قوانین و راهنماها برای راهاندازی Bruno روی سیستم شما آورده شده است.
|
||||
|
||||
### فناوریهای استفادهشده
|
||||
|
||||
به فارسی برونو Bruno با استفاده از Next.js و React ساخته شده است. همچنین از Electron برای بستهبندی نسخه دسکتاپ (که امکان مجموعههای محلی را فراهم میکند) استفاده میکنیم.
|
||||
|
||||
کتابخانههایی که استفاده میکنیم:
|
||||
|
||||
- CSS - Tailwind استایل
|
||||
- Codemirror - ویرایشگر کد
|
||||
- Redux - مدیریت وضعیت
|
||||
- Tabler Icons - آیکونها
|
||||
- formik - فرمها
|
||||
- Yup اعتبارسنجی اسکیمـا
|
||||
- axios - کلاینت درخواست
|
||||
- chokidar - پایشگر سیستم فایل
|
||||
|
||||
### پیشنیازها
|
||||
|
||||
شما به [نود v20.x یا اخرین نسخه پایدار](https://nodejs.org/en/) و npm 8.x نیاز دارید. در این پروژه از فضای کاری npm (npm workspaces) استفاده میکنیم.
|
||||
|
||||
### شروع به کدنویسی
|
||||
|
||||
برای راهاندازی محیط توسعه محلی به فایل [مستندات توسعه](docs/development_fa.md) مراجعه کنید:
|
||||
|
||||
### ارسال Pull Request
|
||||
|
||||
1 - لطفاً Pull Requestها (PR) را کوتاه و متمرکز نگه دارید و تنها یک هدف مشخص را دنبال کنند. </br>
|
||||
2 - لطفاً از فرمت نامگذاری شاخهها استفاده کنید:
|
||||
|
||||
- feature/[name]: این شاخه باید شامل یک قابلیت مشخص باشد.
|
||||
- feature/dark-mode : مثال
|
||||
- bugfix/[name]: این شاخه باید تنها شامل رفع یک باگ مشخص باشد.
|
||||
- bugfix/bug-1 : مثال
|
||||
|
||||
## توسعه
|
||||
|
||||
به فارسی برونو یا Bruno بهصورت یک اپلیکیشن «سنگین» توسعه داده میشود. برای اجرا باید ابتدا Next.js را در یک پنجره ترمینال اجرا کنید و سپس اپلیکیشن Electron را در پنجره ترمینال دیگری راهاندازی نمایید.
|
||||
|
||||
### نیازمندی توسعه
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### اجرای محلی
|
||||
|
||||
```bash
|
||||
# از ورژن NodeJS 18 استفاده کنید
|
||||
nvm use
|
||||
|
||||
# نصب وابستگیها
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# ساخت مستندات GraphQL
|
||||
npm run build:graphql-docs
|
||||
|
||||
# ساخت bruno-query
|
||||
npm run build:bruno-query
|
||||
|
||||
# اجرای اپ Next (ترمینال 1)
|
||||
npm run dev:web
|
||||
|
||||
# اجرای اپ Electron (ترمینال 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### عیبیابی
|
||||
|
||||
ممکن است هنگام اجرای `npm install` خطای `Unsupported platform` ببینید. برای رفع این مشکل، پوشه `node_modules` و فایل `package-lock.json` را حذف کرده و سپس دوباره `npm install` را اجرا کنید. این کار معمولاً همه پکیجهای لازم را نصب میکند.
|
||||
|
||||
```shell
|
||||
# حذف پوشه node_modules در زیردایرکتوریها
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# حذف فایل package-lock.json در زیردایرکتوریها
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### تستها
|
||||
|
||||
```bash
|
||||
# اجرای تستهای schema مربوط به bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# اجرای تستها در همه فضاهای کاری (در صورت وجود)
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
@@ -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.getByTestId('send-arrow-icon').click();
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
|
||||
// Verify response
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[English](../../publishing.md)
|
||||
|
||||
### انتشار Bruno در یک پکیج منیجر جدید
|
||||
|
||||
اگرچه کد ما متنباز است و همه میتوانند از آن استفاده کنند، لطفاً قبل از انتشار Bruno در مدیر بستههای جدید با ما تماس بگیرید. به عنوان سازنده Bruno، علامت تجاری `Bruno` را برای این پروژه دارم و مایلم توزیع آن را مدیریت کنم. اگر دوست دارید Bruno را در یک مدیر بسته جدید ببینید، لطفاً یک issue در گیتهاب ثبت کنید.
|
||||
|
||||
اگرچه بیشتر قابلیتهای ما رایگان و متنباز هستند (شامل REST و GraphQL Apis)،
|
||||
ما تلاش میکنیم بین اصول متنباز و توسعه پایدار تعادل مناسبی برقرار کنیم - https://github.com/usebruno/bruno/discussions/269
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### برونو - بيئة تطوير مفتوحة المصدر لاستكشاف واختبار واجهات برمجة التطبيقات (APIs).
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
@@ -41,6 +41,13 @@
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### الطبعة الذهبية ✨
|
||||
|
||||
غالبية ميزاتنا مجانية ومفتوحة المصدر.
|
||||
نحن نسعى لتحقيق توازن متناغم بين [مبادئ الشفافية والاستدامة](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
طلبات الشراء لـ [الطبعة الذهبية](https://www.usebruno.com/pricing) ستطلق قريبًا بسعر ~~$19~~ **$9** ! <br/>
|
||||
[اشترك هنا](https://usebruno.ck.page/4c65576bd4) لتصلك إشعارات عند الإطلاق.
|
||||
|
||||
### التثبيت
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### ব্রুনো - API অন্বেষণ এবং পরীক্ষা করার জন্য ওপেনসোর্স IDE।
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - 开源 IDE,用于探索和测试 API。
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Opensource IDE zum Erkunden und Testen von APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
@@ -43,6 +43,13 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
Die meisten unserer Funktionen sind kostenlos und quelloffen.
|
||||
Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
Du kannst die [Golden Edition](https://www.usebruno.com/pricing) bestellen **$19**! <br/>
|
||||
|
||||
### Installation
|
||||
|
||||
Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE de código abierto para explorar y probar APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
@@ -43,6 +43,13 @@ Bruno funciona sin conexión a internet. No tenemos intenciones de añadir sincr
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
La mayoría de nuestras funcionalidades son gratis y de código abierto.
|
||||
Queremos alcanzar un equilibrio en armonía entre los [principios open-source y la sostenibilidad](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
¡Puedes reservar la [Golden Edition](https://www.usebruno.com/pricing) por ~~$19~~ **$9**! <br/>
|
||||
|
||||
### Instalación
|
||||
|
||||
Bruno está disponible para su descarga [en nuestro sitio web](https://www.usebruno.com/downloads) para Mac, Windows y Linux.
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](../../readme.md)
|
||||
| [Українська](./readme_ua.md)
|
||||
| [Русский](./readme_ru.md)
|
||||
| [Türkçe](./readme_tr.md)
|
||||
| [Deutsch](./readme_de.md)
|
||||
| [Français](./readme_fr.md)
|
||||
| [Português (BR)](./readme_pt_br.md)
|
||||
| [한국어](./readme_kr.md)
|
||||
| [বাংলা](./readme_bn.md)
|
||||
| [Español](./readme_es.md)
|
||||
| **فارسی**
|
||||
| [Română](./readme_ro.md)
|
||||
| [Polski](./readme_pl.md)
|
||||
| [简体中文](./readme_cn.md)
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
برونو یک کلاینت API جدید و نوآورانه است که هدفش تغییر وضعیت فعلی ابزارهایی مانند Postman و سایر ابزارهای مشابه است.
|
||||
|
||||
برونو مجموعههای شما را مستقیماً در یک پوشه روی فایلسیستم شما ذخیره میکند. ما از یک زبان نشانهگذاری ساده به نام Bru برای ذخیره اطلاعات درخواستهای API استفاده میکنیم.
|
||||
|
||||
شما میتوانید برای همکاری روی مجموعههای API خود، از Git یا هر سیستم کنترل نسخه دلخواهتان استفاده کنید.
|
||||
|
||||
برونو فقط به صورت آفلاین کار میکند. هیچ برنامهای برای اضافه کردن همگامسازی ابری به برونو در آینده وجود ندارد. ما به حریم خصوصی دادههای شما اهمیت میدهیم و معتقدیم که باید روی دستگاه خودتان باقی بمانند. میتوانید چشمانداز بلندمدت ما را مطالعه کنید. [اینجا (به انگلیسی)](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
📢 جدیدترین ارائه ما را در کنفرانس India FOSS 3.0 تماشا کنید.
|
||||
[اینجا](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### نصب
|
||||
|
||||
برونو به صورت یک فایل باینری برای دانلود در دسترس است. [بر روی وبسایت ما](https://www.usebruno.com/downloads) برای مک لینکوس و ویندوز.
|
||||
|
||||
همچنین میتوانید برونو را از طریق مدیر بستههایی مانند Homebrew، Chocolatey، Snap و Apt نصب کنید.
|
||||
|
||||
```sh
|
||||
# بر روی مک از طریق brew
|
||||
brew install bruno
|
||||
|
||||
# بر روی ویندوز از طریق Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# بر روی لینوکس از طریق Snap
|
||||
snap install bruno
|
||||
|
||||
# بر روی لینوکس از طریق Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
### روی پلتفرمهای مختلف کار میکند 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### همکاری از طریق گیت 👩💻🧑💻
|
||||
|
||||
یا هر سیستم کنترل نسخهای که ترجیح میدهید
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### لینکهای مهم 📌
|
||||
|
||||
- [آخرین نسخه پایدار ما](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [نقشه راه](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [مستندات](https://docs.usebruno.com)
|
||||
- [وبسایت](https://www.usebruno.com)
|
||||
- [اشتراک ها](https://www.usebruno.com/pricing)
|
||||
- [دانلود](https://www.usebruno.com/downloads)
|
||||
|
||||
### ویدیوها 🎥
|
||||
|
||||
- [تجربه ها](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [مرکز دانش](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [اسکریپ مانیا](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### حمایت ❤️
|
||||
|
||||
جوون! اگر این پروژه را دوست دارید، روی دکمه ⭐ کلیک کنید!
|
||||
|
||||
### تجربههای به اشتراک گذاشتهشده 📣
|
||||
|
||||
اگر برونو به شما یا تیمتان کمک کرده است، لطفاً فراموش نکنید تجربههای خود را به اشتراک بگذارید. [تجربههای خود را در بحث گیتهاب ما به اشتراک بگذارید](https://github.com/usebruno/bruno/discussions/343).
|
||||
|
||||
### انتشار برونو در یک پکیچ منیجر جدید
|
||||
|
||||
لطفا چک بکنید [اینجارو](../../publishing.md) برای اطلاعات بیشتر.
|
||||
|
||||
### مشارکت 👩💻🧑💻
|
||||
|
||||
خوشحالم که میخواهید برونو را بهتر کنید. لطفا [راهنمای مشارکت را بررسی کنید](../contributing/contributing_fa.md).
|
||||
|
||||
حتی اگر نمیتوانید از طریق کدنویسی مشارکت کنید، در گزارش باگها و درخواست قابلیتهای جدید که به حل نیازهای شما کمک میکند تردید نکنید.
|
||||
|
||||
### نویسنده ها
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### در ارتباط باشید 🌐
|
||||
|
||||
[𝕏 (تویتر)](https://twitter.com/use_bruno) <br />
|
||||
[وبسایت](https://www.usebruno.com) <br />
|
||||
[دیسکورد](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[لینکدین](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### برند
|
||||
|
||||
**نام**
|
||||
|
||||
به فارسی برونو - `Bruno` یک علامت تجاری ثبتشده متعلق به [Anoop M D](https://www.helloanoop.com/)
|
||||
|
||||
**لوگو**
|
||||
|
||||
لوگو توسط [OpenMoji](https://openmoji.org/library/emoji-1F436/) ساخته شده است. مجوز: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### مجوز 📄
|
||||
|
||||
[MIT](../../license.md)
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE Opensource pour explorer et tester des APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Opensource IDE per esplorare e testare gli APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - API の検証・動作テストのためのオープンソース IDE.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
@@ -43,6 +43,13 @@ Bruno はオフラインのみで利用できます。Bruno にクラウド同
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### ゴールデンエディション ✨
|
||||
|
||||
機能のほとんどが無料で使用でき、オープンソースとなっています。
|
||||
私たちは[オープンソースの原則と長期的な維持](https://github.com/usebruno/bruno/discussions/269)の間でうまくバランスを取ろうと努力しています。
|
||||
|
||||
[ゴールデンエディション](https://www.usebruno.com/pricing)を **19 ドル** (買い切り)で購入できます!
|
||||
|
||||
### インストール方法
|
||||
|
||||
Bruno は[私たちのウェブサイト](https://www.usebruno.com/downloads)からバイナリをダウンロードできます。Mac, Windows, Linux に対応しています。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
@@ -43,6 +43,12 @@
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### ოქროს გამოცემა ✨
|
||||
|
||||
მთავარი ფუნქციების უმეტესობა უფასოა და ღია წყაროა. ჩვენ ვცდილობთ ჰარმონიული ბალანსის დაცვას [ღია წყაროების პრინციპებსა და მდგრადობას შორის](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
თქვენ შეგიძლიათ შეიძინოთ [ოქროს გამოცემა](https://www.usebruno.com/pricing) ერთჯერადი გადახდით **19 დოლარად**! <br/>
|
||||
|
||||
### ინსტალაცია
|
||||
|
||||
ბრუნო ხელმისაწვდომია როგორც ბინარული ჩამოტვირთვა [ჩვენ的网站上](https://www.usebruno.com/downloads) Mac-ის, Windows-ისა და Linux-ისთვის.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - API 탐색 및 테스트를 위한 오픈소스 IDE.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Open source IDE voor het verkennen en testen van API's.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
@@ -26,6 +26,13 @@ Bruno is uitsluitend offline. Er zijn geen plannen om ooit cloud-synchronisatie
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
De meeste van onze functies zijn gratis en open source.
|
||||
We streven naar een harmonieuze balans tussen [open-source principes en duurzaamheid](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
Je kunt de [Golden Edition](https://www.usebruno.com/pricing) kopen voor een eenmalige betaling van **$19**! <br/>
|
||||
|
||||
### Installatie
|
||||
|
||||
Bruno is beschikbaar als binaire download [op onze website](https://www.usebruno.com/downloads) voor Mac, Windows en Linux.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Otwartoźródłowe IDE do eksploracji i testów APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE de código aberto para explorar e testar APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
@@ -41,6 +41,13 @@ Bruno é totalmente offline. Não há planos de adicionar sincronização em nuv
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
A grande maioria dos nossos recursos são gratuitos e de código aberto.
|
||||
Nós nos esforçamos para encontrar um equilíbrio harmônico entre [princípios de código aberto e sustentabilidade](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
Você pode pré encomendar o plano [Golden Edition](https://www.usebruno.com/pricing) por ~~USD $19~~ **USD $9**! <br/>
|
||||
|
||||
### Instalação
|
||||
|
||||
Bruno está disponível para download como binário [em nosso site](https://www.usebruno.com/downloads) para Mac, Windows e Linux.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Mediu integrat de dezvoltare cu sursă deschisă pentru explorarea și testarea API-urilor.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE с открытым исходным кодом для изучения и тестирования API.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE із відкритим кодом для тестування та дослідження API
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - 探索和測試 API 的開源 IDE 工具
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
267
eslint.config.js
@@ -1,32 +1,20 @@
|
||||
// eslint.config.js
|
||||
const { defineConfig } = require('eslint/config');
|
||||
const globals = require('globals');
|
||||
const { defineConfig } = require("eslint/config");
|
||||
const globals = require("globals");
|
||||
const { fixupPluginRules } = require('@eslint/compat');
|
||||
const eslintPluginDiff = require('eslint-plugin-diff');
|
||||
|
||||
let stylistic;
|
||||
|
||||
const runESMImports = async () => {
|
||||
stylistic = await import('@stylistic/eslint-plugin').then((d) => d.default);
|
||||
stylistic = await import('@stylistic/eslint-plugin').then(d => d.default);
|
||||
};
|
||||
|
||||
module.exports = runESMImports().then(() => defineConfig([
|
||||
// Global ignores - must be a standalone object with ONLY ignores
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules/**/*',
|
||||
'**/dist/**/*',
|
||||
'**/*.bru',
|
||||
'packages/bruno-js/src/sandbox/bundle-browser-rollup.js',
|
||||
'packages/bruno-app/public/static/**/*',
|
||||
'packages/bruno-app/.next/**/*',
|
||||
'packages/bruno-electron/web/**/*'
|
||||
]
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'diff': fixupPluginRules(eslintPluginDiff),
|
||||
'@stylistic': stylistic
|
||||
'@stylistic': stylistic,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
@@ -38,7 +26,6 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
files: [
|
||||
'./eslint.config.js',
|
||||
'tests/**/*.{ts,js}',
|
||||
'playwright/**/*.{js,ts}',
|
||||
'packages/bruno-app/**/*.{js,jsx,ts}',
|
||||
'packages/bruno-app/src/test-utils/mocks/codemirror.js',
|
||||
'packages/bruno-cli/**/*.js',
|
||||
@@ -46,19 +33,18 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
'packages/bruno-converters/**/*.js',
|
||||
'packages/bruno-electron/**/*.js',
|
||||
'packages/bruno-filestore/**/*.ts',
|
||||
'packages/bruno-schema-types/**/*.ts',
|
||||
'packages/bruno-js/**/*.js',
|
||||
'packages/bruno-lang/**/*.js',
|
||||
'packages/bruno-requests/**/*.ts',
|
||||
'packages/bruno-requests/**/*.js',
|
||||
'packages/bruno-tests/**/*.{js,ts}'
|
||||
],
|
||||
processor: 'diff/diff',
|
||||
rules: {
|
||||
...stylistic.configs.customize({
|
||||
indent: 2,
|
||||
quotes: 'single',
|
||||
semi: true,
|
||||
jsx: true
|
||||
jsx: true,
|
||||
}).rules,
|
||||
'@stylistic/comma-dangle': ['error', 'never'],
|
||||
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
||||
@@ -66,9 +52,9 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
'@stylistic/curly-newline': ['error', {
|
||||
multiline: true,
|
||||
minElements: 2,
|
||||
consistent: true
|
||||
consistent: true,
|
||||
}],
|
||||
'@stylistic/function-paren-newline': ['off'],
|
||||
'@stylistic/function-paren-newline': ['error', 'never'],
|
||||
'@stylistic/array-bracket-spacing': ['error', 'never'],
|
||||
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
|
||||
'@stylistic/function-call-spacing': ['error', 'never'],
|
||||
@@ -76,14 +62,12 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
'@stylistic/padding-line-between-statements': ['off'],
|
||||
'@stylistic/semi-style': ['error', 'last'],
|
||||
'@stylistic/max-len': ['off'],
|
||||
'@stylistic/jsx-one-expression-per-line': ['off'],
|
||||
'@stylistic/max-statements-per-line': ['off'],
|
||||
'@stylistic/no-mixed-operators': ['off']
|
||||
}
|
||||
'@stylistic/jsx-one-expression-per-line': ['off']
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-app/**/*.{js,jsx,ts}'],
|
||||
ignores: ['**/*.config.js', '**/public/**/*'],
|
||||
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
|
||||
ignores: ["**/*.config.js", "**/public/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
@@ -96,127 +80,114 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
}
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
// It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks.
|
||||
files: ['packages/bruno-app/src/test-utils/mocks/codemirror.js'],
|
||||
files: ["packages/bruno-app/src/test-utils/mocks/codemirror.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
}
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
// Storybook config files use CommonJS with __dirname and module.exports
|
||||
files: ['packages/bruno-app/storybook/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
"no-undef": "error",
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-cli/**/*.js'],
|
||||
ignores: ['**/*.config.js'],
|
||||
files: ["packages/bruno-cli/**/*.js"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-common/**/*.ts'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
ecmaVersion: "latest"
|
||||
},
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './packages/bruno-common/tsconfig.json'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-converters/**/*.js'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
files: ["packages/bruno-common/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
...globals.jest,
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-common/tsconfig.json",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-converters/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error',
|
||||
'no-case-declarations': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-electron/**/*.js'],
|
||||
ignores: ['**/*.config.js', '**/web/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-filestore/**/*.ts'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './packages/bruno-filestore/tsconfig.json'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-js/**/*.js'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
files: ["packages/bruno-electron/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/web/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-filestore/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-filestore/tsconfig.json",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-js/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
@@ -227,65 +198,65 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
typeDetectGlobalObject: false
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-lang/**/*.js'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
files: ["packages/bruno-lang/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-requests/**/*.ts'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
files: ["packages/bruno-requests/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
...globals.jest,
|
||||
},
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './packages/bruno-requests/tsconfig.json'
|
||||
}
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-requests/tsconfig.json",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-requests/**/*.js'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
files: ["packages/bruno-requests/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
}
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
]));
|
||||
|
||||
16188
package-lock.json
generated
35
package.json
@@ -8,7 +8,6 @@
|
||||
"packages/bruno-common",
|
||||
"packages/bruno-converters",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-schema-types",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
@@ -25,51 +24,43 @@
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
|
||||
"@storybook/builder-webpack5": "^10.1.10",
|
||||
"@storybook/react": "^10.1.10",
|
||||
"@storybook/react-webpack5": "^10.1.10",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-diff": "^2.0.3",
|
||||
"fs-extra": "^11.1.1",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.2.0",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nano-staged": "^0.8.0",
|
||||
"playwright": "^1.51.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"storybook": "^10.1.10",
|
||||
"ts-jest": "^29.2.6"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
|
||||
"dev": "node ./scripts/dev.js",
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"watch": "npm run dev:watch",
|
||||
"dev:watch": "node ./scripts/dev-hot-reload.js",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
|
||||
"storybook": "npm run storybook --workspace=packages/bruno-app",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-requests": "npm run build --workspace=packages/bruno-requests",
|
||||
"build:bruno-filestore": "npm run build --workspace=packages/bruno-filestore",
|
||||
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:schema-types": "npm run build --workspace=packages/bruno-schema-types",
|
||||
"build:electron": "node ./scripts/build-electron.js",
|
||||
"build:electron:mac": "./scripts/build-electron.sh mac",
|
||||
"build:electron:win": "./scripts/build-electron.sh win",
|
||||
@@ -78,14 +69,12 @@
|
||||
"build:electron:rpm": "./scripts/build-electron.sh rpm",
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"watch:common": "npm run watch --workspace=packages/bruno-common",
|
||||
"watch:requests": "npm run watch --workspace=packages/bruno-requests",
|
||||
"test:codegen": "node playwright/codegen.ts",
|
||||
"test:e2e": "playwright test --project=default --project=system-pac",
|
||||
"test:e2e": "playwright test --project=default",
|
||||
"test:e2e:ssl": "playwright test --project=ssl",
|
||||
"test:e2e:auth": "playwright test --project=auth",
|
||||
"test:benchmark": "playwright test --config=playwright.benchmark.config.ts",
|
||||
"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",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"lint": "node --max_old_space_size=4096 $(npx which eslint)",
|
||||
"lint:fix": "node --max_old_space_size=4096 $(npx which eslint) --fix",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"nano-staged": {
|
||||
@@ -94,17 +83,11 @@
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"axios": "1.16.0",
|
||||
"rollup": "3.30.0",
|
||||
"pbkdf2": "3.1.5",
|
||||
"rollup": "3.29.5",
|
||||
"electron-store": {
|
||||
"conf": {
|
||||
"json-schema-typed": "8.0.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.17.1",
|
||||
"git-url-parse": "^14.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": [["styled-components", { "ssr": true }]]
|
||||
}
|
||||
4
packages/bruno-app/.gitignore
vendored
@@ -22,7 +22,6 @@ build
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.log
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
@@ -34,5 +33,4 @@ yarn-error.log*
|
||||
.next/
|
||||
dist/
|
||||
|
||||
.env
|
||||
storybook-static/
|
||||
.env
|
||||
7
packages/bruno-app/.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -6,4 +6,4 @@ module.exports = {
|
||||
}]
|
||||
],
|
||||
plugins: ['babel-plugin-styled-components']
|
||||
};
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
module.exports = {
|
||||
rootDir: '.',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': 'babel-jest'
|
||||
'^.+\\.[jt]sx?$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/'
|
||||
"/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/",
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^assets/(.*)$': '<rootDir>/src/assets/$1',
|
||||
@@ -22,9 +22,9 @@ module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['@testing-library/jest-dom'],
|
||||
setupFiles: [
|
||||
'<rootDir>/jest.setup.js'
|
||||
'<rootDir>/jest.setup.js',
|
||||
],
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/*.spec.[jt]s?(x)'
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -1,19 +1,3 @@
|
||||
global.ResizeObserver = class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn()
|
||||
}))
|
||||
});
|
||||
|
||||
jest.mock('nanoid', () => {
|
||||
return {
|
||||
nanoid: () => {}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"build": "rsbuild build -m production",
|
||||
"preview": "rsbuild preview",
|
||||
"test": "jest",
|
||||
"storybook": "storybook dev -p 6006 --config-dir storybook",
|
||||
"build-storybook": "storybook build --config-dir storybook"
|
||||
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
@@ -21,17 +21,12 @@
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"classnames": "^2.3.1",
|
||||
"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",
|
||||
"fast-json-format": "~0.4.0",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -39,25 +34,21 @@
|
||||
"github-markdown-css": "^5.2.0",
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "4.2.0",
|
||||
"hexy": "^0.3.5",
|
||||
"graphql-request": "^3.7.0",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsesc": "^3.0.2",
|
||||
"jshint": "^2.13.6",
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"jsonschema": "^1.5.0",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-replace-link": "^1.2.0",
|
||||
"mime-types": "^3.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -65,10 +56,9 @@
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "4.4.168",
|
||||
"platform": "^1.3.6",
|
||||
"polished": "^4.3.1",
|
||||
"posthog-node": "4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.14.1",
|
||||
"qs": "^6.11.0",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "19.0.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
@@ -83,27 +73,23 @@
|
||||
"react-player": "^2.16.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"react-virtuoso": "^4.18.1",
|
||||
"sass": "^1.46.0",
|
||||
"semver": "^7.7.1",
|
||||
"shell-quote": "^1.8.4",
|
||||
"shell-quote": "^1.8.3",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"swagger-ui-react": "^5.31.0",
|
||||
"system": "^2.0.1",
|
||||
"url": "^0.11.3",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
|
||||
713
packages/bruno-app/public/static/diff2Html.min.css
vendored
@@ -1,713 +0,0 @@
|
||||
: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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
const darkTheme = {
|
||||
'brand': '#546de5',
|
||||
'text': 'rgb(52 52 52)',
|
||||
brand: '#546de5',
|
||||
text: 'rgb(52 52 52)',
|
||||
'primary-text': '#ffffff',
|
||||
'primary-theme': '#1e1e1e',
|
||||
'secondary-text': '#929292',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const lightTheme = {
|
||||
'brand': '#546de5',
|
||||
'text': 'rgb(52 52 52)',
|
||||
brand: '#546de5',
|
||||
text: 'rgb(52 52 52)',
|
||||
'primary-text': 'rgb(52 52 52)',
|
||||
'primary-theme': '#ffffff',
|
||||
'secondary-text': '#929292',
|
||||
|
||||
@@ -38,9 +38,6 @@ export default defineConfig({
|
||||
dynamicImportMode: "eager",
|
||||
},
|
||||
},
|
||||
rules: [
|
||||
{ test: /\.md$/, type: 'asset/source' }
|
||||
]
|
||||
},
|
||||
ignoreWarnings: [
|
||||
(warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser')
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 10;
|
||||
|
||||
.ai-assist-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
opacity: 1;
|
||||
color: ${(props) => props.theme.colors.accent};
|
||||
background: ${(props) => props.theme.colors.accent}10;
|
||||
border-color: ${(props) => props.theme.input.border};
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${(props) => props.theme.colors.accent}55;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-assist-popup {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
width: 360px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
border-radius: ${(props) => props.theme.border.radius.md};
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.input.border};
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.text};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.colors.accent};
|
||||
}
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.input.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.popup-input {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
line-height: 1.4;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
background: ${(props) => props.theme.input.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: ${(props) => props.theme.input.focusBorder};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.suggestion-chip {
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
border-radius: 999px;
|
||||
background: ${(props) => props.theme.input.bg};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: ${(props) => props.theme.text};
|
||||
border-color: ${(props) => props.theme.colors.accent}80;
|
||||
background: ${(props) => props.theme.colors.accent}10;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-error {
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background: ${(props) => props.theme.colors.bg.danger}15;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid ${(props) => props.theme.input.border};
|
||||
}
|
||||
|
||||
.popup-hint {
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.popup-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid ${(props) => props.theme.input.border};
|
||||
border-top-color: ${(props) => props.theme.colors.accent};
|
||||
border-radius: 50%;
|
||||
animation: ai-assist-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes ai-assist-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.btn-generate {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
border: 1px solid ${(props) => props.theme.colors.accent};
|
||||
background: ${(props) => props.theme.colors.accent};
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.preview-code {
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
padding: 8px 10px;
|
||||
font-family: ${(props) => props.theme.font.monospace || 'monospace'};
|
||||
font-size: 11.5px;
|
||||
line-height: 1.5;
|
||||
color: ${(props) => props.theme.text};
|
||||
background: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.preview-modes {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.preview-mode-btn {
|
||||
padding: 2px 6px;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.text};
|
||||
border-color: ${(props) => props.theme.input.border};
|
||||
background: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,232 +0,0 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import get from 'lodash/get';
|
||||
import { IconStars, IconX, IconArrowBackUp } from '@tabler/icons';
|
||||
import { aiGenerateScript } from 'utils/ai';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const SUGGESTIONS = {
|
||||
'tests': [
|
||||
{ label: 'Status 200', prompt: 'Add a test asserting the response status code is 200' },
|
||||
{ label: 'JSON body', prompt: 'Add tests validating the JSON response body structure and key fields' },
|
||||
{ label: 'Headers', prompt: 'Add a test checking the content-type response header' },
|
||||
{ label: 'Response time', prompt: 'Add a test asserting the response time is below 1000ms' }
|
||||
],
|
||||
'pre-request': [
|
||||
{ label: 'Auth header', prompt: 'Set an Authorization header from an environment token variable' },
|
||||
{ label: 'Timestamp', prompt: 'Set a variable named "timestamp" containing the current epoch ms' },
|
||||
{ label: 'Random ID', prompt: 'Set a variable named "requestId" containing a random UUID-style id' }
|
||||
],
|
||||
'post-response': [
|
||||
{ label: 'Save token', prompt: 'Extract a token from the response body and save it to an environment variable' },
|
||||
{ label: 'Save id', prompt: 'Extract the primary id from the response body and save it to a variable' },
|
||||
{ label: 'Log response', prompt: 'Log the response status and a short summary of the body' }
|
||||
]
|
||||
};
|
||||
|
||||
const TITLES = {
|
||||
'tests': 'Generate Tests',
|
||||
'pre-request': 'Generate Pre-Request Script',
|
||||
'post-response': 'Generate Post-Response Script'
|
||||
};
|
||||
|
||||
const isValidType = (t) => SUGGESTIONS[t] !== undefined;
|
||||
|
||||
const AIAssist = ({ scriptType, currentScript, requestContext, onApply }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [generated, setGenerated] = useState(null);
|
||||
const buttonRef = useRef(null);
|
||||
|
||||
const focusOnMount = useCallback((el) => {
|
||||
el?.focus();
|
||||
}, []);
|
||||
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const isAiEnabled = get(preferences, 'ai.enabled', false);
|
||||
|
||||
const suggestions = useMemo(() => SUGGESTIONS[scriptType] || [], [scriptType]);
|
||||
const title = TITLES[scriptType] || 'Generate with AI';
|
||||
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const attachPopup = useCallback((el) => {
|
||||
if (!el) return undefined;
|
||||
const onDocMouseDown = (e) => {
|
||||
if (!el.contains(e.target) && !buttonRef.current?.contains(e.target)) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'Escape') close();
|
||||
};
|
||||
document.addEventListener('mousedown', onDocMouseDown);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDocMouseDown);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
const handleGenerate = useCallback(
|
||||
async (overridePrompt) => {
|
||||
const text = (overridePrompt ?? prompt).trim();
|
||||
if (!text || isLoading) return;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await aiGenerateScript({
|
||||
scriptType,
|
||||
prompt: text,
|
||||
currentScript: currentScript || '',
|
||||
requestContext
|
||||
});
|
||||
if (result?.error) {
|
||||
setError(result.error);
|
||||
return;
|
||||
}
|
||||
if (result?.content) {
|
||||
setGenerated(result.content);
|
||||
} else {
|
||||
setError('No content was generated. Try rephrasing your prompt.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err?.message || 'Failed to generate script');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[prompt, isLoading, scriptType, currentScript, requestContext]
|
||||
);
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
if (generated == null) return;
|
||||
onApply(generated);
|
||||
setGenerated(null);
|
||||
setPrompt('');
|
||||
close();
|
||||
}, [generated, onApply, close]);
|
||||
|
||||
const handleBackToPrompt = useCallback(() => {
|
||||
setGenerated(null);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
if (!isAiEnabled || !isValidType(scriptType)) return null;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
className={`ai-assist-trigger ${isOpen ? 'open' : ''}`}
|
||||
onClick={() => setIsOpen((v) => !v)}
|
||||
title={title}
|
||||
type="button"
|
||||
aria-label={title}
|
||||
>
|
||||
<IconStars size={14} strokeWidth={1.75} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div ref={attachPopup} className="ai-assist-popup" role="dialog" aria-label={title}>
|
||||
<div className="popup-header">
|
||||
<span className="popup-title">
|
||||
<IconStars size={12} strokeWidth={1.75} />
|
||||
{title}
|
||||
</span>
|
||||
<button className="popup-close" onClick={close} type="button" aria-label="Close">
|
||||
<IconX size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{generated == null ? (
|
||||
<>
|
||||
<div className="popup-body">
|
||||
<textarea
|
||||
ref={focusOnMount}
|
||||
className="popup-input"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
handleGenerate();
|
||||
}
|
||||
}}
|
||||
placeholder="Describe what you want to generate..."
|
||||
rows={3}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
{!isLoading && !prompt && suggestions.length > 0 && (
|
||||
<div className="popup-suggestions">
|
||||
{suggestions.map((s) => (
|
||||
<button
|
||||
key={s.label}
|
||||
className="suggestion-chip"
|
||||
type="button"
|
||||
onClick={() => handleGenerate(s.prompt)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div className="popup-error">{error}</div>}
|
||||
</div>
|
||||
|
||||
<div className="popup-footer">
|
||||
{isLoading ? (
|
||||
<span className="popup-loading">
|
||||
<span className="loading-spinner" />
|
||||
Generating...
|
||||
</span>
|
||||
) : (
|
||||
<span className="popup-hint">⌘ + Enter to generate</span>
|
||||
)}
|
||||
<button
|
||||
className="btn-generate"
|
||||
type="button"
|
||||
onClick={() => handleGenerate()}
|
||||
disabled={!prompt.trim() || isLoading}
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="popup-body">
|
||||
<div className="preview-section">
|
||||
<span className="preview-label">Preview · replaces current script</span>
|
||||
<pre className="preview-code">{generated}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="popup-footer">
|
||||
<button className="btn-secondary" type="button" onClick={handleBackToPrompt}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
<IconArrowBackUp size={12} /> Back
|
||||
</span>
|
||||
</button>
|
||||
<button className="btn-generate" type="button" onClick={handleApply}>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIAssist;
|
||||
@@ -4,7 +4,7 @@ import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrappe
|
||||
|
||||
const AccordionContext = createContext();
|
||||
|
||||
const Accordion = ({ children, defaultIndex, dataTestId }) => {
|
||||
const Accordion = ({ children, defaultIndex }) => {
|
||||
const [openIndex, setOpenIndex] = useState(defaultIndex);
|
||||
|
||||
const toggleItem = (index) => {
|
||||
@@ -13,7 +13,7 @@ const Accordion = ({ children, defaultIndex, dataTestId }) => {
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
|
||||
<div data-testid={dataTestId}>{children}</div>
|
||||
<div>{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
const yamlPlugin = (cm) => {
|
||||
cm.defineMode('yaml', function () {
|
||||
var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];
|
||||
var keywordRegex = new RegExp('\\b((' + cons.join(')|(') + '))$', 'i');
|
||||
|
||||
return {
|
||||
token: function (stream, state) {
|
||||
var ch = stream.peek();
|
||||
var esc = state.escaped;
|
||||
state.escaped = false;
|
||||
/* comments */
|
||||
if (ch == '#' && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) {
|
||||
stream.skipToEnd();
|
||||
return 'comment';
|
||||
}
|
||||
|
||||
if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)) return 'string';
|
||||
|
||||
if (state.literal && stream.indentation() > state.keyCol) {
|
||||
stream.skipToEnd();
|
||||
return 'string';
|
||||
} else if (state.literal) {
|
||||
state.literal = false;
|
||||
}
|
||||
if (stream.sol()) {
|
||||
state.keyCol = 0;
|
||||
state.pair = false;
|
||||
state.pairStart = false;
|
||||
/* document start */
|
||||
if (stream.match('---')) {
|
||||
return 'def';
|
||||
}
|
||||
/* document end */
|
||||
if (stream.match('...')) {
|
||||
return 'def';
|
||||
}
|
||||
/* array list item */
|
||||
if (stream.match(/\s*-\s+/)) {
|
||||
return 'meta';
|
||||
}
|
||||
}
|
||||
/* inline pairs/lists */
|
||||
if (stream.match(/^(\{|\}|\[|\])/)) {
|
||||
if (ch == '{') state.inlinePairs++;
|
||||
else if (ch == '}') state.inlinePairs--;
|
||||
else if (ch == '[') state.inlineList++;
|
||||
else state.inlineList--;
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* list separator */
|
||||
if (state.inlineList > 0 && !esc && ch == ',') {
|
||||
stream.next();
|
||||
return 'meta';
|
||||
}
|
||||
/* pairs separator */
|
||||
if (state.inlinePairs > 0 && !esc && ch == ',') {
|
||||
state.keyCol = 0;
|
||||
state.pair = false;
|
||||
state.pairStart = false;
|
||||
stream.next();
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* start of value of a pair */
|
||||
if (state.pairStart) {
|
||||
/* block literals */
|
||||
if (stream.match(/^\s*(\||\>)\s*/)) {
|
||||
state.literal = true;
|
||||
return 'meta';
|
||||
}
|
||||
/* references */
|
||||
if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) {
|
||||
return 'variable-2';
|
||||
}
|
||||
/* numbers */
|
||||
if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) {
|
||||
return 'number';
|
||||
}
|
||||
if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) {
|
||||
return 'number';
|
||||
}
|
||||
/* keywords */
|
||||
if (stream.match(keywordRegex)) {
|
||||
return 'keyword';
|
||||
}
|
||||
}
|
||||
|
||||
/* pairs (associative arrays) -> key */
|
||||
if (
|
||||
!state.pair
|
||||
&& stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)
|
||||
) {
|
||||
state.pair = true;
|
||||
state.keyCol = stream.indentation();
|
||||
return 'atom';
|
||||
}
|
||||
if (state.pair && stream.match(/^:\s*/)) {
|
||||
state.pairStart = true;
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* nothing found, continue */
|
||||
state.pairStart = false;
|
||||
state.escaped = ch == '\\';
|
||||
stream.next();
|
||||
return null;
|
||||
},
|
||||
startState: function () {
|
||||
return {
|
||||
pair: false,
|
||||
pairStart: false,
|
||||
keyCol: 0,
|
||||
inlinePairs: 0,
|
||||
inlineList: 0,
|
||||
literal: false,
|
||||
escaped: false
|
||||
};
|
||||
},
|
||||
lineComment: '#',
|
||||
fold: 'indent'
|
||||
};
|
||||
});
|
||||
|
||||
cm.defineMIME('text/x-yaml', 'yaml');
|
||||
cm.defineMIME('text/yaml', 'yaml');
|
||||
};
|
||||
|
||||
export default yamlPlugin;
|
||||
@@ -1,77 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
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;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog {
|
||||
overflow: visible;
|
||||
input {
|
||||
background: transparent;
|
||||
border: 1px solid #d3d6db;
|
||||
outline: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #d2d7db;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.CodeMirror.cm-s-monokai {
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property,
|
||||
.cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-string {
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number {
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom {
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid {
|
||||
color: ${(props) => props.theme.codemirror.variable.valid};
|
||||
}
|
||||
.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;
|
||||
@@ -1,128 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import yamlPlugin from './Plugins/Yaml/index';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
this.lintOptions = {
|
||||
esversion: 11,
|
||||
expr: true,
|
||||
asi: true
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
switch (this.props.mode) {
|
||||
case 'yaml':
|
||||
// YAML linting and hightlighting plugin
|
||||
yamlPlugin(CodeMirror);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const editor = (this.editor = CodeMirror(this._node, {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
tabSize: 2,
|
||||
mode: this.props.mode || 'application/text',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
lint: this.lintOptions,
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: 'overlay',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
extraKeys: {
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
'Tab': function (cm) {
|
||||
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
|
||||
? cm.execCommand('indentMore')
|
||||
: cm.replaceSelection(' ', 'end');
|
||||
},
|
||||
'Shift-Tab': 'indentLess',
|
||||
'Ctrl-Space': 'autocomplete',
|
||||
'Cmd-Space': 'autocomplete',
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll'
|
||||
}
|
||||
}));
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this.ignoreChangeEvent = true;
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
}
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.editor) {
|
||||
this.editor.refresh();
|
||||
}
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full w-full graphiql-container"
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
this.cachedValue = this.editor.getValue();
|
||||
if (this.props.onEdit) {
|
||||
this.props.onEdit(this.cachedValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,872 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.swagger-root {
|
||||
height: calc(100vh - 7rem);
|
||||
border-left: solid 1px ${(props) => props.theme.border.border1};
|
||||
overflow-y: auto;
|
||||
background: ${(props) => props.theme.bg};
|
||||
padding-bottom: 20px;
|
||||
|
||||
/* ── 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};
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,75 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { serializeBody } from './serializeBody';
|
||||
|
||||
const serializeHeaders = (headers) => {
|
||||
if (!headers) return {};
|
||||
if (typeof headers.entries === 'function') {
|
||||
const out = {};
|
||||
for (const [k, v] of headers.entries()) out[k] = v;
|
||||
return out;
|
||||
}
|
||||
return { ...headers };
|
||||
};
|
||||
|
||||
const proxiedFetch = async (url, options = {}) => {
|
||||
const result = await window.ipcRenderer.invoke('renderer:swagger-fetch', {
|
||||
url,
|
||||
method: options.method || 'GET',
|
||||
headers: serializeHeaders(options.headers),
|
||||
body: serializeBody(options.body)
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
const err = new TypeError(result.message);
|
||||
err.code = result.code;
|
||||
throw err;
|
||||
}
|
||||
|
||||
// The Response constructor throws if a null-body status carries a body.
|
||||
const nullBodyStatus = [101, 204, 205, 304].includes(result.status);
|
||||
const bodyBytes = !nullBodyStatus && result.bodyBase64
|
||||
? Uint8Array.from(atob(result.bodyBase64), (c) => c.charCodeAt(0))
|
||||
: null;
|
||||
|
||||
// Build Headers manually so multi-value response headers (e.g. Set-Cookie,
|
||||
// which axios returns as string[]) end up as repeated entries rather than
|
||||
// joined via toString(). new Headers({ 'set-cookie': ['a','b'] }) coerces
|
||||
// the array to "a,b", which is invalid Set-Cookie syntax.
|
||||
const responseHeaders = new Headers();
|
||||
for (const [name, value] of Object.entries(result.headers || {})) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => responseHeaders.append(name, String(v)));
|
||||
} else if (value != null) {
|
||||
responseHeaders.append(name, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(bodyBytes, {
|
||||
status: result.status,
|
||||
statusText: result.statusText,
|
||||
headers: responseHeaders
|
||||
});
|
||||
};
|
||||
|
||||
const requestInterceptor = (req) => {
|
||||
req.userFetch = proxiedFetch;
|
||||
return req;
|
||||
};
|
||||
|
||||
const Swagger = ({ spec, onComplete }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="swagger-root w-full">
|
||||
<SwaggerUI
|
||||
spec={spec}
|
||||
onComplete={onComplete}
|
||||
requestInterceptor={requestInterceptor}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Swagger);
|
||||
@@ -1,83 +0,0 @@
|
||||
// Serializes a SwaggerUI fetch body for transport across the renderer ↔ main
|
||||
// IPC bridge in `renderer:swagger-fetch`. Only types that survive Electron's
|
||||
// structured-clone serialization (and that our axios bridge knows how to send
|
||||
// as an HTTP body) are supported. Multipart / binary types throw so the user
|
||||
// gets a clear message in the SwaggerUI response panel instead of a silent
|
||||
// failure.
|
||||
|
||||
const detectBodyType = (body) => {
|
||||
if (body == null) return 'null';
|
||||
if (typeof body === 'string') return 'string';
|
||||
if (typeof FormData !== 'undefined' && body instanceof FormData) return 'FormData';
|
||||
if (typeof File !== 'undefined' && body instanceof File) return 'File';
|
||||
if (typeof Blob !== 'undefined' && body instanceof Blob) return 'Blob';
|
||||
if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) return 'URLSearchParams';
|
||||
if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) return 'ArrayBuffer';
|
||||
if (ArrayBuffer.isView && ArrayBuffer.isView(body)) return body.constructor?.name || 'TypedArray';
|
||||
if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) return 'ReadableStream';
|
||||
return typeof body;
|
||||
};
|
||||
|
||||
export const UNSUPPORTED_BODY_TYPE_CODE = 'UNSUPPORTED_BODY_TYPE';
|
||||
|
||||
// Mapping from Web API class name (the raw detected type) to the user-facing
|
||||
// subject used in the error message. SwaggerUI itself supports these body
|
||||
// types fine; the limitation is Bruno's renderer↔main IPC bridge, not Swagger.
|
||||
const BODY_TYPE_LABEL_MAP = {
|
||||
File: 'File upload',
|
||||
Blob: 'Binary file upload',
|
||||
FormData: 'Multipart form data',
|
||||
ArrayBuffer: 'Binary data',
|
||||
ReadableStream: 'Streaming upload'
|
||||
};
|
||||
|
||||
const mapBodyTypeToLabel = (typeName) => {
|
||||
if (BODY_TYPE_LABEL_MAP[typeName]) return BODY_TYPE_LABEL_MAP[typeName];
|
||||
// TypedArrays (Uint8Array, Float32Array, etc.) share a label.
|
||||
if (typeof typeName === 'string' && typeName.endsWith('Array')) return 'Binary data';
|
||||
return 'This request body type';
|
||||
};
|
||||
|
||||
export const UNSUPPORTED_BODY_MESSAGE = (typeName) =>
|
||||
`${mapBodyTypeToLabel(typeName)} via the Swagger Try-it-out panel isn't supported in Bruno yet. `
|
||||
+ `Supported body types: JSON, URL-encoded forms, plain text. `
|
||||
+ `Create a Bruno request to test this endpoint.`;
|
||||
|
||||
// Build a TypeError that carries the detected type as a property so downstream
|
||||
// catchers can branch on `err.code` / `err.bodyType` instead of regex-parsing
|
||||
// the message. `err.bodyType` keeps the raw Web API class name for diagnostics;
|
||||
// the user-visible message uses the friendly subject above.
|
||||
const unsupportedBodyError = (typeName) => {
|
||||
const err = new TypeError(UNSUPPORTED_BODY_MESSAGE(typeName));
|
||||
err.code = UNSUPPORTED_BODY_TYPE_CODE;
|
||||
err.bodyType = typeName;
|
||||
return err;
|
||||
};
|
||||
|
||||
export const serializeBody = (body) => {
|
||||
const typeName = detectBodyType(body);
|
||||
|
||||
switch (typeName) {
|
||||
case 'null':
|
||||
return undefined;
|
||||
case 'string':
|
||||
return body;
|
||||
case 'URLSearchParams':
|
||||
return body.toString();
|
||||
case 'FormData':
|
||||
case 'File':
|
||||
case 'Blob':
|
||||
case 'ArrayBuffer':
|
||||
case 'ReadableStream':
|
||||
throw unsupportedBodyError(typeName);
|
||||
default:
|
||||
// TypedArrays land here (Uint8Array, etc.) — also unsupported by the bridge.
|
||||
if (ArrayBuffer.isView && ArrayBuffer.isView(body)) {
|
||||
throw unsupportedBodyError(typeName);
|
||||
}
|
||||
// Plain objects, numbers, booleans — pass through. SwaggerUI rarely sends
|
||||
// these as body directly (it stringifies JSON before fetch), but keep the
|
||||
// path open rather than rejecting unexpectedly.
|
||||
return body;
|
||||
}
|
||||
};
|
||||
@@ -1,95 +0,0 @@
|
||||
import { serializeBody, UNSUPPORTED_BODY_MESSAGE, UNSUPPORTED_BODY_TYPE_CODE } from './serializeBody';
|
||||
|
||||
// Helper: invoke serializeBody and return the thrown error (or fail).
|
||||
const catchSerializeError = (body) => {
|
||||
try {
|
||||
serializeBody(body);
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
throw new Error('expected serializeBody to throw');
|
||||
};
|
||||
|
||||
describe('serializeBody', () => {
|
||||
describe('supported body types', () => {
|
||||
it('returns undefined for null', () => {
|
||||
expect(serializeBody(null)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for undefined', () => {
|
||||
expect(serializeBody(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns string bodies as-is', () => {
|
||||
expect(serializeBody('{"name":"doggie"}')).toBe('{"name":"doggie"}');
|
||||
expect(serializeBody('plain text')).toBe('plain text');
|
||||
});
|
||||
|
||||
it('stringifies URLSearchParams', () => {
|
||||
const params = new URLSearchParams({ a: '1', b: '2' });
|
||||
expect(serializeBody(params)).toBe('a=1&b=2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsupported body types (BRU-3300)', () => {
|
||||
it('throws TypeError for FormData using "Multipart form data" subject', () => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', new Blob(['x']));
|
||||
expect(() => serializeBody(fd)).toThrow(TypeError);
|
||||
expect(() => serializeBody(fd)).toThrow(/Multipart form data/);
|
||||
expect(() => serializeBody(fd)).toThrow(/Create a Bruno request/);
|
||||
});
|
||||
|
||||
it('throws TypeError for Blob using "Binary file upload" subject', () => {
|
||||
const blob = new Blob(['payload']);
|
||||
expect(() => serializeBody(blob)).toThrow(TypeError);
|
||||
expect(() => serializeBody(blob)).toThrow(/Binary file upload/);
|
||||
});
|
||||
|
||||
it('throws TypeError for File using "File upload" subject', () => {
|
||||
const file = new File(['payload'], 'test.txt', { type: 'text/plain' });
|
||||
expect(() => serializeBody(file)).toThrow(TypeError);
|
||||
expect(() => serializeBody(file)).toThrow(/File upload/);
|
||||
});
|
||||
|
||||
it('throws TypeError for ArrayBuffer using "Binary data" subject', () => {
|
||||
const buf = new ArrayBuffer(8);
|
||||
expect(() => serializeBody(buf)).toThrow(TypeError);
|
||||
expect(() => serializeBody(buf)).toThrow(/Binary data/);
|
||||
});
|
||||
|
||||
it('throws TypeError for TypedArray using "Binary data" subject', () => {
|
||||
const u8 = new Uint8Array([1, 2, 3]);
|
||||
expect(() => serializeBody(u8)).toThrow(TypeError);
|
||||
expect(() => serializeBody(u8)).toThrow(/Binary data/);
|
||||
});
|
||||
|
||||
it('message attributes the limitation to Bruno, not Swagger', () => {
|
||||
expect(UNSUPPORTED_BODY_MESSAGE('FormData')).toMatch(/isn't supported in Bruno yet/);
|
||||
});
|
||||
|
||||
it('message lists supported alternatives', () => {
|
||||
expect(UNSUPPORTED_BODY_MESSAGE('FormData')).toMatch(/JSON, URL-encoded forms, plain text/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error metadata preservation (Bijin review feedback)', () => {
|
||||
it('attaches err.code = UNSUPPORTED_BODY_TYPE so callers can branch programmatically', () => {
|
||||
const err = catchSerializeError(new FormData());
|
||||
expect(err.code).toBe(UNSUPPORTED_BODY_TYPE_CODE);
|
||||
expect(UNSUPPORTED_BODY_TYPE_CODE).toBe('UNSUPPORTED_BODY_TYPE');
|
||||
});
|
||||
|
||||
it('attaches err.bodyType naming the specific unsupported type', () => {
|
||||
expect(catchSerializeError(new FormData()).bodyType).toBe('FormData');
|
||||
expect(catchSerializeError(new Blob(['x'])).bodyType).toBe('Blob');
|
||||
expect(catchSerializeError(new File(['x'], 'a.txt')).bodyType).toBe('File');
|
||||
expect(catchSerializeError(new ArrayBuffer(4)).bodyType).toBe('ArrayBuffer');
|
||||
expect(catchSerializeError(new Uint8Array([1, 2])).bodyType).toBe('Uint8Array');
|
||||
});
|
||||
|
||||
it('thrown error is still a TypeError instance', () => {
|
||||
expect(catchSerializeError(new FormData())).toBeInstanceOf(TypeError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,123 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { IconDeviceFloppy, IconLoader2 } from '@tabler/icons';
|
||||
import CodeEditor from './FileEditor/CodeEditor/index';
|
||||
import Swagger from './Renderers/Swagger';
|
||||
import { useDragResize } from 'hooks/useDragResize';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 450;
|
||||
|
||||
/**
|
||||
* 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 (fn) Called with current editor content on save (editable mode only)
|
||||
* - leftPaneWidth (number|null) Persisted left pane width in px; null = use 50/50 default
|
||||
* - onLeftPaneWidthChange (fn) Persist the new width (called on mouseup / double-click / resize-clamp)
|
||||
*/
|
||||
const SpecViewer = ({ content, readOnly, onSave, leftPaneWidth, onLeftPaneWidthChange }) => {
|
||||
const { displayedTheme, theme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const [editorContent, setEditorContent] = useState(content);
|
||||
|
||||
useEffect(() => {
|
||||
setEditorContent(content);
|
||||
}, [content]);
|
||||
|
||||
const hasChanges = !readOnly && editorContent !== content;
|
||||
|
||||
const handleSave = () => {
|
||||
if (onSave) onSave(editorContent);
|
||||
};
|
||||
|
||||
const mainSectionRef = useRef(null);
|
||||
const { dragging, dragWidth, dragbarProps } = useDragResize({
|
||||
containerRef: mainSectionRef,
|
||||
width: leftPaneWidth,
|
||||
onWidthChange: onLeftPaneWidthChange,
|
||||
minLeft: MIN_LEFT_PANE_WIDTH,
|
||||
minRight: MIN_RIGHT_PANE_WIDTH
|
||||
});
|
||||
|
||||
const effectiveWidth = dragging ? dragWidth : leftPaneWidth;
|
||||
const leftPaneStyle = effectiveWidth != null
|
||||
? { width: `${effectiveWidth}px`, flexShrink: 0 }
|
||||
: { flex: '1 1 50%', minWidth: 0 };
|
||||
|
||||
const [swaggerReady, setSwaggerReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSwaggerReady(false);
|
||||
}, [content]);
|
||||
|
||||
const handleSwaggerComplete = useCallback(() => {
|
||||
// Double rAF: wait for one full paint cycle so Swagger is actually on screen
|
||||
// before hiding the loader — avoids a flash of unrendered content.
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => setSwaggerReady(true));
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={mainSectionRef}
|
||||
className={`main flex flex-grow pl-4 relative ${dragging ? 'dragging' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="api-spec-left-pane flex flex-grow relative h-full"
|
||||
style={leftPaneStyle}
|
||||
>
|
||||
<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 className="dragbar-wrapper" {...dragbarProps}>
|
||||
<div className="dragbar-handle" />
|
||||
</div>
|
||||
<div
|
||||
className="api-spec-right-pane relative"
|
||||
style={{ flex: '1 1 50%', minWidth: 0 }}
|
||||
>
|
||||
<div style={{ visibility: swaggerReady ? 'visible' : 'hidden', height: '100%' }}>
|
||||
<Swagger spec={content} onComplete={handleSwaggerComplete} />
|
||||
</div>
|
||||
{!swaggerReady && (
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center gap-2"
|
||||
style={{ background: theme.bg }}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2 opacity-70">
|
||||
<IconLoader2 size={20} className="animate-spin" />
|
||||
<span>Generating preview…</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecViewer;
|
||||
@@ -1,51 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.menu-icon {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
}
|
||||
|
||||
div.dropdown-item.menu-item {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.colors.bg.danger};
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.react-tooltip {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
section.main.dragging {
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
div.dragbar-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 10px;
|
||||
min-width: 10px;
|
||||
padding: 0;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
|
||||
div.dragbar-handle {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
|
||||
}
|
||||
|
||||
&:hover div.dragbar-handle {
|
||||
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { forwardRef, useRef, useCallback } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconFileCode, IconDots } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SpecViewer from './SpecViewer';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { openApiSpec, saveApiSpecToFile, updateApiSpecPanelLeftPaneWidth } from 'providers/ReduxStore/slices/apiSpec';
|
||||
import { useState } from 'react';
|
||||
import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const ApiSpecPanel = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [createApiSpecModalOpen, setCreateApiSpecModalOpen] = useState(false);
|
||||
|
||||
const { apiSpecs, activeApiSpecUid } = useSelector((state) => state.apiSpec);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
let apiSpec = find(apiSpecs, (c) => c.uid === activeApiSpecUid);
|
||||
const { filename, pathname, raw, uid, leftPaneWidth } = apiSpec || {};
|
||||
|
||||
const handleLeftPaneWidthChange = useCallback(
|
||||
(w) => {
|
||||
if (!uid) return;
|
||||
dispatch(updateApiSpecPanelLeftPaneWidth({ uid, leftPaneWidth: w }));
|
||||
},
|
||||
[dispatch, uid]
|
||||
);
|
||||
|
||||
if (!uid) {
|
||||
return <div className="p-4 opacity-50">API Spec not found!</div>;
|
||||
}
|
||||
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<IconDots size={22} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleOpenApiSpec = () => {
|
||||
dispatch(openApiSpec()).catch(
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the API spec')
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col flex-grow relative">
|
||||
{createApiSpecModalOpen ? <CreateApiSpec onClose={() => setCreateApiSpecModalOpen(false)} /> : null}
|
||||
<div className="p-3 mb-2 w-full flex flex-row justify-between grid grid-cols-3">
|
||||
<div className="flex flex-row justify-start gap-x-4 col-span-1">
|
||||
<div className="flex w-fit items-center cursor-pointer">
|
||||
<IconFileCode size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-semibold">API Designer</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full col-span-1 flex justify-center" title={pathname}>
|
||||
{filename}
|
||||
</div>
|
||||
<div className="menu-icon pr-2 col-span-1 flex justify-end">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setCreateApiSpecModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Create API Spec
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleOpenApiSpec();
|
||||
}}
|
||||
>
|
||||
Open API Spec
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<SpecViewer
|
||||
content={raw}
|
||||
onSave={(content) => dispatch(saveApiSpecToFile({ uid, content }))}
|
||||
leftPaneWidth={leftPaneWidth ?? null}
|
||||
onLeftPaneWidthChange={handleLeftPaneWidthChange}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiSpecPanel;
|
||||
@@ -1,15 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.shortcut {
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,154 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconMenu2 } from '@tabler/icons';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AppMenu = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'file',
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
id: 'open-collection',
|
||||
label: 'Open Collection',
|
||||
onClick: () => ipcRenderer?.invoke('renderer:open-collection')
|
||||
},
|
||||
{ type: 'divider', id: 'file-div-1' },
|
||||
{
|
||||
id: 'preferences',
|
||||
label: 'Preferences',
|
||||
rightSection: <span className="shortcut">Ctrl+,</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:open-preferences')
|
||||
},
|
||||
{ type: 'divider', id: 'file-div-2' },
|
||||
{
|
||||
id: 'quit',
|
||||
label: 'Quit',
|
||||
rightSection: <span className="shortcut">Alt+F4</span>,
|
||||
onClick: () => ipcRenderer?.send('renderer:window-close')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{
|
||||
id: 'undo',
|
||||
label: 'Undo',
|
||||
rightSection: <span className="shortcut">Ctrl+Z</span>,
|
||||
onClick: () => document.execCommand('undo')
|
||||
},
|
||||
{
|
||||
id: 'redo',
|
||||
label: 'Redo',
|
||||
rightSection: <span className="shortcut">Ctrl+Y</span>,
|
||||
onClick: () => document.execCommand('redo')
|
||||
},
|
||||
{ type: 'divider', id: 'edit-div-1' },
|
||||
{
|
||||
id: 'cut',
|
||||
label: 'Cut',
|
||||
rightSection: <span className="shortcut">Ctrl+X</span>,
|
||||
onClick: () => document.execCommand('cut')
|
||||
},
|
||||
{
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
rightSection: <span className="shortcut">Ctrl+C</span>,
|
||||
onClick: () => document.execCommand('copy')
|
||||
},
|
||||
{
|
||||
id: 'paste',
|
||||
label: 'Paste',
|
||||
rightSection: <span className="shortcut">Ctrl+V</span>,
|
||||
onClick: () => document.execCommand('paste')
|
||||
},
|
||||
{ type: 'divider', id: 'edit-div-2' },
|
||||
{
|
||||
id: 'select-all',
|
||||
label: 'Select All',
|
||||
rightSection: <span className="shortcut">Ctrl+A</span>,
|
||||
onClick: () => document.execCommand('selectAll')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'view',
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
id: 'toggle-devtools',
|
||||
label: 'Developer Tools',
|
||||
rightSection: <span className="shortcut">Ctrl+Shift+I</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:toggle-devtools')
|
||||
},
|
||||
{ type: 'divider', id: 'view-div-1' },
|
||||
{
|
||||
id: 'reset-zoom',
|
||||
label: 'Reset Zoom',
|
||||
rightSection: <span className="shortcut">Ctrl+0</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:reset-zoom')
|
||||
},
|
||||
{
|
||||
id: 'zoom-in',
|
||||
label: 'Zoom In',
|
||||
rightSection: <span className="shortcut">Ctrl++</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:zoom-in')
|
||||
},
|
||||
{
|
||||
id: 'zoom-out',
|
||||
label: 'Zoom Out',
|
||||
rightSection: <span className="shortcut">Ctrl+-</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:zoom-out')
|
||||
},
|
||||
{ type: 'divider', id: 'view-div-2' },
|
||||
{
|
||||
id: 'toggle-fullscreen',
|
||||
label: 'Full Screen',
|
||||
rightSection: <span className="shortcut">F11</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:toggle-fullscreen')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'help',
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
id: 'about',
|
||||
label: 'About Bruno',
|
||||
onClick: () => ipcRenderer?.invoke('renderer:open-about')
|
||||
},
|
||||
{
|
||||
id: 'documentation',
|
||||
label: 'Documentation',
|
||||
onClick: () => ipcRenderer?.invoke('renderer:open-docs')
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<MenuDropdown
|
||||
opened={isOpen}
|
||||
onChange={setIsOpen}
|
||||
placement="bottom-start"
|
||||
showTickMark={false}
|
||||
items={menuItems}
|
||||
>
|
||||
<ActionIcon label="Menu" size="lg">
|
||||
<IconMenu2 size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
</MenuDropdown>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppMenu;
|
||||
@@ -1,255 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
|
||||
.titlebar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
padding-left: 70px; /* Space for macOS window controls */
|
||||
transition: padding-left 0.15s ease;
|
||||
}
|
||||
|
||||
/* When in full screen, no traffic lights so reduce padding */
|
||||
&.fullscreen .titlebar-content {
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
/* Remove drag region from interactive elements */
|
||||
.workspace-name-container,
|
||||
.dropdown-item,
|
||||
.home-button,
|
||||
.dropdown,
|
||||
button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Left section */
|
||||
.titlebar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* When in full screen, no traffic lights so remove margin-left */
|
||||
&.fullscreen .titlebar-left {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
/* Workspace Name Dropdown Trigger */
|
||||
.workspace-name-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
flex-shrink: 0;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Center section - Bruno branding */
|
||||
.titlebar-center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
pointer-events: none;
|
||||
|
||||
.bruno-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.text};
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right section */
|
||||
.titlebar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* App action buttons container */
|
||||
.titlebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Workspace Dropdown Styles */
|
||||
.workspace-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 10px !important;
|
||||
margin: 0 !important;
|
||||
|
||||
&.active {
|
||||
.check-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.pin-btn:not(.pinned) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.workspace-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
||||
opacity: 0;
|
||||
|
||||
&.pinned {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.dropdown.hoverBg};
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Adjust for non-macOS platforms */
|
||||
&:not(.os-mac) .titlebar-content {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* Windows-specific styles */
|
||||
&.os-windows .titlebar-content {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.os-windows .titlebar-left {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
&.os-linux .titlebar-content {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.os-linux .titlebar-left {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.app-menu {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Custom window control buttons for Windows - always interactive, above modal overlay */
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 36px;
|
||||
margin-left: 8px;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.window-control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
&.close:hover {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,357 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconCheck, IconChevronDown, IconFolder, IconHome, IconPin, IconPinned, IconPlus, IconDownload, IconSettings, IconMinus, IconSquare, IconX, IconCopy } from '@tabler/icons';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { savePreferences, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';
|
||||
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';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
|
||||
import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';
|
||||
import ImportWorkspace from 'components/WorkspaceSidebar/ImportWorkspace';
|
||||
|
||||
import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';
|
||||
import AppMenu from './AppMenu';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle';
|
||||
import { isMacOS, isWindowsOS, isLinuxOS } from 'utils/common/platform';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const getOsClass = () => {
|
||||
if (isMacOS()) return 'os-mac';
|
||||
if (isWindowsOS()) return 'os-windows';
|
||||
if (isLinuxOS()) return 'os-linux';
|
||||
return 'os-other';
|
||||
};
|
||||
|
||||
// Helper to get display name for workspace
|
||||
export const getWorkspaceDisplayName = (name) => {
|
||||
if (!name) return 'Untitled Workspace';
|
||||
return name;
|
||||
};
|
||||
|
||||
const AppTitleBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const osClass = getOsClass();
|
||||
const isWindows = osClass === 'os-windows';
|
||||
const isLinux = osClass === 'os-linux';
|
||||
const showWindowControls = isWindows || isLinux;
|
||||
|
||||
// Listen for fullscreen changes
|
||||
useEffect(() => {
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) return;
|
||||
|
||||
ipcRenderer.invoke('renderer:window-is-fullscreen')
|
||||
.then((fullscreen) => {
|
||||
setIsFullScreen(fullscreen);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error getting initial fullscreen state:', error);
|
||||
});
|
||||
|
||||
const removeEnterFullScreenListener = ipcRenderer.on('main:enter-full-screen', () => {
|
||||
setIsFullScreen(true);
|
||||
});
|
||||
|
||||
const removeLeaveFullScreenListener = ipcRenderer.on('main:leave-full-screen', () => {
|
||||
setIsFullScreen(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeEnterFullScreenListener();
|
||||
removeLeaveFullScreenListener();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showWindowControls) return;
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) return;
|
||||
|
||||
ipcRenderer.invoke('renderer:window-is-maximized')
|
||||
.then((maximized) => {
|
||||
setIsMaximized(maximized);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error getting initial maximized state:', error);
|
||||
});
|
||||
|
||||
const removeMaximizedListener = ipcRenderer.on('main:window-maximized', () => {
|
||||
setIsMaximized(true);
|
||||
});
|
||||
|
||||
const removeUnmaximizedListener = ipcRenderer.on('main:window-unmaximized', () => {
|
||||
setIsMaximized(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeMaximizedListener();
|
||||
removeUnmaximizedListener();
|
||||
};
|
||||
}, [showWindowControls]);
|
||||
|
||||
const handleMinimize = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-minimize');
|
||||
}, []);
|
||||
|
||||
const handleMaximize = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-maximize');
|
||||
// State will be updated via IPC events from main process (main:window-maximized/main:window-unmaximized)
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-close');
|
||||
}, []);
|
||||
|
||||
// Get workspace info
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);
|
||||
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
|
||||
|
||||
// Sort workspaces according to preferences
|
||||
const sortedWorkspaces = useMemo(() => {
|
||||
return sortWorkspaces(workspaces, preferences);
|
||||
}, [workspaces, preferences]);
|
||||
|
||||
const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
|
||||
const [importWorkspaceModalOpen, setImportWorkspaceModalOpen] = useState(false);
|
||||
|
||||
const WorkspaceName = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="workspace-name-container" {...props}>
|
||||
<span data-testid="workspace-name" className={classNames('workspace-name', { 'italic text-muted': !activeWorkspace?.name })}>{getWorkspaceDisplayName(activeWorkspace?.name)}</span>
|
||||
<IconChevronDown size={14} stroke={1.5} className="chevron-icon" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleHomeClick = () => {
|
||||
const scratchCollectionUid = activeWorkspace?.scratchCollectionUid;
|
||||
if (scratchCollectionUid) {
|
||||
dispatch(focusTab({ uid: `${scratchCollectionUid}-overview` }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkspaceSwitch = (workspaceUid) => {
|
||||
dispatch(switchWorkspace(workspaceUid));
|
||||
toast.success(`Switched to ${getWorkspaceDisplayName(workspaces.find((w) => w.uid === workspaceUid)?.name)}`);
|
||||
};
|
||||
|
||||
const handleOpenWorkspace = async () => {
|
||||
try {
|
||||
const result = await dispatch(openWorkspaceDialog());
|
||||
if (result) {
|
||||
toast.success('Workspace opened successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Failed to open workspace');
|
||||
}
|
||||
};
|
||||
|
||||
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());
|
||||
};
|
||||
|
||||
const handleImportWorkspace = () => {
|
||||
setImportWorkspaceModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePinWorkspace = useCallback((workspaceUid, e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const newPreferences = toggleWorkspacePin(workspaceUid, preferences);
|
||||
dispatch(savePreferences(newPreferences));
|
||||
}, [dispatch, preferences]);
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
dispatch(toggleSidebarCollapse());
|
||||
};
|
||||
|
||||
const handleToggleDevtools = () => {
|
||||
if (isConsoleOpen) {
|
||||
dispatch(closeConsole());
|
||||
} else {
|
||||
dispatch(openConsole());
|
||||
}
|
||||
};
|
||||
|
||||
// Build workspace menu items
|
||||
const workspaceMenuItems = useMemo(() => {
|
||||
const items = sortedWorkspaces.map((workspace) => {
|
||||
const isActive = workspace.uid === activeWorkspaceUid;
|
||||
const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid);
|
||||
|
||||
return {
|
||||
id: workspace.uid,
|
||||
label: getWorkspaceDisplayName(workspace.name),
|
||||
onClick: () => handleWorkspaceSwitch(workspace.uid),
|
||||
className: `workspace-item ${isActive ? 'active' : ''}`,
|
||||
rightSection: (
|
||||
<div className="workspace-actions">
|
||||
{workspace.type !== 'default' && (
|
||||
<ActionIcon
|
||||
className={`pin-btn ${isPinned ? 'pinned' : ''}`}
|
||||
onClick={(e) => handlePinWorkspace(workspace.uid, e)}
|
||||
label={isPinned ? 'Unpin workspace' : 'Pin workspace'}
|
||||
size="sm"
|
||||
>
|
||||
{isPinned ? <IconPinned size={14} stroke={1.5} /> : <IconPin size={14} stroke={1.5} />}
|
||||
</ActionIcon>
|
||||
)}
|
||||
{isActive && <IconCheck size={16} stroke={1.5} className="check-icon" />}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
// Add label and action items
|
||||
items.push(
|
||||
{ type: 'label', label: 'Workspaces' },
|
||||
{
|
||||
id: 'create-workspace',
|
||||
leftSection: IconPlus,
|
||||
label: 'Create workspace',
|
||||
onClick: handleCreateWorkspace
|
||||
},
|
||||
{
|
||||
id: 'open-workspace',
|
||||
leftSection: IconFolder,
|
||||
label: 'Open workspace',
|
||||
onClick: handleOpenWorkspace
|
||||
},
|
||||
{
|
||||
id: 'import-workspace',
|
||||
leftSection: IconDownload,
|
||||
label: 'Import workspace',
|
||||
onClick: handleImportWorkspace
|
||||
},
|
||||
{
|
||||
id: 'manage-workspaces',
|
||||
leftSection: IconSettings,
|
||||
label: 'Manage workspaces',
|
||||
onClick: handleManageWorkspaces
|
||||
}
|
||||
);
|
||||
|
||||
return items;
|
||||
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace, handleCreateWorkspace]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`app-titlebar ${osClass} ${isFullScreen ? 'fullscreen' : ''}`}>
|
||||
{createWorkspaceModalOpen && (
|
||||
<CreateWorkspace onClose={() => setCreateWorkspaceModalOpen(false)} />
|
||||
)}
|
||||
{importWorkspaceModalOpen && (
|
||||
<ImportWorkspace onClose={() => setImportWorkspaceModalOpen(false)} />
|
||||
)}
|
||||
|
||||
<div className="titlebar-content">
|
||||
<div className="titlebar-left">
|
||||
{showWindowControls && <AppMenu />}
|
||||
|
||||
<ActionIcon onClick={handleHomeClick} label="Home" size="lg" className="home-button">
|
||||
<IconHome size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Workspace Dropdown */}
|
||||
<MenuDropdown
|
||||
data-testid="workspace-menu"
|
||||
items={workspaceMenuItems}
|
||||
placement="bottom-start"
|
||||
selectedItemId={activeWorkspaceUid}
|
||||
>
|
||||
<WorkspaceName />
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
|
||||
{/* Center section: Bruno logo + text */}
|
||||
<div className="titlebar-center">
|
||||
<Bruno width={18} />
|
||||
<span className="bruno-text">Bruno</span>
|
||||
</div>
|
||||
|
||||
{/* Right section: Action buttons */}
|
||||
<div className="titlebar-right">
|
||||
<div className="titlebar-actions">
|
||||
{/* Toggle sidebar */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleSidebar}
|
||||
label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
|
||||
size="lg"
|
||||
data-testid="toggle-sidebar-button"
|
||||
>
|
||||
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Toggle devtools */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleDevtools}
|
||||
label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
|
||||
size="lg"
|
||||
data-testid="toggle-devtools-button"
|
||||
>
|
||||
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
<ResponseLayoutToggle />
|
||||
</div>
|
||||
|
||||
{showWindowControls && (
|
||||
<div className="window-controls">
|
||||
<button
|
||||
className="window-control-btn minimize"
|
||||
onClick={handleMinimize}
|
||||
aria-label="Minimize"
|
||||
>
|
||||
<IconMinus size={16} stroke={1} />
|
||||
</button>
|
||||
<button
|
||||
className="window-control-btn maximize"
|
||||
onClick={handleMaximize}
|
||||
aria-label={isMaximized ? 'Restore' : 'Maximize'}
|
||||
>
|
||||
{isMaximized ? <IconCopy size={14} stroke={1} /> : <IconSquare size={14} stroke={1} />}
|
||||
</button>
|
||||
<button
|
||||
className="window-control-btn close"
|
||||
onClick={handleClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconX size={16} stroke={1} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppTitleBar;
|
||||
@@ -1,128 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react';
|
||||
import { render, waitFor, act } from '@testing-library/react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
|
||||
jest.mock('ui/MenuDropdown', () => ({ children }) => <div>{children}</div>);
|
||||
jest.mock('ui/ActionIcon', () => ({ children, onClick, label }) => (
|
||||
<button onClick={onClick} aria-label={label}>{children}</button>
|
||||
));
|
||||
jest.mock('components/ResponsePane/ResponseLayoutToggle', () => () => null);
|
||||
|
||||
import AppTitleBar from './index';
|
||||
|
||||
const theme = {
|
||||
text: '#333',
|
||||
sidebar: {
|
||||
bg: '#fff',
|
||||
color: '#333',
|
||||
muted: '#888',
|
||||
collection: { item: { hoverBg: '#eee' } }
|
||||
},
|
||||
dropdown: { color: '#333', mutedText: '#888', hoverBg: '#eee' }
|
||||
};
|
||||
|
||||
const mockStore = configureStore({
|
||||
reducer: {
|
||||
workspaces: (state = { workspaces: [], activeWorkspaceUid: null }) => state,
|
||||
app: (state = { preferences: {}, sidebarCollapsed: false }) => state,
|
||||
logs: (state = { isConsoleOpen: false }) => state
|
||||
}
|
||||
});
|
||||
|
||||
const renderWithProviders = () => render(
|
||||
<Provider store={mockStore}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AppTitleBar />
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
const getTitleBar = (container) => container.querySelector('.app-titlebar');
|
||||
|
||||
const mockInvokeWithFullscreen = (isFullScreen) => jest.fn((channel) => {
|
||||
if (channel === 'renderer:window-is-fullscreen') return Promise.resolve(isFullScreen);
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
|
||||
describe('AppTitleBar — fullscreen state sync', () => {
|
||||
let ipcListeners;
|
||||
|
||||
beforeEach(() => {
|
||||
ipcListeners = {};
|
||||
window.ipcRenderer = {
|
||||
invoke: jest.fn().mockResolvedValue(false),
|
||||
send: jest.fn(),
|
||||
on: jest.fn((channel, cb) => {
|
||||
ipcListeners[channel] = cb;
|
||||
return jest.fn();
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.ipcRenderer;
|
||||
});
|
||||
|
||||
describe('initial state on mount', () => {
|
||||
it('should query the main process for current fullscreen state', async () => {
|
||||
renderWithProviders();
|
||||
await waitFor(() => {
|
||||
expect(window.ipcRenderer.invoke).toHaveBeenCalledWith('renderer:window-is-fullscreen');
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply fullscreen class when window is already fullscreen at mount', async () => {
|
||||
window.ipcRenderer.invoke = mockInvokeWithFullscreen(true);
|
||||
|
||||
const { container } = renderWithProviders();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getTitleBar(container)).toHaveClass('fullscreen');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not apply fullscreen class when window is windowed at mount', async () => {
|
||||
const { container } = renderWithProviders();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.ipcRenderer.invoke).toHaveBeenCalledWith('renderer:window-is-fullscreen');
|
||||
});
|
||||
expect(getTitleBar(container)).not.toHaveClass('fullscreen');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fullscreen transitions after mount', () => {
|
||||
it('should add fullscreen class on main:enter-full-screen event', async () => {
|
||||
const { container } = renderWithProviders();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.ipcRenderer.invoke).toHaveBeenCalledWith('renderer:window-is-fullscreen');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
ipcListeners['main:enter-full-screen']();
|
||||
});
|
||||
|
||||
expect(getTitleBar(container)).toHaveClass('fullscreen');
|
||||
});
|
||||
|
||||
it('should remove fullscreen class on main:leave-full-screen event', async () => {
|
||||
window.ipcRenderer.invoke = mockInvokeWithFullscreen(true);
|
||||
|
||||
const { container } = renderWithProviders();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getTitleBar(container)).toHaveClass('fullscreen');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
ipcListeners['main:leave-full-screen']();
|
||||
});
|
||||
|
||||
expect(getTitleBar(container)).not.toHaveClass('fullscreen');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
|
||||
.body-mode-selector {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
padding-left: 1.5rem !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
fill: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||