Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d9dfa239d | ||
|
|
b7be6aacce | ||
|
|
955d33f1fe | ||
|
|
f3c38400ff |
@@ -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 @bijin-bruno @lohit-bruno @naman-bruno @sid-bruno @vijayh-bruno @utkarsh-bruno @sanish-bruno
|
||||
* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno
|
||||
|
||||
13
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
@@ -49,21 +49,14 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug and how it's affecting your work
|
||||
description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: The exact steps that can be performed to reproduce the issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Collection to reproduce
|
||||
description: If possible, please attach the collection where the bug is present
|
||||
label: .bru file to reproduce the bug
|
||||
description: Attach your .bru file here that can reproduce the problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Discussions
|
||||
url: https://github.com/usebruno/bruno/discussions
|
||||
|
||||
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,18 +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
|
||||
|
||||
CHROME_SANDBOX="${GITHUB_WORKSPACE}/node_modules/electron/dist/chrome-sandbox"
|
||||
if [[ -f "$CHROME_SANDBOX" ]]; then
|
||||
sudo chown root "$CHROME_SANDBOX"
|
||||
sudo chmod 4755 "$CHROME_SANDBOX"
|
||||
fi
|
||||
@@ -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
|
||||
|
||||
@@ -11,8 +11,5 @@ runs:
|
||||
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
|
||||
xvfb libxml2-utils
|
||||
|
||||
CHROME_SANDBOX="${GITHUB_WORKSPACE}/node_modules/electron/dist/chrome-sandbox"
|
||||
if [[ -f "$CHROME_SANDBOX" ]]; then
|
||||
sudo chown root "$CHROME_SANDBOX"
|
||||
sudo chmod 4755 "$CHROME_SANDBOX"
|
||||
fi
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
91
.github/workflows/benchmarks.yml
vendored
@@ -1,91 +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: |
|
||||
CHROME_SANDBOX="${GITHUB_WORKSPACE}/node_modules/electron/dist/chrome-sandbox"
|
||||
if [[ -f "$CHROME_SANDBOX" ]]; then
|
||||
sudo chown root "$CHROME_SANDBOX"
|
||||
sudo chmod 4755 "$CHROME_SANDBOX"
|
||||
fi
|
||||
|
||||
- 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 }})'
|
||||
});
|
||||
123
.github/workflows/flaky-test-detector.yml
vendored
@@ -1,123 +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
|
||||
CHROME_SANDBOX="${GITHUB_WORKSPACE}/node_modules/electron/dist/chrome-sandbox"
|
||||
if [[ -f "$CHROME_SANDBOX" ]]; then
|
||||
sudo chown root "$CHROME_SANDBOX"
|
||||
sudo chmod 4755 "$CHROME_SANDBOX"
|
||||
fi
|
||||
|
||||
- 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' }}
|
||||
6
.github/workflows/npm-bru-cli.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
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
|
||||
146
.github/workflows/tests-linux.yml
vendored
@@ -1,146 +0,0 @@
|
||||
name: Linux Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, 'release/v*']
|
||||
pull_request:
|
||||
branches: [main, 'release/v*']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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: |
|
||||
CHROME_SANDBOX="${GITHUB_WORKSPACE}/node_modules/electron/dist/chrome-sandbox"
|
||||
if [[ -f "$CHROME_SANDBOX" ]]; then
|
||||
sudo chown root "$CHROME_SANDBOX"
|
||||
sudo chmod 4755 "$CHROME_SANDBOX"
|
||||
fi
|
||||
|
||||
- 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
|
||||
127
.github/workflows/tests-macos.yml
vendored
@@ -1,127 +0,0 @@
|
||||
name: macOS Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, 'release/v*']
|
||||
pull_request:
|
||||
branches: [main, 'release/v*']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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
|
||||
138
.github/workflows/tests-windows.yml
vendored
@@ -1,138 +0,0 @@
|
||||
name: Windows Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, 'release/v*']
|
||||
pull_request:
|
||||
branches: [main, 'release/v*']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './package-lock.json'
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
# build libraries
|
||||
- name: Build libraries
|
||||
run: |
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
npm run build --workspace=packages/bruno-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@v5
|
||||
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@v5
|
||||
with:
|
||||
node-version: v22.11.x
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get --no-install-recommends install -y \
|
||||
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
|
||||
xvfb
|
||||
npm ci --legacy-peer-deps
|
||||
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
|
||||
- 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
255
eslint.config.js
@@ -1,6 +1,6 @@
|
||||
// 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');
|
||||
|
||||
@@ -11,18 +11,6 @@ const runESMImports = async () => {
|
||||
};
|
||||
|
||||
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),
|
||||
@@ -46,13 +34,13 @@ 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,
|
||||
@@ -68,7 +56,7 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
minElements: 2,
|
||||
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 +64,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 +82,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 +200,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",
|
||||
},
|
||||
},
|
||||
]));
|
||||
|
||||
16183
package-lock.json
generated
38
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,53 +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/adm-zip": "^0.5.8",
|
||||
"@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",
|
||||
"adm-zip": "^0.5.17",
|
||||
"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",
|
||||
@@ -80,15 +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:e2e:sanity": "playwright test --project=default --project=system-pac --grep @sanity",
|
||||
"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": {
|
||||
@@ -97,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,14 +21,10 @@
|
||||
"@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",
|
||||
"diff": "^5.2.0",
|
||||
"diff2html": "^3.4.47",
|
||||
"dompurify": "^3.2.4",
|
||||
"escape-html": "^1.0.3",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
@@ -40,25 +36,20 @@
|
||||
"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",
|
||||
"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.18.1",
|
||||
"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",
|
||||
@@ -66,10 +57,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",
|
||||
@@ -84,27 +74,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,317 +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;
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
// Tippy renders the popup into document.body, outside StyledWrapper's subtree.
|
||||
export const PopupWrapper = styled.div`
|
||||
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-stop {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
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};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.colors.text.danger};
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
.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,302 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import get from 'lodash/get';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import { IconStars, IconX, IconArrowBackUp, IconPlayerStop } from '@tabler/icons';
|
||||
import { aiGenerateScript, stopAiGeneration } from 'utils/ai';
|
||||
import StyledWrapper, { PopupWrapper } 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' }
|
||||
],
|
||||
'docs': [
|
||||
{ label: 'Overview', prompt: 'Write an overview section describing the purpose and key features' },
|
||||
{ label: 'Request', prompt: 'Document the request method, URL, headers, parameters, and body' },
|
||||
{ label: 'Examples', prompt: 'Add request and response examples with sample JSON' },
|
||||
{ label: 'Errors', prompt: 'Document common error responses and status codes' }
|
||||
],
|
||||
'app-request': [
|
||||
{ label: 'Send button', prompt: 'Add a button that calls ctx.sendRequest() and displays the response status, headers, and pretty-printed body' },
|
||||
{ label: 'Form for body', prompt: 'Build a form whose fields override the request body, then send it with ctx.sendRequest({ variables }) and show the result' },
|
||||
{ label: 'Response viewer', prompt: 'Render ctx.response with collapsible JSON and a banner showing status and response time; update on ctx.onResponseUpdate' },
|
||||
{ label: 'Test results', prompt: 'List ctx.testResults and ctx.assertionResults with pass/fail badges; refresh on ctx.onResultsUpdate' }
|
||||
],
|
||||
'app-collection': [
|
||||
{ label: 'Request list', prompt: 'List all requests from ctx.listRequests() with their method and url, and a Run button next to each that calls ctx.runRequest(pathname)' },
|
||||
{ label: 'Dashboard', prompt: 'Build a small dashboard that runs every request from ctx.listRequests() on load and shows status code, response time, and a pass/fail dot for each' },
|
||||
{ label: 'Form runner', prompt: 'Render a form, and on submit call ctx.runRequest(pathname, { variables }) for a chosen request and display the response' },
|
||||
{ label: 'Variables panel', prompt: 'Show ctx.variables in a table and allow editing values via ctx.setRuntimeVariable(key, value); react to ctx.onVariablesUpdate' }
|
||||
]
|
||||
};
|
||||
|
||||
const TITLES = {
|
||||
'tests': 'Generate Tests',
|
||||
'pre-request': 'Generate Pre-Request Script',
|
||||
'post-response': 'Generate Post-Response Script',
|
||||
'docs': 'Generate Documentation',
|
||||
'app-request': 'Generate App',
|
||||
'app-collection': 'Generate App'
|
||||
};
|
||||
|
||||
const PREVIEW_LABELS = {
|
||||
'docs': 'Preview · replaces current documentation',
|
||||
'app-request': 'Preview · replaces current app',
|
||||
'app-collection': 'Preview · replaces current app'
|
||||
};
|
||||
|
||||
const isValidType = (t) => SUGGESTIONS[t] !== undefined;
|
||||
|
||||
const AIAssist = ({ scriptType, currentScript, requestContext, docsContext, variables, 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 streamIdRef = useRef(null);
|
||||
const tippyRef = useRef(null);
|
||||
|
||||
// Focus the prompt textarea when coming back from preview
|
||||
useEffect(() => {
|
||||
if (isOpen && generated == null) {
|
||||
tippyRef.current?.popper?.querySelector('.popup-input')?.focus();
|
||||
}
|
||||
}, [isOpen, generated]);
|
||||
|
||||
// handle Escape key to close the popup
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const onKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation();
|
||||
tippyRef.current?.hide();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', onKeyDown, true);
|
||||
return () => document.removeEventListener('keydown', onKeyDown, true);
|
||||
}, [isOpen]);
|
||||
|
||||
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 previewLabel = PREVIEW_LABELS[scriptType] || 'Preview · replaces current script';
|
||||
|
||||
const close = useCallback(() => {
|
||||
tippyRef.current?.hide();
|
||||
}, []);
|
||||
|
||||
const handleGenerate = useCallback(
|
||||
async (overridePrompt) => {
|
||||
const text = (overridePrompt ?? prompt).trim();
|
||||
if (!text || isLoading) return;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const streamId = `sparkle-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
streamIdRef.current = streamId;
|
||||
|
||||
try {
|
||||
const result = await aiGenerateScript({
|
||||
scriptType,
|
||||
prompt: text,
|
||||
currentScript: currentScript || '',
|
||||
requestContext,
|
||||
docsContext,
|
||||
variables,
|
||||
streamId
|
||||
});
|
||||
if (result?.stopped) {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
streamIdRef.current = null;
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[prompt, isLoading, scriptType, currentScript, requestContext, docsContext, variables]
|
||||
);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
if (streamIdRef.current) {
|
||||
stopAiGeneration(streamIdRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
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>
|
||||
<Tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
arrow={false}
|
||||
animation={false}
|
||||
maxWidth="none"
|
||||
appendTo={() => document.body}
|
||||
onCreate={(instance) => (tippyRef.current = instance)}
|
||||
onShow={(instance) => {
|
||||
setIsOpen(true);
|
||||
// rAF so the popup content is in the DOM
|
||||
requestAnimationFrame(() => instance.popper?.querySelector('.popup-input')?.focus());
|
||||
}}
|
||||
onHide={() => {
|
||||
setIsOpen(false);
|
||||
setError(null);
|
||||
}}
|
||||
render={(attrs) => (
|
||||
<PopupWrapper className="ai-assist-popup" role="dialog" aria-label={title} tabIndex={-1} {...attrs}>
|
||||
<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
|
||||
className="popup-input"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
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 · Shift+Enter for newline</span>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<button
|
||||
className="btn-stop"
|
||||
type="button"
|
||||
onClick={handleStop}
|
||||
title="Stop generating"
|
||||
>
|
||||
<IconPlayerStop size={12} /> Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn-generate"
|
||||
type="button"
|
||||
onClick={() => handleGenerate()}
|
||||
disabled={!prompt.trim()}
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="popup-body">
|
||||
<div className="preview-section">
|
||||
<span className="preview-label">{previewLabel}</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>
|
||||
</>
|
||||
)}
|
||||
</PopupWrapper>
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className={`ai-assist-trigger ${isOpen ? 'open' : ''}`}
|
||||
title={title}
|
||||
type="button"
|
||||
aria-label={title}
|
||||
>
|
||||
<IconStars size={14} strokeWidth={1.75} />
|
||||
</button>
|
||||
</Tippy>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIAssist;
|
||||
@@ -1,404 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { aiGenerateScript, stopAiGeneration } from 'utils/ai';
|
||||
import AIAssist from './index';
|
||||
|
||||
jest.mock('utils/ai', () => ({
|
||||
aiGenerateScript: jest.fn(),
|
||||
stopAiGeneration: jest.fn()
|
||||
}));
|
||||
|
||||
const theme = {
|
||||
bg: '#1e1e1e',
|
||||
text: '#ffffff',
|
||||
border: { radius: { sm: '4px', md: '6px' } },
|
||||
colors: {
|
||||
accent: '#6366f1',
|
||||
text: { muted: '#9ca3af', danger: '#ef4444' },
|
||||
bg: { danger: '#ef4444' }
|
||||
},
|
||||
input: {
|
||||
border: '#374151',
|
||||
bg: '#111827',
|
||||
focusBorder: '#6366f1'
|
||||
},
|
||||
font: { monospace: 'monospace' }
|
||||
};
|
||||
|
||||
const createStore = (aiEnabled = true) => configureStore({
|
||||
reducer: {
|
||||
app: (state = { preferences: { ai: { enabled: aiEnabled } } }) => state
|
||||
}
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
scriptType: 'tests',
|
||||
currentScript: 'test("ok", () => {});',
|
||||
onApply: jest.fn()
|
||||
};
|
||||
|
||||
const renderAIAssist = ({
|
||||
props = {},
|
||||
aiEnabled = true
|
||||
} = {}) => {
|
||||
const mergedProps = { ...defaultProps, ...props };
|
||||
return render(
|
||||
<Provider store={createStore(aiEnabled)}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AIAssist {...mergedProps} />
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const openPopup = () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Generate Tests' }));
|
||||
};
|
||||
|
||||
describe('AIAssist', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
aiGenerateScript.mockResolvedValue({ content: 'test("generated", () => {});' });
|
||||
});
|
||||
|
||||
describe('visibility', () => {
|
||||
it('renders nothing when AI is disabled', () => {
|
||||
const { container } = renderAIAssist({ aiEnabled: false });
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders nothing for an unsupported script type', () => {
|
||||
const { container } = renderAIAssist({ props: { scriptType: 'unknown-type' } });
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the trigger when AI is enabled and the script type is supported', () => {
|
||||
renderAIAssist();
|
||||
expect(screen.getByRole('button', { name: 'Generate Tests' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('titles', () => {
|
||||
it.each([
|
||||
['tests', 'Generate Tests'],
|
||||
['pre-request', 'Generate Pre-Request Script'],
|
||||
['post-response', 'Generate Post-Response Script'],
|
||||
['docs', 'Generate Documentation']
|
||||
])('uses the correct title for %s', (scriptType, title) => {
|
||||
renderAIAssist({ props: { scriptType } });
|
||||
expect(screen.getByRole('button', { name: title })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('popup interactions', () => {
|
||||
it('opens and closes the popup from the trigger and close button', () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
|
||||
expect(screen.getByRole('dialog', { name: 'Generate Tests' })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
||||
expect(screen.queryByRole('dialog', { name: 'Generate Tests' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the popup into document.body as a portal', () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
const dialog = screen.getByRole('dialog', { name: 'Generate Tests' });
|
||||
const tippyRoot = dialog.closest('[data-tippy-root]');
|
||||
expect(tippyRoot).not.toBeNull();
|
||||
expect(tippyRoot.parentElement).toBe(document.body);
|
||||
});
|
||||
|
||||
it('closes the popup when Escape is pressed', () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
expect(screen.queryByRole('dialog', { name: 'Generate Tests' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes the popup when clicking outside', () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
|
||||
fireEvent.mouseDown(document.body);
|
||||
expect(screen.queryByRole('dialog', { name: 'Generate Tests' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('prompt view', () => {
|
||||
it('shows suggestion chips when the prompt is empty', () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Status 200' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'JSON body' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows docs suggestions for the docs script type', () => {
|
||||
renderAIAssist({ props: { scriptType: 'docs', currentScript: '# Overview' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Generate Documentation' }));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Overview' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Request' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides suggestions once the user starts typing', () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Describe what you want to generate...'), {
|
||||
target: { value: 'Add a status test' }
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Status 200' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps Generate disabled until the prompt has text', () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Generate' })).toBeDisabled();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Describe what you want to generate...'), {
|
||||
target: { value: 'Add a status test' }
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Generate' })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generation flow', () => {
|
||||
it('generates from a suggestion chip', async () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(aiGenerateScript).toHaveBeenCalledWith(expect.objectContaining({
|
||||
scriptType: 'tests',
|
||||
prompt: 'Add a test asserting the response status code is 200',
|
||||
currentScript: 'test("ok", () => {});',
|
||||
requestContext: undefined,
|
||||
streamId: expect.any(String)
|
||||
}));
|
||||
});
|
||||
|
||||
expect(screen.getByText('test("generated", () => {});')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('passes docs context for folder and collection documentation', async () => {
|
||||
const docsContext = {
|
||||
scope: 'folder',
|
||||
name: 'Users',
|
||||
collectionName: 'Pet Store API',
|
||||
folders: [{ name: 'Admin', requestCount: 1, subfolderCount: 0 }],
|
||||
requests: [{ name: 'List Users', method: 'GET', url: '{{base}}/users' }]
|
||||
};
|
||||
|
||||
renderAIAssist({ props: { scriptType: 'docs', currentScript: '', docsContext } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Generate Documentation' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Overview' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(aiGenerateScript).toHaveBeenCalledWith(expect.objectContaining({
|
||||
scriptType: 'docs',
|
||||
prompt: 'Write an overview section describing the purpose and key features',
|
||||
currentScript: '',
|
||||
requestContext: undefined,
|
||||
docsContext
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('generates from the prompt input and passes request context', async () => {
|
||||
const requestContext = {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users',
|
||||
headers: [],
|
||||
params: [],
|
||||
body: null
|
||||
};
|
||||
|
||||
renderAIAssist({ props: { requestContext } });
|
||||
openPopup();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Describe what you want to generate...'), {
|
||||
target: { value: 'Add auth header test' }
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Generate' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(aiGenerateScript).toHaveBeenCalledWith(expect.objectContaining({
|
||||
scriptType: 'tests',
|
||||
prompt: 'Add auth header test',
|
||||
currentScript: 'test("ok", () => {});',
|
||||
requestContext
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('generates when pressing Enter', async () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
|
||||
const textarea = screen.getByPlaceholderText('Describe what you want to generate...');
|
||||
fireEvent.change(textarea, { target: { value: 'Add response time test' } });
|
||||
fireEvent.keyDown(textarea, { key: 'Enter' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(aiGenerateScript).toHaveBeenCalledWith(expect.objectContaining({
|
||||
scriptType: 'tests',
|
||||
prompt: 'Add response time test',
|
||||
currentScript: 'test("ok", () => {});',
|
||||
requestContext: undefined
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('does not generate when pressing Shift+Enter (allows newline)', () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
|
||||
const textarea = screen.getByPlaceholderText('Describe what you want to generate...');
|
||||
fireEvent.change(textarea, { target: { value: 'Add response time test' } });
|
||||
fireEvent.keyDown(textarea, { key: 'Enter', shiftKey: true });
|
||||
|
||||
expect(aiGenerateScript).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a loading state while generation is in progress', async () => {
|
||||
let resolveGenerate;
|
||||
aiGenerateScript.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveGenerate = resolve;
|
||||
}));
|
||||
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
|
||||
|
||||
expect(screen.getByText('Generating...')).toBeInTheDocument();
|
||||
|
||||
resolveGenerate({ content: 'test("done", () => {});' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test("done", () => {});')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a Stop button during generation and cancels via streamId', async () => {
|
||||
let resolveGenerate;
|
||||
aiGenerateScript.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveGenerate = resolve;
|
||||
}));
|
||||
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
|
||||
|
||||
const stopButton = await screen.findByRole('button', { name: /stop/i });
|
||||
expect(stopButton).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Generate' })).not.toBeInTheDocument();
|
||||
|
||||
const passedStreamId = aiGenerateScript.mock.calls[0][0].streamId;
|
||||
expect(passedStreamId).toEqual(expect.any(String));
|
||||
|
||||
fireEvent.click(stopButton);
|
||||
expect(stopAiGeneration).toHaveBeenCalledWith(passedStreamId);
|
||||
|
||||
resolveGenerate({ stopped: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('button', { name: /stop/i })).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Generate' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows an API error without entering preview mode', async () => {
|
||||
aiGenerateScript.mockResolvedValue({ error: 'Provider unavailable' });
|
||||
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Provider unavailable')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows a fallback error when no content is returned', async () => {
|
||||
aiGenerateScript.mockResolvedValue({});
|
||||
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No content was generated. Try rephrasing your prompt.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('preview and apply', () => {
|
||||
const showPreview = async () => {
|
||||
renderAIAssist();
|
||||
openPopup();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Apply' })).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
it('uses the script preview label for script types', async () => {
|
||||
await showPreview();
|
||||
expect(screen.getByText('Preview · replaces current script')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the documentation preview label for docs', async () => {
|
||||
aiGenerateScript.mockResolvedValue({ content: '# API Docs' });
|
||||
|
||||
renderAIAssist({ props: { scriptType: 'docs', currentScript: '# Existing' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Generate Documentation' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Overview' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Preview · replaces current documentation')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('# API Docs')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies generated content and closes the popup', async () => {
|
||||
const onApply = jest.fn();
|
||||
renderAIAssist({ props: { onApply } });
|
||||
openPopup();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Status 200' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Apply' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
||||
|
||||
expect(onApply).toHaveBeenCalledWith('test("generated", () => {});');
|
||||
expect(screen.queryByRole('dialog', { name: 'Generate Tests' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('returns to the prompt view when Back is clicked', async () => {
|
||||
await showPreview();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Back' }));
|
||||
|
||||
expect(screen.getByPlaceholderText('Describe what you want to generate...')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Status 200' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,46 +0,0 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { IconCopy, IconCheck } from '@tabler/icons';
|
||||
|
||||
const AssistantCodeBlock = ({ content, language, isOpen, isStreaming, isLast }) => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const preRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isStreaming && isOpen && preRef.current) {
|
||||
preRef.current.scrollTop = preRef.current.scrollHeight;
|
||||
}
|
||||
}, [content, isStreaming, isOpen]);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 1500);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="assistant-code-block">
|
||||
<div className="assistant-code-block__header">
|
||||
<div className="assistant-code-block__meta">
|
||||
<span className="assistant-code-block__lang">{language || 'code'}</span>
|
||||
{isOpen && <span className="assistant-code-block__spinner" />}
|
||||
</div>
|
||||
<button className="assistant-code-block__btn" onClick={handleCopy} title="Copy">
|
||||
{isCopied ? <IconCheck size={12} /> : <IconCopy size={12} />}
|
||||
{isCopied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
<pre ref={preRef} className="assistant-code-block__body">
|
||||
<code className={`language-${language || 'text'}`}>
|
||||
{content}
|
||||
{isStreaming && isLast && <span className="cursor">|</span>}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssistantCodeBlock;
|
||||
@@ -1,298 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
margin-top: 8px;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
overflow: hidden;
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
|
||||
&.accepted {
|
||||
border-color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.diff-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
background: ${(props) => props.theme.background.mantle};
|
||||
border-bottom: 1px solid ${(props) => props.theme.border.border1};
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.diff-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
flex-shrink: 0;
|
||||
|
||||
.diff-icon {
|
||||
color: ${(props) => props.theme.brand};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.diff-content-type {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.diff-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
|
||||
.stat {
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.additions {
|
||||
background: ${(props) => props.theme.status.success.background};
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
.deletions {
|
||||
background: ${(props) => props.theme.status.danger.background};
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
.diff-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.diff-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&.accept {
|
||||
background: ${(props) => props.theme.brand};
|
||||
color: ${(props) => (props.theme.mode === 'dark' ? '#000' : '#fff')};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&.reject {
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
border-color: ${(props) => props.theme.border.border1};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.status.danger.background};
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
border-color: ${(props) => props.theme.status.danger.background};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
font-weight: 500;
|
||||
|
||||
&.accepted {
|
||||
background: ${(props) => props.theme.status.success.background};
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
background: ${(props) => props.theme.status.danger.background};
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
.diff-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.border.border1};
|
||||
|
||||
&.warn {
|
||||
background: ${(props) => props.theme.status.warning.background};
|
||||
color: ${(props) => props.theme.status.warning.text};
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: ${(props) => props.theme.status.danger.background};
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
.diff-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-top: 1px solid ${(props) => props.theme.border.border1};
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.diff-content {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: ${(props) => props.theme.border.border1};
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.diff-line {
|
||||
padding: 0 8px 0 4px;
|
||||
white-space: pre;
|
||||
display: flex;
|
||||
min-height: 18px;
|
||||
line-height: 18px;
|
||||
|
||||
.line-number {
|
||||
width: 24px;
|
||||
text-align: right;
|
||||
padding-right: 8px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.line-prefix {
|
||||
width: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.line-content {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
&.added {
|
||||
background: ${(props) => props.theme.status.success.background};
|
||||
.line-content { color: ${(props) => props.theme.colors.text.green}; }
|
||||
.line-prefix { color: ${(props) => props.theme.colors.text.green}; font-weight: 600; }
|
||||
}
|
||||
|
||||
&.removed {
|
||||
background: ${(props) => props.theme.status.danger.background};
|
||||
.line-content { color: ${(props) => props.theme.colors.text.danger}; }
|
||||
.line-prefix { color: ${(props) => props.theme.colors.text.danger}; font-weight: 600; }
|
||||
}
|
||||
|
||||
&.unchanged {
|
||||
.line-content { color: ${(props) => props.theme.colors.text.muted}; }
|
||||
.line-prefix { opacity: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
.expand-marker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px 0 4px;
|
||||
min-height: 22px;
|
||||
background: ${(props) => props.theme.background.mantle};
|
||||
|
||||
.expand-gutter {
|
||||
width: 24px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.expand-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 11px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: ${(props) => props.theme.border.border1};
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,210 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { diffLines } from 'diff';
|
||||
import { IconCheck, IconX, IconCode, IconChevronDown, IconChevronUp } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CONTEXT_LINES = 2;
|
||||
const EXPAND_CHUNK_SIZE = 20;
|
||||
|
||||
const DiffView = ({ originalCode, newCode, onAccept, onReject, status, contentTypeLabel, warning, disableAccept }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [expandedFromTop, setExpandedFromTop] = useState({});
|
||||
const [expandedFromBottom, setExpandedFromBottom] = useState({});
|
||||
|
||||
const diffResult = useMemo(() => {
|
||||
const changes = diffLines(originalCode || '', newCode || '');
|
||||
let additions = 0;
|
||||
let deletions = 0;
|
||||
let lineNumber = 1;
|
||||
|
||||
const lines = changes.flatMap((part) => {
|
||||
const partLines = part.value.split('\n');
|
||||
if (partLines[partLines.length - 1] === '') partLines.pop();
|
||||
|
||||
return partLines.map((line) => {
|
||||
const entry = { content: line, lineNumber: null };
|
||||
if (part.added) {
|
||||
additions += 1;
|
||||
entry.type = 'added';
|
||||
entry.lineNumber = lineNumber++;
|
||||
} else if (part.removed) {
|
||||
deletions += 1;
|
||||
entry.type = 'removed';
|
||||
} else {
|
||||
entry.type = 'unchanged';
|
||||
entry.lineNumber = lineNumber++;
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
});
|
||||
|
||||
return { lines, additions, deletions };
|
||||
}, [originalCode, newCode]);
|
||||
|
||||
const hunks = useMemo(() => {
|
||||
const { lines } = diffResult;
|
||||
if (lines.length === 0) return [];
|
||||
|
||||
const changedIndices = new Set();
|
||||
lines.forEach((line, idx) => {
|
||||
if (line.type === 'added' || line.type === 'removed') changedIndices.add(idx);
|
||||
});
|
||||
|
||||
const visibleIndices = new Set();
|
||||
changedIndices.forEach((idx) => {
|
||||
for (let i = Math.max(0, idx - CONTEXT_LINES); i <= Math.min(lines.length - 1, idx + CONTEXT_LINES); i++) {
|
||||
visibleIndices.add(i);
|
||||
}
|
||||
});
|
||||
|
||||
const result = [];
|
||||
let i = 0;
|
||||
while (i < lines.length) {
|
||||
if (visibleIndices.has(i)) {
|
||||
result.push({ type: 'line', data: lines[i], index: i });
|
||||
i += 1;
|
||||
} else {
|
||||
const start = i;
|
||||
while (i < lines.length && !visibleIndices.has(i)) i += 1;
|
||||
result.push({
|
||||
type: 'collapsed',
|
||||
startIndex: start,
|
||||
count: i - start,
|
||||
lines: lines.slice(start, i)
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [diffResult]);
|
||||
|
||||
const expandUp = (startIndex, totalLines) => {
|
||||
setExpandedFromTop((prev) => {
|
||||
const current = prev[startIndex] || 0;
|
||||
const bottomExpanded = expandedFromBottom[startIndex] || 0;
|
||||
const remaining = totalLines - current - bottomExpanded;
|
||||
return { ...prev, [startIndex]: Math.min(current + EXPAND_CHUNK_SIZE, current + remaining) };
|
||||
});
|
||||
};
|
||||
|
||||
const expandDown = (startIndex, totalLines) => {
|
||||
setExpandedFromBottom((prev) => {
|
||||
const current = prev[startIndex] || 0;
|
||||
const topExpanded = expandedFromTop[startIndex] || 0;
|
||||
const remaining = totalLines - topExpanded - current;
|
||||
return { ...prev, [startIndex]: Math.min(current + EXPAND_CHUNK_SIZE, current + remaining) };
|
||||
});
|
||||
};
|
||||
|
||||
if (diffResult.additions === 0 && diffResult.deletions === 0) return null;
|
||||
|
||||
const renderActions = () => {
|
||||
if (status === 'accepted') {
|
||||
return (
|
||||
<span className="status-badge accepted">
|
||||
<IconCheck size={12} /> Applied
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (status === 'rejected') {
|
||||
return (
|
||||
<span className="status-badge rejected">
|
||||
<IconX size={12} /> Dismissed
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="diff-actions">
|
||||
<button className="diff-btn reject" onClick={onReject} title="Dismiss changes">
|
||||
<IconX size={12} />
|
||||
</button>
|
||||
<button className="diff-btn accept" onClick={onAccept} title="Apply changes" disabled={disableAccept}>
|
||||
<IconCheck size={12} /> Apply
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLine = (line, key) => (
|
||||
<div key={key} className={`diff-line ${line.type}`}>
|
||||
<span className="line-number">{line.type !== 'removed' ? line.lineNumber : ''}</span>
|
||||
<span className="line-prefix">{line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' '}</span>
|
||||
<span className="line-content">{line.content || ' '}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderHunks = () =>
|
||||
hunks.map((hunk, idx) => {
|
||||
if (hunk.type === 'line') return renderLine(hunk.data, `line-${hunk.index}`);
|
||||
|
||||
const topCount = expandedFromTop[hunk.startIndex] || 0;
|
||||
const bottomCount = expandedFromBottom[hunk.startIndex] || 0;
|
||||
const remainingCount = hunk.count - topCount - bottomCount;
|
||||
|
||||
const topLines = hunk.lines.slice(0, topCount);
|
||||
const bottomLines = hunk.lines.slice(hunk.count - bottomCount);
|
||||
const isAtTop = idx === 0;
|
||||
const isAtBottom = idx === hunks.length - 1;
|
||||
|
||||
return (
|
||||
<React.Fragment key={`collapsed-${hunk.startIndex}`}>
|
||||
{topLines.map((line, lineIdx) => renderLine(line, `top-${hunk.startIndex}-${lineIdx}`))}
|
||||
|
||||
{remainingCount > 0 && (
|
||||
<div className="expand-marker">
|
||||
<div className="expand-gutter">
|
||||
<div className="expand-buttons">
|
||||
{!isAtTop && (
|
||||
<button className="expand-btn" onClick={() => expandUp(hunk.startIndex, hunk.count)} title="Expand up">
|
||||
<IconChevronUp size={10} />
|
||||
</button>
|
||||
)}
|
||||
{!isAtBottom && (
|
||||
<button className="expand-btn" onClick={() => expandDown(hunk.startIndex, hunk.count)} title="Expand down">
|
||||
<IconChevronDown size={10} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="expand-line" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bottomLines.map((line, lineIdx) => renderLine(line, `bottom-${hunk.startIndex}-${lineIdx}`))}
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper className={status || ''}>
|
||||
<div className="diff-header">
|
||||
<div className="diff-title">
|
||||
<span className="diff-icon"><IconCode size={12} /></span>
|
||||
{contentTypeLabel && <span className="diff-content-type">{contentTypeLabel}</span>}
|
||||
<div className="diff-stats">
|
||||
<span className="stat additions">+{diffResult.additions}</span>
|
||||
<span className="stat deletions">-{diffResult.deletions}</span>
|
||||
</div>
|
||||
</div>
|
||||
{renderActions()}
|
||||
</div>
|
||||
|
||||
{warning && (
|
||||
<div className={`diff-warning ${disableAccept ? 'error' : 'warn'}`}>
|
||||
{warning}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded && <div className="diff-content">{renderHunks()}</div>}
|
||||
|
||||
<button className="diff-toggle" onClick={() => setIsExpanded((v) => !v)}>
|
||||
{isExpanded ? (
|
||||
<><IconChevronUp size={12} /> Hide</>
|
||||
) : (
|
||||
<><IconChevronDown size={12} /> Show ({diffResult.additions + diffResult.deletions})</>
|
||||
)}
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiffView;
|
||||
@@ -1,831 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
|
||||
.ai-sidebar {
|
||||
width: 420px;
|
||||
height: 100%;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-left: 1px solid ${(props) => props.theme.border.border1};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ai-sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.border.border1};
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
color: ${(props) => props.theme.brand};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-method {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.method-get { color: ${(props) => props.theme.request.methods.get}; }
|
||||
&.method-post { color: ${(props) => props.theme.request.methods.post}; }
|
||||
&.method-put { color: ${(props) => props.theme.request.methods.put}; }
|
||||
&.method-delete { color: ${(props) => props.theme.request.methods.delete}; }
|
||||
&.method-patch { color: ${(props) => props.theme.request.methods.patch}; }
|
||||
&.method-options { color: ${(props) => props.theme.request.methods.options}; }
|
||||
&.method-head { color: ${(props) => props.theme.request.methods.head}; }
|
||||
&.method-grpc { color: ${(props) => props.theme.request.grpc}; }
|
||||
&.method-ws { color: ${(props) => props.theme.request.ws}; }
|
||||
&.method-gql { color: ${(props) => props.theme.request.gql}; }
|
||||
&.method-app { color: ${(props) => props.theme.brand}; }
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 13px;
|
||||
color: ${(props) => props.theme.text};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-switcher-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.history-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
position: relative;
|
||||
padding: 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.close-btn:hover {
|
||||
background: ${(props) => props.theme.status.danger.background};
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.history-popover {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
width: 300px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
border-radius: 8px;
|
||||
box-shadow: ${(props) => props.theme.shadow.md};
|
||||
padding: 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: ${(props) => props.theme.scrollbar.color};
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 2px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
padding: 6px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&__title-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 10px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
&__delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.status.danger.background};
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ai-sidebar-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: ${(props) => props.theme.scrollbar.color};
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
|
||||
.empty-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: ${(props) => props.theme.brand};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) => (props.theme.mode === 'dark' ? '#000' : '#fff')};
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
> p {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: 12px;
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.suggestions-title {
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.suggestion-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.suggestion-chip {
|
||||
padding: 5px 10px;
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.brand};
|
||||
color: ${(props) => props.theme.brand};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
animation: slideIn 0.25s ease;
|
||||
|
||||
&.user .message-content {
|
||||
background: ${(props) => props.theme.background.mantle};
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&.assistant .message-content {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.message-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&__spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${(props) => props.theme.brand};
|
||||
border-top-color: transparent;
|
||||
animation: spin 0.9s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-activity-log {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
margin: 6px 0;
|
||||
padding: 4px 0;
|
||||
|
||||
&.completed {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
line-height: 1.6;
|
||||
padding: 1px 0;
|
||||
|
||||
.tool-activity-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.done .tool-activity-indicator {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
.tool-activity-indicator {
|
||||
color: ${(props) => props.theme.brand};
|
||||
}
|
||||
}
|
||||
|
||||
.tool-activity-spinner {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid ${(props) => props.theme.brand};
|
||||
border-top-color: transparent;
|
||||
animation: spin 0.9s linear infinite;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.message-cancelled {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.assistant-code-block {
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
border-radius: 8px;
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
overflow: hidden;
|
||||
margin: 8px 0;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.border.border1};
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
&__lang {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
&__spinner {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${(props) => props.theme.brand};
|
||||
border-top-color: transparent;
|
||||
animation: spin 0.9s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 6px;
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
border-radius: 4px;
|
||||
background: ${(props) => props.theme.background.mantle};
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.brand};
|
||||
color: ${(props) => props.theme.brand};
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
overflow: auto;
|
||||
max-height: 240px;
|
||||
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
animation: blink 1s infinite;
|
||||
color: ${(props) => props.theme.brand};
|
||||
margin-left: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.prose.markdown-body {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
animation: blink 1s infinite;
|
||||
color: ${(props) => props.theme.brand};
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 13px;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 10px 0 6px 0;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
&:first-child { margin-top: 0; }
|
||||
}
|
||||
|
||||
h1 { font-size: 1.3em; }
|
||||
h2 { font-size: 1.2em; }
|
||||
h3 { font-size: 1.1em; }
|
||||
|
||||
ul, ol {
|
||||
margin: 6px 0;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
code {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre, .code-block {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid ${(props) => props.theme.brand};
|
||||
margin: 8px 0;
|
||||
padding: 4px 0 4px 10px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
text-decoration: none;
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
|
||||
strong { font-weight: 600; }
|
||||
em { font-style: italic; }
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid ${(props) => props.theme.border.border1};
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.processing-indicator {
|
||||
padding: 8px 10px;
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
border-radius: 8px;
|
||||
animation: slideIn 0.2s ease;
|
||||
|
||||
.processing-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.processing-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
background: ${(props) => props.theme.background.surface1};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) => props.theme.brand};
|
||||
}
|
||||
|
||||
.processing-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.processing-dots {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
margin-left: 2px;
|
||||
|
||||
span {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: ${(props) => props.theme.brand};
|
||||
border-radius: 50%;
|
||||
animation: dotBounce 1.4s infinite ease-in-out both;
|
||||
|
||||
&:nth-child(1) { animation-delay: -0.32s; }
|
||||
&:nth-child(2) { animation-delay: -0.16s; }
|
||||
}
|
||||
}
|
||||
|
||||
.processing-bar {
|
||||
height: 2px;
|
||||
background: ${(props) => props.theme.border.border1};
|
||||
border-radius: 1px;
|
||||
overflow: hidden;
|
||||
|
||||
.processing-bar-fill {
|
||||
height: 100%;
|
||||
width: 30%;
|
||||
background: ${(props) => props.theme.brand};
|
||||
border-radius: 1px;
|
||||
animation: progressSlide 1.5s infinite ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background: ${(props) => props.theme.status.danger.background};
|
||||
border: 1px solid ${(props) => props.theme.status.danger.border};
|
||||
border-radius: 6px;
|
||||
|
||||
.error-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: ${(props) => props.theme.colors.text.danger};
|
||||
color: ${(props) => (props.theme.mode === 'dark' ? '#000' : '#fff')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-sidebar-input {
|
||||
padding: 12px;
|
||||
border-top: 1px solid ${(props) => props.theme.border.border1};
|
||||
|
||||
.no-models-warning {
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: ${(props) => props.theme.input.bg};
|
||||
border: 1px dashed ${(props) => props.theme.border.border1};
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
border-radius: 8px;
|
||||
|
||||
&:focus-within {
|
||||
border-color: ${(props) => props.theme.brand};
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 4px 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
line-height: 1.4;
|
||||
resize: none;
|
||||
outline: none;
|
||||
max-height: 100px;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.model-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.model-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 6px 4px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
|
||||
svg:first-child {
|
||||
color: ${(props) => props.theme.brand};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.border.border2};
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn, .stop-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
background: ${(props) => props.theme.brand};
|
||||
color: ${(props) => (props.theme.mode === 'dark' ? '#000' : '#fff')};
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.stop-btn {
|
||||
background: ${(props) => props.theme.colors.text.danger};
|
||||
color: ${(props) => (props.theme.mode === 'dark' ? '#000' : '#fff')};
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes dotBounce {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes progressSlide {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(400%); }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,54 +0,0 @@
|
||||
export const PROCESSING_STAGES = [
|
||||
{ id: 'sending', label: 'Sending request', icon: 'send' },
|
||||
{ id: 'thinking', label: 'AI is thinking', icon: 'sparkles' },
|
||||
{ id: 'generating', label: 'Generating response', icon: 'wand' },
|
||||
{ id: 'applying', label: 'Preparing changes', icon: 'code' }
|
||||
];
|
||||
|
||||
export const CONTENT_TYPE_LABELS = {
|
||||
'app': 'App',
|
||||
'tests': 'Tests',
|
||||
'pre-request': 'Script',
|
||||
'post-response': 'Script',
|
||||
'docs': 'Docs'
|
||||
};
|
||||
|
||||
export const SUGGESTIONS_BY_TYPE = {
|
||||
'app': [
|
||||
{ label: 'Create a form for this request', prompt: 'Create a simple form to send this request' },
|
||||
{ label: 'Add a loading spinner', prompt: 'Add a loading spinner while the request is pending' },
|
||||
{ label: 'Show response in a table', prompt: 'Display the response data in a table' },
|
||||
{ label: 'Add error handling', prompt: 'Add error handling with user-friendly messages' }
|
||||
],
|
||||
'tests': [
|
||||
{ label: 'Generate basic tests', prompt: 'Generate tests for status code, response body, and headers' },
|
||||
{ label: 'Test response structure', prompt: 'Write tests to validate the response body structure and data types' },
|
||||
{ label: 'Test error cases', prompt: 'Write tests for common error scenarios' },
|
||||
{ label: 'Test response time', prompt: 'Add a test to verify response time is acceptable' }
|
||||
],
|
||||
'pre-request': [
|
||||
{ label: 'Add authentication', prompt: 'Add authorization header from environment variable' },
|
||||
{ label: 'Set dynamic variables', prompt: 'Set dynamic request variables like timestamp or unique ID' },
|
||||
{ label: 'Conditional logic', prompt: 'Add conditional logic to modify the request based on environment' }
|
||||
],
|
||||
'post-response': [
|
||||
{ label: 'Extract to variables', prompt: 'Extract data from response and save to environment variables' },
|
||||
{ label: 'Store auth token', prompt: 'Extract auth token from response and save for future requests' },
|
||||
{ label: 'Log response', prompt: 'Log response status and body for debugging' },
|
||||
{ label: 'Transform response', prompt: 'Transform and process the response data' }
|
||||
],
|
||||
'docs': [
|
||||
{ label: 'Generate full docs', prompt: 'Generate comprehensive API documentation for this endpoint' },
|
||||
{ label: 'Document parameters', prompt: 'Document all request parameters, headers, and body' },
|
||||
{ label: 'Add examples', prompt: 'Add request and response examples' },
|
||||
{ label: 'Document errors', prompt: 'Document common error responses and status codes' }
|
||||
]
|
||||
};
|
||||
|
||||
export const PLACEHOLDER_BY_TYPE = {
|
||||
'tests': { empty: 'Describe the tests you want...', filled: 'Ask to modify or add tests...' },
|
||||
'pre-request': { empty: 'Describe the script you want...', filled: 'Ask to modify the script...' },
|
||||
'post-response': { empty: 'Describe the script you want...', filled: 'Ask to modify the script...' },
|
||||
'docs': { empty: 'Describe the documentation...', filled: 'Ask to update the docs...' },
|
||||
'app': { empty: 'Describe the app you want to create...', filled: 'Ask to modify your app...' }
|
||||
};
|
||||
@@ -1,864 +0,0 @@
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo, forwardRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
IconX,
|
||||
IconPlayerStop,
|
||||
IconCheck,
|
||||
IconCode,
|
||||
IconWand,
|
||||
IconStars,
|
||||
IconCornerDownLeft,
|
||||
IconChevronDown,
|
||||
IconHistory,
|
||||
IconPlus,
|
||||
IconTrash
|
||||
} from '@tabler/icons';
|
||||
import get from 'lodash/get';
|
||||
import find from 'lodash/find';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import { focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import {
|
||||
closeAiSidebar,
|
||||
sendAiMessage,
|
||||
stopAiStream,
|
||||
setChatBinding,
|
||||
startNewConversation,
|
||||
refreshChatHistory,
|
||||
openConversation,
|
||||
removeConversation,
|
||||
setMessageCodeStatus
|
||||
} from 'providers/ReduxStore/slices/chat';
|
||||
import {
|
||||
updateAppCode,
|
||||
updateRequestTests,
|
||||
updateRequestScript,
|
||||
updateResponseScript,
|
||||
updateRequestDocs,
|
||||
updateFolderRequestScript,
|
||||
updateFolderResponseScript,
|
||||
updateFolderTests,
|
||||
updateFolderDocs,
|
||||
updateCollectionRequestScript,
|
||||
updateCollectionResponseScript,
|
||||
updateCollectionTests,
|
||||
updateCollectionDocs
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, isItemAFolder, isItemARequest } from 'utils/collections';
|
||||
import { buildAiVariablesPayload, getAiStatus } from 'utils/ai';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import DiffView from './DiffView';
|
||||
import AssistantCodeBlock from './AssistantCodeBlock';
|
||||
import { PROCESSING_STAGES, CONTENT_TYPE_LABELS, SUGGESTIONS_BY_TYPE, PLACEHOLDER_BY_TYPE } from './constants';
|
||||
import { renderMarkdown, parseMessageSegments } from './utils';
|
||||
|
||||
const SELECTED_MODEL_LS_KEY = 'bruno.ai.chat.selectedModel';
|
||||
const AUTO_MODEL_ID = '';
|
||||
|
||||
const ToolActivityGroup = ({ activities }) => {
|
||||
if (!activities?.length) return null;
|
||||
const allDone = activities.every((a) => a.done);
|
||||
return (
|
||||
<div className={`tool-activity-log ${allDone ? 'completed' : ''}`}>
|
||||
{activities.map((activity, i) => (
|
||||
<div key={i} className={`tool-activity-item ${activity.done ? 'done' : 'active'}`}>
|
||||
<span className="tool-activity-indicator">
|
||||
{activity.done ? <IconCheck size={10} /> : <span className="tool-activity-spinner" />}
|
||||
</span>
|
||||
<span>{activity.label}{!activity.done ? '…' : ''}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const buildMessageTimeline = (cleanedContent, activities) => {
|
||||
if (!activities?.length) {
|
||||
return cleanedContent ? [{ type: 'text', content: cleanedContent }] : [];
|
||||
}
|
||||
if (!cleanedContent) return [{ type: 'tools', activities }];
|
||||
|
||||
const groups = [];
|
||||
for (const activity of activities) {
|
||||
const offset = Math.min(activity.textOffset || 0, cleanedContent.length);
|
||||
const last = groups[groups.length - 1];
|
||||
if (last && last.offset === offset) last.activities.push(activity);
|
||||
else groups.push({ offset, activities: [activity] });
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
let cursor = 0;
|
||||
for (const group of groups) {
|
||||
if (group.offset > cursor) {
|
||||
parts.push({ type: 'text', content: cleanedContent.substring(cursor, group.offset) });
|
||||
}
|
||||
parts.push({ type: 'tools', activities: group.activities });
|
||||
cursor = Math.max(cursor, group.offset);
|
||||
}
|
||||
if (cursor < cleanedContent.length) {
|
||||
parts.push({ type: 'text', content: cleanedContent.substring(cursor) });
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
|
||||
const formatRelativeTime = (timestamp) => {
|
||||
if (!timestamp) return '';
|
||||
const diff = Date.now() - timestamp;
|
||||
const minute = 60 * 1000;
|
||||
const hour = 60 * minute;
|
||||
const day = 24 * hour;
|
||||
if (diff < minute) return 'just now';
|
||||
if (diff < hour) return `${Math.floor(diff / minute)}m ago`;
|
||||
if (diff < day) return `${Math.floor(diff / hour)}h ago`;
|
||||
if (diff < 7 * day) return `${Math.floor(diff / day)}d ago`;
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
};
|
||||
|
||||
const HistoryPopover = ({ items, activeId, onPick, onDelete, onClose }) => {
|
||||
const popoverRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e) => {
|
||||
if (popoverRef.current && !popoverRef.current.contains(e.target)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
const handleKey = (e) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
document.addEventListener('keydown', handleKey);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClick);
|
||||
document.removeEventListener('keydown', handleKey);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div className="history-popover" ref={popoverRef} role="menu">
|
||||
{items.length === 0 ? (
|
||||
<div className="history-popover__empty">No past conversations</div>
|
||||
) : (
|
||||
items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`history-popover__item ${item.id === activeId ? 'is-active' : ''}`}
|
||||
role="menuitem"
|
||||
>
|
||||
<button className="history-popover__title" onClick={() => onPick(item.id)} title={item.title}>
|
||||
<span className="history-popover__title-text">{item.title || '(untitled)'}</span>
|
||||
<span className="history-popover__meta">{formatRelativeTime(item.updatedAt)}</span>
|
||||
</button>
|
||||
<button
|
||||
className="history-popover__delete"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); onDelete(item.id);
|
||||
}}
|
||||
title="Delete conversation"
|
||||
aria-label="Delete conversation"
|
||||
>
|
||||
<IconTrash size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AiChatSidebar = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [input, setInput] = useState('');
|
||||
const [processingStage, setProcessingStage] = useState(null);
|
||||
const [availableModels, setAvailableModels] = useState([]);
|
||||
const [selectedModel, setSelectedModel] = useState(() => {
|
||||
try { return localStorage.getItem(SELECTED_MODEL_LS_KEY) ?? AUTO_MODEL_ID; } catch { return AUTO_MODEL_ID; }
|
||||
});
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
const messagesEndRef = useRef(null);
|
||||
const messagesContainerRef = useRef(null);
|
||||
const isNearBottomRef = useRef(true);
|
||||
const textareaRef = useRef(null);
|
||||
|
||||
const isOpen = useSelector((state) => state.chat.isOpen);
|
||||
const allChats = useSelector((state) => state.chat.chats);
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const aiEnabled = get(preferences, 'ai.enabled', false);
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const activeItem = focusedTab && collection ? findItemInCollection(collection, activeTabUid) : null;
|
||||
|
||||
const aiContext = useMemo(() => {
|
||||
if (!focusedTab || !collection) return null;
|
||||
if (activeItem && (isItemARequest(activeItem) || activeItem.type === 'app')) {
|
||||
return { kind: 'request', item: activeItem, pathname: activeItem.pathname || '', name: activeItem.name || 'Untitled' };
|
||||
}
|
||||
if (activeItem && isItemAFolder(activeItem)) {
|
||||
return { kind: 'folder', folder: activeItem, pathname: activeItem.pathname || '', name: activeItem.name || 'Untitled' };
|
||||
}
|
||||
// Anything else (collection-settings, runner, variables, openapi-sync,
|
||||
// .js files in File Mode …) falls back to the collection root so the AI
|
||||
// button always opens a useful chat instead of a no-op.
|
||||
return { kind: 'collection', pathname: collection.pathname || '', name: collection.name || 'Untitled Collection' };
|
||||
}, [focusedTab, collection, activeItem]);
|
||||
|
||||
const currentChat = allChats[activeTabUid] || { messages: [], isLoading: false, error: null, historyList: [] };
|
||||
const { messages, isLoading, error, historyList, conversationId } = currentChat;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !aiEnabled) return;
|
||||
let cancelled = false;
|
||||
getAiStatus()
|
||||
.then((status) => {
|
||||
if (cancelled) return;
|
||||
setAvailableModels(status?.availableModels || []);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setAvailableModels([]);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [isOpen, aiEnabled, preferences?.ai]);
|
||||
|
||||
// Auto = empty string. We don't auto-correct to the first model — let the
|
||||
// backend pick, so users get smart defaults that adapt as providers change.
|
||||
useEffect(() => {
|
||||
if (selectedModel === AUTO_MODEL_ID) return;
|
||||
if (availableModels.length === 0) return;
|
||||
if (availableModels.some((m) => m.id === selectedModel)) return;
|
||||
setSelectedModel(AUTO_MODEL_ID);
|
||||
try { localStorage.setItem(SELECTED_MODEL_LS_KEY, AUTO_MODEL_ID); } catch {}
|
||||
}, [availableModels, selectedModel]);
|
||||
|
||||
const requestName = aiContext?.name || activeItem?.name || 'Untitled';
|
||||
const requestMethod = useMemo(() => {
|
||||
if (aiContext?.kind === 'folder') return 'FOLDER';
|
||||
if (aiContext?.kind === 'collection') return 'ROOT';
|
||||
if (!activeItem) return 'GET';
|
||||
if (activeItem.type === 'grpc-request') return 'GRPC';
|
||||
if (activeItem.type === 'ws-request') return 'WS';
|
||||
if (activeItem.type === 'graphql-request') return 'GQL';
|
||||
if (activeItem.type === 'app') return 'APP';
|
||||
const appOn = activeItem.draft
|
||||
? get(activeItem, 'draft.app.enabled', false)
|
||||
: get(activeItem, 'app.enabled', false);
|
||||
if (appOn) return 'APP';
|
||||
return activeItem.draft
|
||||
? get(activeItem, 'draft.request.method', 'GET')
|
||||
: get(activeItem, 'request.method', 'GET');
|
||||
}, [aiContext?.kind, activeItem]);
|
||||
|
||||
// contentType drives the AI prompt, the diff target, and which entry of
|
||||
// allContent the backend treats as "active". For requests it follows the
|
||||
// request-pane tab. For folders / collections we read the settings sub-tab
|
||||
// (and the inner pre/post script split for the Script sub-tab).
|
||||
const requestPaneTab = focusedTab?.requestPaneTab;
|
||||
const scriptPaneTab = focusedTab?.scriptPaneTab;
|
||||
const contentType = useMemo(() => {
|
||||
if (aiContext?.kind === 'folder') {
|
||||
const sub = collection?.folderLevelSettingsSelectedTab?.[aiContext.folder.uid];
|
||||
if (sub === 'test') return 'tests';
|
||||
if (sub === 'docs') return 'docs';
|
||||
if (sub === 'script') return scriptPaneTab === 'post-response' ? 'post-response' : 'pre-request';
|
||||
return 'pre-request';
|
||||
}
|
||||
if (aiContext?.kind === 'collection') {
|
||||
const sub = collection?.settingsSelectedTab;
|
||||
if (sub === 'tests') return 'tests';
|
||||
if (sub === 'overview') return 'docs';
|
||||
if (sub === 'script') return scriptPaneTab === 'post-response' ? 'post-response' : 'pre-request';
|
||||
return 'pre-request';
|
||||
}
|
||||
switch (requestPaneTab) {
|
||||
case 'tests': return 'tests';
|
||||
case 'script': return scriptPaneTab === 'post-response' ? 'post-response' : 'pre-request';
|
||||
case 'docs': return 'docs';
|
||||
default: return 'app';
|
||||
}
|
||||
}, [aiContext, collection?.folderLevelSettingsSelectedTab, collection?.settingsSelectedTab, requestPaneTab, scriptPaneTab]);
|
||||
|
||||
// Bind the chat to the active context's pathname so the history list
|
||||
// reflects this specific request/folder/collection and persistence keys stay
|
||||
// stable across sessions. Restoring the most recent conversation happens
|
||||
// once per tab — if the user explicitly starts a new chat, we don't
|
||||
// auto-replace it.
|
||||
const restoredOnceRef = useRef({});
|
||||
useEffect(() => {
|
||||
if (!isOpen || !aiContext || !collection) return;
|
||||
dispatch(setChatBinding({
|
||||
tabUid: activeTabUid,
|
||||
pathname: aiContext.pathname,
|
||||
collectionUid: collection.uid,
|
||||
contentType
|
||||
}));
|
||||
dispatch(refreshChatHistory(activeTabUid));
|
||||
}, [isOpen, aiContext?.pathname, collection?.uid, activeTabUid, contentType, dispatch]);
|
||||
|
||||
// First-open restore: if this tab has no conversation yet and there's a
|
||||
// saved one for the same file, load the most recent.
|
||||
useEffect(() => {
|
||||
if (!isOpen || !activeTabUid) return;
|
||||
if (restoredOnceRef.current[activeTabUid]) return;
|
||||
if (currentChat.conversationId) return;
|
||||
if (currentChat.messages?.length > 0) return;
|
||||
if (!historyList || historyList.length === 0) return;
|
||||
restoredOnceRef.current[activeTabUid] = true;
|
||||
dispatch(openConversation(activeTabUid, historyList[0].id));
|
||||
}, [isOpen, activeTabUid, currentChat.conversationId, currentChat.messages?.length, historyList, dispatch]);
|
||||
|
||||
const allContent = useMemo(() => {
|
||||
if (!aiContext) return {};
|
||||
if (aiContext.kind === 'request') {
|
||||
const item = aiContext.item;
|
||||
const draft = item.draft;
|
||||
const draftAppCode = get(item, 'draft.app.code');
|
||||
return {
|
||||
'app': draftAppCode != null ? draftAppCode : get(item, 'app.code', ''),
|
||||
'tests': draft ? get(draft, 'request.tests', '') : get(item, 'request.tests', ''),
|
||||
'pre-request': draft ? get(draft, 'request.script.req', '') : get(item, 'request.script.req', ''),
|
||||
'post-response': draft ? get(draft, 'request.script.res', '') : get(item, 'request.script.res', ''),
|
||||
'docs': draft ? get(draft, 'request.docs', '') : get(item, 'request.docs', '')
|
||||
};
|
||||
}
|
||||
if (aiContext.kind === 'folder') {
|
||||
const folder = aiContext.folder;
|
||||
const root = folder.draft || folder.root || {};
|
||||
return {
|
||||
'tests': get(root, 'request.tests', ''),
|
||||
'pre-request': get(root, 'request.script.req', ''),
|
||||
'post-response': get(root, 'request.script.res', ''),
|
||||
'docs': get(root, 'docs', '')
|
||||
};
|
||||
}
|
||||
// collection
|
||||
const root = collection?.draft?.root || collection?.root || {};
|
||||
return {
|
||||
'tests': get(root, 'request.tests', ''),
|
||||
'pre-request': get(root, 'request.script.req', ''),
|
||||
'post-response': get(root, 'request.script.res', ''),
|
||||
'docs': get(root, 'docs', '')
|
||||
};
|
||||
}, [aiContext, collection?.draft?.root, collection?.root]);
|
||||
|
||||
const currentContent = allContent[contentType] || '';
|
||||
|
||||
// requestContext (URL/method/headers/response shape) only makes sense for
|
||||
// HTTP-style request items. Folder, collection, and App chats skip it —
|
||||
// App items live under kind: 'request' but have no URL/method to surface.
|
||||
const requestContext = useMemo(() => {
|
||||
if (aiContext?.kind !== 'request' || !isItemARequest(aiContext.item)) return null;
|
||||
const item = aiContext.item;
|
||||
const draft = item.draft;
|
||||
return {
|
||||
url: draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', ''),
|
||||
method: draft ? get(item, 'draft.request.method', '') : get(item, 'request.method', ''),
|
||||
headers: draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []),
|
||||
params: draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []),
|
||||
body: draft ? get(item, 'draft.request.body', null) : get(item, 'request.body', null),
|
||||
docs: draft ? get(item, 'draft.request.docs', null) : get(item, 'request.docs', null),
|
||||
responseStatus: get(item, 'response.status', null),
|
||||
responseData: get(item, 'response.data', null)
|
||||
};
|
||||
}, [aiContext]);
|
||||
|
||||
// Variables payload is collection-scoped — works for request, folder, and
|
||||
// collection chats alike. Each entry is { name, value, scope, secret }; the
|
||||
// model gets a name-only preview in the prompt and can call search_variables
|
||||
// to fetch values (secrets come back redacted).
|
||||
const aiVariables = useMemo(() => {
|
||||
if (aiContext?.kind === 'request') return buildAiVariablesPayload(collection, aiContext.item);
|
||||
if (aiContext?.kind === 'folder') return buildAiVariablesPayload(collection, aiContext.folder);
|
||||
return buildAiVariablesPayload(collection, null);
|
||||
}, [collection, aiContext]);
|
||||
|
||||
const chatsWithMessages = useMemo(() => {
|
||||
if (!collection) return [];
|
||||
return Object.entries(allChats)
|
||||
.filter(([, chat]) => chat.messages?.length > 0)
|
||||
.map(([tabUid, chat]) => {
|
||||
if (tabUid === collection.uid) {
|
||||
return { id: tabUid, name: collection.name || 'Untitled Collection', method: 'ROOT', messageCount: chat.messages.length };
|
||||
}
|
||||
const item = findItemInCollection(collection, tabUid);
|
||||
if (!item) return null;
|
||||
if (isItemAFolder(item)) {
|
||||
return { id: tabUid, name: item.name || 'Untitled', method: 'FOLDER', messageCount: chat.messages.length };
|
||||
}
|
||||
const method = item.draft
|
||||
? get(item, 'draft.request.method', 'GET')
|
||||
: get(item, 'request.method', 'GET');
|
||||
return {
|
||||
id: tabUid,
|
||||
name: item.name || 'Untitled',
|
||||
method,
|
||||
messageCount: chat.messages.length
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}, [allChats, collection]);
|
||||
|
||||
const scrollToBottom = useCallback((behavior = 'smooth') => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior });
|
||||
}, []);
|
||||
|
||||
const handleMessagesScroll = useCallback(() => {
|
||||
const el = messagesContainerRef.current;
|
||||
if (!el) return;
|
||||
isNearBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNearBottomRef.current) return;
|
||||
const behavior = messages.some((m) => m.isStreaming) ? 'auto' : 'smooth';
|
||||
scrollToBottom(behavior);
|
||||
}, [messages, scrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) textareaRef.current?.focus();
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setProcessingStage(null);
|
||||
return;
|
||||
}
|
||||
const last = messages[messages.length - 1];
|
||||
if (last?.isStreaming && last.content) setProcessingStage('generating');
|
||||
else if (last?.isStreaming) setProcessingStage('thinking');
|
||||
else setProcessingStage('sending');
|
||||
}, [isLoading, messages]);
|
||||
|
||||
const handleTextareaChange = (e) => {
|
||||
setInput(e.target.value);
|
||||
const el = textareaRef.current;
|
||||
if (el) {
|
||||
el.style.height = 'auto';
|
||||
el.style.height = Math.min(el.scrollHeight, 150) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e?.preventDefault();
|
||||
if (!input.trim() || isLoading || availableModels.length === 0) return;
|
||||
|
||||
const text = input.trim();
|
||||
setInput('');
|
||||
setProcessingStage('sending');
|
||||
if (textareaRef.current) textareaRef.current.style.height = 'auto';
|
||||
|
||||
try {
|
||||
await dispatch(sendAiMessage(activeTabUid, text, allContent, requestContext, selectedModel, contentType, aiVariables));
|
||||
setProcessingStage('applying');
|
||||
setTimeout(() => setProcessingStage(null), 500);
|
||||
} catch (err) {
|
||||
console.error('Failed to send AI message:', err);
|
||||
setProcessingStage(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
dispatch(stopAiStream(activeTabUid));
|
||||
setProcessingStage(null);
|
||||
};
|
||||
|
||||
const handleApplyCode = (code, originalCode, messageIndex, msgContentType, writeIndex) => {
|
||||
if (!aiContext || code == null) return;
|
||||
const targetType = msgContentType || contentType;
|
||||
|
||||
// Bail if the live buffer has drifted from what the AI based the diff on.
|
||||
// The DiffView already disables the button in this case, but guarding here
|
||||
// too means the keyboard / programmatic path can't blow away local edits.
|
||||
const liveContent = allContent[targetType] || '';
|
||||
if (originalCode != null && liveContent !== originalCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aiContext.kind === 'request') {
|
||||
const payload = { itemUid: aiContext.item.uid, collectionUid: collection.uid };
|
||||
switch (targetType) {
|
||||
case 'tests': dispatch(updateRequestTests({ ...payload, tests: code })); break;
|
||||
case 'pre-request': dispatch(updateRequestScript({ ...payload, script: code })); break;
|
||||
case 'post-response': dispatch(updateResponseScript({ ...payload, script: code })); break;
|
||||
case 'docs': dispatch(updateRequestDocs({ ...payload, docs: code })); break;
|
||||
default: dispatch(updateAppCode({ ...payload, code })); break;
|
||||
}
|
||||
} else if (aiContext.kind === 'folder') {
|
||||
const payload = { folderUid: aiContext.folder.uid, collectionUid: collection.uid };
|
||||
switch (targetType) {
|
||||
case 'tests': dispatch(updateFolderTests({ ...payload, tests: code })); break;
|
||||
case 'pre-request': dispatch(updateFolderRequestScript({ ...payload, script: code })); break;
|
||||
case 'post-response': dispatch(updateFolderResponseScript({ ...payload, script: code })); break;
|
||||
case 'docs': dispatch(updateFolderDocs({ ...payload, docs: code })); break;
|
||||
// Folders / collections have no 'app' equivalent. Bail rather than
|
||||
// marking the diff accepted when nothing was dispatched.
|
||||
default: return;
|
||||
}
|
||||
} else {
|
||||
const payload = { collectionUid: collection.uid };
|
||||
switch (targetType) {
|
||||
case 'tests': dispatch(updateCollectionTests({ ...payload, tests: code })); break;
|
||||
case 'pre-request': dispatch(updateCollectionRequestScript({ ...payload, script: code })); break;
|
||||
case 'post-response': dispatch(updateCollectionResponseScript({ ...payload, script: code })); break;
|
||||
case 'docs': dispatch(updateCollectionDocs({ ...payload, docs: code })); break;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(setMessageCodeStatus({
|
||||
tabUid: activeTabUid,
|
||||
messageIndex,
|
||||
status: 'accepted',
|
||||
writeIndex
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRejectCode = (messageIndex, writeIndex) => {
|
||||
dispatch(setMessageCodeStatus({
|
||||
tabUid: activeTabUid,
|
||||
messageIndex,
|
||||
status: 'rejected',
|
||||
writeIndex
|
||||
}));
|
||||
};
|
||||
|
||||
const handleNewChat = () => {
|
||||
setHistoryOpen(false);
|
||||
restoredOnceRef.current[activeTabUid] = true; // suppress restore
|
||||
dispatch(startNewConversation({ tabUid: activeTabUid, contentType }));
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
const handlePickConversation = (id) => {
|
||||
setHistoryOpen(false);
|
||||
restoredOnceRef.current[activeTabUid] = true;
|
||||
dispatch(openConversation(activeTabUid, id));
|
||||
};
|
||||
|
||||
const handleDeleteConversation = (id) => {
|
||||
dispatch(removeConversation(activeTabUid, id));
|
||||
};
|
||||
|
||||
const handleClose = () => dispatch(closeAiSidebar());
|
||||
const handleSwitchChat = (tabUid) => dispatch(focusTab({ uid: tabUid }));
|
||||
|
||||
const handleSuggestionClick = (suggestion) => {
|
||||
setInput(suggestion);
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
const handleModelSelect = (modelId) => {
|
||||
setSelectedModel(modelId);
|
||||
try { localStorage.setItem(SELECTED_MODEL_LS_KEY, modelId); } catch {}
|
||||
};
|
||||
|
||||
const selectedModelLabel = useMemo(() => {
|
||||
if (selectedModel === AUTO_MODEL_ID) return 'Auto';
|
||||
return availableModels.find((m) => m.id === selectedModel)?.label || 'Auto';
|
||||
}, [availableModels, selectedModel]);
|
||||
|
||||
const ModelSelectorTrigger = forwardRef((props, ref) => (
|
||||
<div ref={ref} className="model-btn" {...props}>
|
||||
<IconStars size={14} strokeWidth={1.75} />
|
||||
<span>{selectedModelLabel}</span>
|
||||
<IconChevronDown size={12} />
|
||||
</div>
|
||||
));
|
||||
ModelSelectorTrigger.displayName = 'ModelSelectorTrigger';
|
||||
|
||||
const modelMenuItems = useMemo(
|
||||
() => [
|
||||
{ id: AUTO_MODEL_ID, label: 'Auto', onClick: () => handleModelSelect(AUTO_MODEL_ID) },
|
||||
...availableModels.map((model) => ({
|
||||
id: model.id,
|
||||
label: model.label,
|
||||
onClick: () => handleModelSelect(model.id)
|
||||
}))
|
||||
],
|
||||
[availableModels]
|
||||
);
|
||||
|
||||
const hasActiveStream = messages.some((m) => m.isStreaming);
|
||||
|
||||
const renderProcessingIndicator = () => {
|
||||
if (!processingStage || processingStage === 'thinking' || hasActiveStream) return null;
|
||||
const stage = PROCESSING_STAGES.find((s) => s.id === processingStage) || PROCESSING_STAGES[0];
|
||||
return (
|
||||
<div className="processing-indicator">
|
||||
<div className="processing-content">
|
||||
<div className="processing-icon">
|
||||
{stage.icon === 'sparkles' && <IconStars size={12} />}
|
||||
{stage.icon === 'wand' && <IconWand size={12} />}
|
||||
{stage.icon === 'code' && <IconCode size={12} />}
|
||||
{stage.icon === 'send' && <IconCornerDownLeft size={12} />}
|
||||
</div>
|
||||
<span className="processing-label">{stage.label}</span>
|
||||
<div className="processing-dots"><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
<div className="processing-bar"><div className="processing-bar-fill"></div></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMessage = (msg, index) => {
|
||||
const isUser = msg.role === 'user';
|
||||
const isStreaming = msg.isStreaming;
|
||||
const activities = msg.toolActivity || [];
|
||||
const hasPendingTool = activities.some((a) => !a.done);
|
||||
const content = msg.content || '';
|
||||
|
||||
const showThinking = isStreaming && !content && activities.length === 0;
|
||||
const showWorking = isStreaming && activities.length > 0 && !hasPendingTool;
|
||||
const timeline = buildMessageTimeline(content, activities);
|
||||
|
||||
return (
|
||||
<div key={index} className={`message ${msg.role} ${isStreaming ? 'streaming' : ''}`}>
|
||||
<div className="message-content">
|
||||
{isUser ? content : (
|
||||
<>
|
||||
{showThinking && (
|
||||
<div className="message-status">
|
||||
<span className="message-status__spinner" />
|
||||
<span>Thinking…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{timeline.map((part, partIndex) => {
|
||||
if (part.type === 'tools') {
|
||||
return <ToolActivityGroup key={`tools-${partIndex}`} activities={part.activities} />;
|
||||
}
|
||||
const segments = parseMessageSegments(part.content);
|
||||
const isLastTextPart = !timeline.slice(partIndex + 1).some((p) => p.type === 'text');
|
||||
return (
|
||||
<React.Fragment key={`text-${partIndex}`}>
|
||||
{segments.map((segment, segIndex) => {
|
||||
const isLastSegment = isLastTextPart && segIndex === segments.length - 1;
|
||||
if (segment.type === 'code') {
|
||||
return (
|
||||
<AssistantCodeBlock
|
||||
key={`p${partIndex}-s${segIndex}`}
|
||||
content={segment.content}
|
||||
language={segment.language}
|
||||
isOpen={segment.isOpen}
|
||||
isStreaming={isStreaming}
|
||||
isLast={isLastSegment}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={`p${partIndex}-s${segIndex}`} className="prose markdown-body">
|
||||
<div dangerouslySetInnerHTML={{ __html: renderMarkdown(segment.content) }} />
|
||||
{isStreaming && isLastSegment && <span className="cursor">|</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{showWorking && (
|
||||
<div className="message-status">
|
||||
<span className="message-status__spinner" />
|
||||
<span>Working…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isStreaming && msg.writes?.length > 0 && msg.writes.map((write, writeIdx) => {
|
||||
if (write.content === write.originalContent) return null;
|
||||
const liveContent = allContent[write.type] || '';
|
||||
const isStale = liveContent !== write.originalContent;
|
||||
const notRead = !write.wasRead;
|
||||
return (
|
||||
<DiffView
|
||||
key={`write-${writeIdx}`}
|
||||
originalCode={write.originalContent || ''}
|
||||
newCode={write.content}
|
||||
contentTypeLabel={CONTENT_TYPE_LABELS[write.type] || write.type}
|
||||
warning={
|
||||
notRead ? 'Content was not read first — changes may overwrite unrelated edits'
|
||||
: isStale ? 'Content has been modified since AI read it'
|
||||
: null
|
||||
}
|
||||
disableAccept={isStale || notRead}
|
||||
onAccept={() => handleApplyCode(write.content, write.originalContent, index, write.type, writeIdx)}
|
||||
onReject={() => handleRejectCode(index, writeIdx)}
|
||||
status={write.status}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{!isStreaming && !msg.writes && msg.code && msg.originalCode && msg.code !== msg.originalCode && (
|
||||
<DiffView
|
||||
originalCode={msg.originalCode || ''}
|
||||
newCode={msg.code}
|
||||
onAccept={() => handleApplyCode(msg.code, msg.originalCode, index, msg.contentType)}
|
||||
onReject={() => handleRejectCode(index)}
|
||||
status={msg.codeStatus}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isStreaming && msg.cancelled && (
|
||||
<div className="message-cancelled"><em>Cancelled</em></div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEmptyState = () => {
|
||||
const suggestions = SUGGESTIONS_BY_TYPE[contentType] || SUGGESTIONS_BY_TYPE.app;
|
||||
return (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon"><IconStars size={20} /></div>
|
||||
<h3>AI Assistant</h3>
|
||||
<p>Ask me to generate or modify code, tests, scripts, and docs.</p>
|
||||
<div className="suggestions">
|
||||
<p className="suggestions-title">Try asking:</p>
|
||||
<div className="suggestion-chips">
|
||||
{suggestions.map((s, i) => (
|
||||
<button key={i} className="suggestion-chip" onClick={() => handleSuggestionClick(s.prompt)}>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
if (!aiContext) return null;
|
||||
|
||||
const placeholders = PLACEHOLDER_BY_TYPE[contentType] || PLACEHOLDER_BY_TYPE.app;
|
||||
const placeholder = currentContent ? placeholders.filled : placeholders.empty;
|
||||
const historyCount = historyList?.length || 0;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="ai-sidebar">
|
||||
<div className="ai-sidebar-header">
|
||||
<div className="header-left">
|
||||
<IconStars size={18} className="header-icon" />
|
||||
<span className={`header-method method-${(requestMethod || 'get').toLowerCase()}`}>{requestMethod}</span>
|
||||
<span className="header-title">{requestName}</span>
|
||||
{chatsWithMessages.length > 1 && (
|
||||
<MenuDropdown
|
||||
items={chatsWithMessages.map((chat) => ({
|
||||
id: chat.id,
|
||||
label: `${chat.method} · ${chat.name}`,
|
||||
onClick: () => handleSwitchChat(chat.id)
|
||||
}))}
|
||||
placement="bottom-start"
|
||||
selectedItemId={activeTabUid}
|
||||
>
|
||||
<button className="chat-switcher-btn" title="Switch chat">
|
||||
<IconChevronDown size={14} />
|
||||
</button>
|
||||
</MenuDropdown>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<button
|
||||
className="icon-btn"
|
||||
onClick={handleNewChat}
|
||||
title="New chat"
|
||||
disabled={isLoading || messages.length === 0}
|
||||
>
|
||||
<IconPlus size={14} />
|
||||
</button>
|
||||
<div className="history-wrap">
|
||||
<button
|
||||
className={`icon-btn ${historyOpen ? 'is-active' : ''}`}
|
||||
onClick={() => setHistoryOpen((v) => !v)}
|
||||
title="History"
|
||||
disabled={historyCount === 0}
|
||||
>
|
||||
<IconHistory size={14} />
|
||||
</button>
|
||||
{historyOpen && (
|
||||
<HistoryPopover
|
||||
items={historyList || []}
|
||||
activeId={conversationId}
|
||||
onPick={handlePickConversation}
|
||||
onDelete={handleDeleteConversation}
|
||||
onClose={() => setHistoryOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<button className="icon-btn close-btn" onClick={handleClose} title="Close">
|
||||
<IconX size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ai-sidebar-messages" ref={messagesContainerRef} onScroll={handleMessagesScroll}>
|
||||
{messages.length === 0 ? renderEmptyState() : (
|
||||
<>
|
||||
{messages.map(renderMessage)}
|
||||
{renderProcessingIndicator()}
|
||||
</>
|
||||
)}
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
<div className="error-icon">!</div>
|
||||
<div className="error-text">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<div className="ai-sidebar-input">
|
||||
{availableModels.length === 0 ? (
|
||||
<div className="no-models-warning">
|
||||
No AI models available. Configure a provider and enable models in Preferences > AI.
|
||||
</div>
|
||||
) : (
|
||||
<div className="input-container">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={handleTextareaChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={isLoading}
|
||||
rows={1}
|
||||
/>
|
||||
<div className="input-actions">
|
||||
<div className="model-selector">
|
||||
<MenuDropdown items={modelMenuItems} placement="top-start" selectedItemId={selectedModel}>
|
||||
<ModelSelectorTrigger />
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<button className="stop-btn" onClick={handleStop} title="Stop generating">
|
||||
<IconPlayerStop size={12} /> Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="send-btn"
|
||||
onClick={handleSubmit}
|
||||
title="Send (Enter)"
|
||||
disabled={!input.trim()}
|
||||
>
|
||||
Send <IconCornerDownLeft size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AiChatSidebar;
|
||||
@@ -1,63 +0,0 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
|
||||
const SAFE_LANG = /^[a-z0-9_+#.-]+$/i;
|
||||
const safeLanguage = (lang) => (lang && SAFE_LANG.test(lang) ? lang : 'text');
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: false,
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
highlight: (str, lang) =>
|
||||
`<pre class="code-block"><code class="language-${safeLanguage(lang)}">${md.utils.escapeHtml(str)}</code></pre>`
|
||||
});
|
||||
|
||||
export const renderMarkdown = (content) => md.render(content || '');
|
||||
|
||||
export const parseMessageSegments = (content = '') => {
|
||||
if (!content) return [];
|
||||
|
||||
const segments = [];
|
||||
let cursor = 0;
|
||||
let inCode = false;
|
||||
let language = '';
|
||||
|
||||
while (cursor <= content.length) {
|
||||
const fenceIndex = content.indexOf('```', cursor);
|
||||
|
||||
if (fenceIndex === -1) {
|
||||
const chunk = content.slice(cursor);
|
||||
if (inCode || chunk) {
|
||||
segments.push({
|
||||
type: inCode ? 'code' : 'text',
|
||||
content: chunk,
|
||||
language,
|
||||
isOpen: inCode
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!inCode) {
|
||||
const textChunk = content.slice(cursor, fenceIndex);
|
||||
if (textChunk) {
|
||||
segments.push({ type: 'text', content: textChunk });
|
||||
}
|
||||
const fenceEnd = fenceIndex + 3;
|
||||
const lineEnd = content.indexOf('\n', fenceEnd);
|
||||
language = (lineEnd === -1 ? content.slice(fenceEnd) : content.slice(fenceEnd, lineEnd)).trim();
|
||||
inCode = true;
|
||||
cursor = lineEnd === -1 ? content.length : lineEnd + 1;
|
||||
} else {
|
||||
const codeChunk = content.slice(cursor, fenceIndex);
|
||||
if (codeChunk.trim()) {
|
||||
segments.push({ type: 'code', content: codeChunk, language, isOpen: false });
|
||||
}
|
||||
inCode = false;
|
||||
language = '';
|
||||
cursor = fenceIndex + 3;
|
||||
if (content[cursor] === '\n') cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return segments.filter((seg) => seg.content && seg.content.trim());
|
||||
};
|
||||
@@ -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,870 +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 {
|
||||
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);
|
||||