Compare commits

..

223 Commits

Author SHA1 Message Date
Anoop M D
ff3321d643 chore: bumped version to 0.22.1 2023-10-10 04:40:03 +05:30
Anoop M D
659a71491d publish: bru lang v0.7.0 2023-10-10 04:38:56 +05:30
Anoop M D
3d366477d0 feat(#309): use dotenv for .env file parsing 2023-10-10 04:22:21 +05:30
Anoop M D
beaa20c134 fix(#491): fix response overlay scroller 2023-10-10 01:08:00 +05:30
Anoop M D
53e49ffdd3 Merge pull request #476 from Levminer/main
Replace macOS icon
2023-10-10 00:11:48 +05:30
Anoop M D
9ee398dc3b Merge pull request #480 from grubersjoe/gql-validation
Fix GraphQL validation
2023-10-10 00:11:24 +05:30
Anoop M D
c4a21e1089 Merge pull request #492 from lared/bugfix/set-up-ajv-in-tests
fix(#473) add Ajv and node-fetch to the test runtime
2023-10-10 00:08:53 +05:30
Michał Szymborski
2b25a0be18 fix(#473) add Ajv to the test runtime 2023-10-09 20:23:07 +02:00
Anoop M D
524c3f8445 Merge pull request #488 from lared/bugfix/set-up-ajv-in-scripts
fix(#473) add Ajv to the runtime
2023-10-09 22:15:17 +05:30
Michał Szymborski
c67cf6cca2 fix(#473) add Ajv to the runtime 2023-10-09 18:41:29 +02:00
Anoop M D
33da3a1303 Merge pull request #486 from Its-treason/fix/r-replace-not-a-function
fix(#382): TypeError r.replace is not a function
2023-10-09 21:31:33 +05:30
Its-treason
c586deeb30 fix(#382): TypeError r.replace is not a function 2023-10-09 17:56:48 +02:00
Anoop M D
051dac02cd Merge pull request #475 from Sl-Alex/bugfix/assertions-modify-value
Fix assertion value modification
2023-10-09 19:19:29 +05:30
Jonathan Gruber
0668331822 fix(#222): harden content security policy and allow loading inline images 2023-10-09 15:45:06 +02:00
Lőrik Levente
0ad2186c43 Replace macos icon 2023-10-09 14:10:14 +02:00
Jonathan Gruber
5db50339e0 fix(#222): add missing codemirror/addon/lint/lint import to fix GQL validation 2023-10-09 13:09:45 +02:00
Jonathan Gruber
2df7f63ba9 update package-lock.json 2023-10-09 13:08:34 +02:00
Oleksii Slabchenko
0e1c4008e7 Fix assertion value modification 2023-10-09 11:41:46 +02:00
Anoop M D
239caa1763 chore: published bru cli v0.14.0 2023-10-09 08:27:32 +05:30
Anoop M D
362289b7cd feat(#334): bru cli support for collection headers, auth, scripts and tests 2023-10-09 08:26:10 +05:30
Anoop M D
f9f1ca0640 chore: version bump 0.22.0 2023-10-09 07:30:37 +05:30
Anoop M D
53eb53e062 chore: collection and folder as request names are reserved 2023-10-09 07:08:36 +05:30
Anoop M D
2d149d94ef Merge pull request #456 from survivant/feature/request-indexname
Feature/request indexname
2023-10-09 07:00:50 +05:30
Anoop M D
f0c3b8a877 Merge pull request #467 from game5413/enhancement/collection-dropdown
Better experience for open request or folder dropdown action
2023-10-09 06:52:13 +05:30
Anoop M D
3a222458d1 Merge pull request #468 from rqbazan/feat/add-support-for-socks5-proxy
feat: add support for socks5 proxy
2023-10-09 06:40:32 +05:30
Anoop M D
46bc097733 Merge branch 'main' into feat/add-support-for-socks5-proxy 2023-10-09 06:40:12 +05:30
Anoop M D
1ce8d707f1 feat(#334): collection level headers, auth, scripts and tests 2023-10-09 06:18:05 +05:30
Ricardo Q. Bazan
bf90ee96e5 chore: add support for cli 2023-10-08 12:09:33 -05:00
Ricardo Q. Bazan
ce2e997c06 feat: add support for socks5 proxy 2023-10-08 10:04:16 -05:00
game5413
7617ae12cb Merge branch 'main' into enhancement/collection-dropdown 2023-10-08 21:48:36 +07:00
game5413
905e993a13 better experience for open dropdown collection 2023-10-08 21:46:13 +07:00
Anoop M D
159dd90b03 fix: fixed issue where window width and height were not set for first time users 2023-10-08 05:26:49 +05:30
Anoop M D
c222bf47c3 Merge pull request #452 from jarne/feature/persist-window-state
Persist window state (size, position)
2023-10-08 04:36:09 +05:30
Anoop M D
c91bc0d1c6 Merge pull request #457 from Sl-Alex/bugfix/between-assert
Fix 'between' assertion
2023-10-08 04:32:12 +05:30
Anoop M D
64ac763a13 Merge pull request #441 from grubersjoe/gql-auth
Improve GQL introspection request
2023-10-08 04:17:00 +05:30
Anoop M D
bca3d5749b Merge pull request #447 from therealrinku/two
minor styling fixes
2023-10-08 04:15:33 +05:30
Anoop M D
2ee6759282 Merge pull request #455 from survivant/patch-1
typo in the error message
2023-10-08 04:10:58 +05:30
Anoop M D
c479c2e786 Merge pull request #459 from lared/feature/show-devtools
fix(#440) open dev tools from menu
2023-10-08 04:10:16 +05:30
Michał Szymborski
6d8bdfa276 fix(#440) open dev tools from menu 2023-10-07 23:42:12 +02:00
Oleksii Slabchenko
024f61a95e Fix 'between' assertion 2023-10-07 22:42:13 +02:00
Anoop M D
856236c918 feat(#334): Bru lang updates for collection.bru file 2023-10-08 01:57:30 +05:30
Sebastien Dionne
0179c58cd2 fix typo and use singular 2023-10-07 15:54:33 -04:00
Sebastien Dionne
515381b930 fix typo and use singular 2023-10-07 15:53:34 -04:00
Sebastien Dionne
5ef0de2e4d Revert "Building electron script in js to support different OS"
This reverts commit ae78ed36ef.
2023-10-07 15:33:38 -04:00
Anoop M D
c6fef2f1be chore: removed legacy code 2023-10-08 01:01:40 +05:30
Sebastien Dionne
21536f9a27 Support request name with "index" in the name if index is not separated 2023-10-07 15:28:17 -04:00
Anoop M D
faf8581ddf feat(#253): hide hotkey 2023-10-08 00:55:09 +05:30
Sebastien Dionne
47992288c4 Use singular instead 2023-10-07 15:04:45 -04:00
Sebastien Dionne
640692abc7 typo in the error message
there was a little typo
2023-10-07 14:59:55 -04:00
Anoop M D
ed95b8349e chore: bump version v0.21.1 2023-10-08 00:05:10 +05:30
Anoop M D
81b1982d4f feat: response overlay polish 2023-10-08 00:03:32 +05:30
Anoop M D
e381d0c00b chore: ui layout polish 2023-10-07 19:12:20 +05:30
Anoop M D
50f2f54335 fix(#150): fixed arrow direction 2023-10-07 19:10:11 +05:30
Jarne
fab350a32d Add some checks if position/size is inside screen border 2023-10-07 14:51:47 +02:00
Jarne
4600217606 Add basic window state persistance 2023-10-07 14:27:24 +02:00
therealrinku
6421148dc1 minor styling fixes 2023-10-07 12:50:19 +05:45
Anoop M D
12f3f802a7 request save control 2023-10-07 09:02:19 +05:30
Sebastien Dionne
ae78ed36ef Building electron script in js to support different OS 2023-10-06 22:17:10 -04:00
Anoop M D
1e27427d09 chore: bump version to 0.21.0 2023-10-07 03:28:11 +05:30
Jonathan Gruber
f2a8bd5d10 chore: update package-lock.json 2023-10-06 23:57:50 +02:00
Anoop M D
860a3b16ad fix(#440): bring back menu bar 2023-10-07 03:26:12 +05:30
Anoop M D
8fadaf98ae feat(#306): bru cli v0.13.0 release 2023-10-07 03:23:39 +05:30
Anoop M D
8f1f41374c feat(#306): module whitelisting support 2023-10-07 03:20:44 +05:30
Anoop M D
e3679c9ee9 feat(#306): module whitelisting support 2023-10-07 03:19:02 +05:30
Jonathan Gruber
7f17999486 fix(#354): add set headers to introspection request 2023-10-06 23:48:40 +02:00
Jonathan Gruber
d2b35beb6f fix(#354): use Handlebars instead of deprecated Mustache 2023-10-06 23:23:57 +02:00
Anoop M D
0f3a8a87bb feat(#306): bru cli support for allowScriptFilesystemAccess 2023-10-07 02:41:56 +05:30
Jonathan Gruber
3c0770b792 fix(#354): also interpolate env vars in introspection request 2023-10-06 23:10:53 +02:00
Anoop M D
5a89d12716 feat: removed legacy v1 bru lang auto migrate functionality 2023-10-07 00:25:26 +05:30
Anoop M D
8730c5a85b Merge pull request #413 from therealrinku/one
request colors made consistent on the sidebar and tabs
2023-10-06 23:34:08 +05:30
Anoop M D
24fcb8caf2 Merge pull request #430 from lared/bugfix/misordered-requests
fix(#428): moving requests into directories or into root of collection broke ordering
2023-10-06 22:58:08 +05:30
Anoop M D
5a0e68073f Merge pull request #436 from not-known-person/fix-/#419
fix-/#419
2023-10-06 22:56:17 +05:30
not-known-person
945f1eb74a fix-/#419 2023-10-06 22:00:20 -04:00
Anoop M D
de74edb50f feat: interpolation of proxy vars 2023-10-06 22:49:48 +05:30
Anoop M D
9474918853 chore: fixed typo 2023-10-06 22:11:14 +05:30
Anoop M D
6db36513c5 Merge pull request #434 from oscarpas/fix/graphql-docs-explorer-styling
fix: add missing CSS for GraphQL docs explorer
2023-10-06 21:43:57 +05:30
Oscar Pastarus
7be38bcfe0 fix: add missing CSS for graphql docs explorer 2023-10-06 17:46:09 +02:00
Michał Szymborski
1f5aa25d5e fix(#428): moving requests into directories or into root of collection broke ordering 2023-10-06 16:39:13 +02:00
Anoop M D
65e448b1eb Merge pull request #374 from acostinescu/main
Changed Proxied Requests to Use https-proxy-agent/http-proxy-agent.
2023-10-06 17:55:39 +05:30
Anoop M D
d5a6522563 Merge branch 'main' into main 2023-10-06 17:36:35 +05:30
therealrinku
be72d24ecb request colors made consistent on the sidebar and tabs 2023-10-06 14:57:20 +05:45
Anoop M D
c25542bbdf chore: bru cli v0.12.0 release 2023-10-06 04:59:57 +05:30
Anoop M D
a838185ddf chore: version bump v0.20.0 2023-10-06 04:18:44 +05:30
Anoop M D
ac3637fcfa Merge pull request #404 from qweme32/feature/russian-localization
Add Russian localization
2023-10-06 04:15:22 +05:30
Anoop M D
e3f32dfc87 Merge pull request #397 from dozed/response-time-axios
Use axios interceptor to measure response time
2023-10-06 04:13:52 +05:30
Anoop M D
a204e3814b Merge pull request #400 from lared/bug/env-with-empty-secret-variable
fix(#399): loading environment with empty secret variable fails
2023-10-06 04:11:57 +05:30
Anoop M D
be8db1876a feat(#111): refactor new request auto open logic 2023-10-06 04:02:51 +05:30
Qweme Dev
64d9413777 Add Russian localization 2023-10-05 22:47:14 +03:00
Michał Szymborski
a71afc8f73 fix(#399): loading environment with empty secret variable fails 2023-10-05 19:38:17 +02:00
Anoop M D
4e4130acf5 Merge pull request #373 from Its-treason/feature/auto-open-new-request
feat(#111): Automaticly open a newly created request
2023-10-05 21:22:56 +05:30
Stefan Ollinger
bb7d13d2d9 Use axios interceptor to measure response time 2023-10-05 17:46:12 +02:00
Anoop M D
c08be14d87 Merge branch 'main' of github.com:usebruno/bruno 2023-10-05 21:15:59 +05:30
Anoop M D
90f47e5877 feat(#396): decomment support in json and scripts 2023-10-05 21:15:38 +05:30
Its-treason
8a19189dcd feat(#111): Use existing last actions for auto open tabs 2023-10-05 17:43:17 +02:00
Anoop M D
efbc119e7c Merge pull request #395 from dozed/fix-show-response-time
Put constant in outer scope
2023-10-05 20:50:00 +05:30
Anoop M D
d36956e0a4 Merge pull request #393 from zyrouge/linux-qol
Better BrowserWindow
2023-10-05 20:41:31 +05:30
Stefan Ollinger
efbdde8252 Set responseTime 2023-10-05 17:11:29 +02:00
Stefan Ollinger
ae2f170fe6 Put constant in outer scope 2023-10-05 17:05:31 +02:00
zyrouge
4f0d87fba4 refactor: set title 2023-10-05 20:29:50 +05:30
zyrouge
8703124007 refactor: enable auto hide menu bar 2023-10-05 20:24:02 +05:30
zyrouge
f9f99adbf9 refactor: set icon of windows 2023-10-05 20:23:33 +05:30
Anoop M D
da9a6c434a Merge pull request #390 from grubersjoe/gql-auth
feat: Add authentication for GraphQL requests
2023-10-05 20:21:58 +05:30
Anoop M D
c8764f6555 Merge pull request #391 from dozed/show-response-time
Show response time in milliseconds per request and total
2023-10-05 20:17:58 +05:30
Stefan Ollinger
e90718f47b Show response time in milliseconds per request and total 2023-10-05 16:42:49 +02:00
Jonathan Gruber
af130bac17 feat(usebruno#354): Add authentication for GraphQL requests
Schema introspection query will also use specified authentication.
2023-10-05 16:37:05 +02:00
Stefan Ollinger
04481a26e1 Show response time in milliseconds per request and total 2023-10-05 16:36:57 +02:00
Anoop M D
86c2a60184 Merge pull request #380 from petoc/bugfix/insomnia-importer
updated insomnia importer
2023-10-05 19:54:06 +05:30
Anoop M D
0cbf803ed9 chore: restructured tests into seperate dir as jest error was preventing local exec of cli 2023-10-05 19:51:06 +05:30
Anoop M D
c91819c072 Merge branch 'main' of github.com:usebruno/bruno 2023-10-05 19:40:44 +05:30
Anoop M D
2c4a3a5eb4 Merge pull request #241 from tpyle/bug/correct-result-reporting
Handle failed requests and reduce duplication
2023-10-05 19:40:28 +05:30
Anoop M D
99fc980a48 chore: updated readme 2023-10-05 19:30:38 +05:30
Anoop M D
960f62ac8e chore: updated readme 2023-10-05 15:44:34 +05:30
Peter C.
57e0f0c0c4 updated insomnia importer 2023-10-05 09:45:08 +02:00
Alex Costinescu
8216bf5eec Merge remote-tracking branch 'upstream/main' 2023-10-04 20:37:18 -04:00
Alex Costinescu
9f11cfc836 Changed proxied requests to use https-proxy-agent to handle TCP tunneling. 2023-10-04 20:31:34 -04:00
Its-treason
51cb930b6a feat(#111): Automaticly open a newly created request 2023-10-05 01:57:02 +02:00
Anoop M D
1c8712b6eb chore: bru cli v0.11.0 release 2023-10-05 04:04:05 +05:30
Anoop M D
6c1f8c78b2 chore: version bumped to v0.19.0 2023-10-05 03:47:43 +05:30
Anoop M D
b35b814561 chore: added package-lock.json 2023-10-05 03:46:34 +05:30
Anoop M D
80eaaad658 Merge pull request #356 from therealrinku/main
github star button color fix
2023-10-05 03:35:38 +05:30
Anoop M D
51a73d864e Merge pull request #368 from luizfonseca/fix/use-correct-duration-axios-interceptor
fix(network): use axios interceptors to better reflect durations
2023-10-05 03:34:34 +05:30
Anoop M D
87e29db545 Merge pull request #370 from VersusF/feature/copy-environment
Feature: Add new button to copy existing environments
2023-10-05 03:29:31 +05:30
Anoop M D
0cf18e6804 feat(#306): allow fs access inside scripts 2023-10-05 03:20:53 +05:30
Filippo Contro
3a6dacc1f4 Add: Add new button to copy existing environments 2023-10-04 23:32:02 +02:00
Anoop M D
8d89496304 fix(#329): fixed basic auth header issue + added cli support for basic and bearer auth 2023-10-05 02:32:24 +05:30
Luiz Fonseca
6ef7891f8a fix(network): use interceptors when measuring request duration 2023-10-04 22:09:48 +02:00
Luiz Fonseca
e1bf475f1f chore: pretty-quick fixes 2023-10-04 22:09:11 +02:00
Luiz Fonseca
f7eb325e11 fix: husky pre-commit missing correct prettier version 2023-10-04 22:08:25 +02:00
Luiz Fonseca
c50b88d60d fix: missing typescript module on npm build script 2023-10-04 22:07:26 +02:00
Anoop M D
90fedc8ec6 fix(#334): fixed yaml response parsing issue 2023-10-04 22:55:17 +05:30
therealrinku
9c46bc79d9 github star button color fix 2023-10-04 22:14:34 +05:45
Anoop M D
9ecfb3a640 chore: added testimnoial link in readme 2023-10-04 18:14:45 +05:30
Thomas Pyle
da4e96d1ef Merge remote-tracking branch 'upstream/main' into bug/correct-result-reporting 2023-10-03 22:15:53 -04:00
Anoop M D
bf2d1220a1 chore: version bumped to v0.18.0 2023-10-04 05:53:59 +05:30
Anoop M D
744abe585c revert pr #252 2023-10-04 05:52:59 +05:30
Anoop M D
5e2640fe73 fix: fixed query result issue 2023-10-04 05:48:05 +05:30
Anoop M D
7513314179 Merge branch 'main' of github.com:usebruno/bruno 2023-10-04 04:20:52 +05:30
Anoop M D
d4b663bfa8 Merge pull request #252 from acostinescu/main
Changed Proxied Requests to Use https-proxy-agent.
2023-10-04 04:20:38 +05:30
Anoop M D
9b2e2ed3d8 chore: style updates 2023-10-04 04:11:23 +05:30
Anoop M D
e4ea6b9109 Merge pull request #299 from ilumin/feature/double-click-rename
feat(291): add double click request to open rename modal
2023-10-04 03:40:29 +05:30
Anoop M D
1a8feb8029 Merge pull request #282 from Its-treason/feature/preview-response-html
feature/preview-response-html
2023-10-04 03:40:11 +05:30
Anoop M D
72a5f05009 Merge pull request #302 from j0k3r/bugfix/reponse-size-two-decimal
Fix rounding response size
2023-10-04 03:32:30 +05:30
Anoop M D
d5ef240de6 Merge pull request #308 from Its-treason/bugfix/assert-not-being-removed
fix(#270): Fix deleted assertions & tests shown in test tab
2023-10-04 03:29:44 +05:30
Anoop M D
1932d98b57 Merge pull request #321 from lared/bugfix/tabbing-order
fix(#314): tabbing order through tables (variables etc) was inconsistent or impossible
2023-10-04 03:26:00 +05:30
Anoop M D
bdb7ff9947 chore: style updates 2023-10-04 03:07:50 +05:30
Anoop M D
3f4f7fb24c feat(#119): basic auth support completed 2023-10-04 02:58:22 +05:30
Michał Szymborski
494b484cd9 fix(#312): tabbing order through tables (variables etc) was inconsistent or impossible
resolves #312
2023-10-03 21:36:31 +02:00
Anoop M D
48e4e60696 feat(#119): bearer auth support completed 2023-10-04 00:36:52 +05:30
Anoop M D
2e600838b2 Merge branch 'main' of github.com:usebruno/bruno 2023-10-03 22:48:56 +05:30
Anoop M D
52428c6b35 Merge pull request #318 from usebruno/feature/auth-support
Feature/auth support
2023-10-03 22:47:22 +05:30
Its-treason
2734f26c78 fix(#270): Fix deleted assertions & tests shown in test tab
After all tests or assertions were deleted the store was not
updated because of the length checks. Now the assertions/tests
will always try to run, to ensure the data is always updated.
2023-10-03 13:04:34 +02:00
Jeremy Benoist
73c62010b7 Fix rounding response size
I noticed that sometimes the response size is weirdly displayed.
- size `112932` is displayed as `110.28.999999999999996KB`
- size `112990` is displayed as `110.34KB`

The problem is in the decimal calculation. Rounding the value ensure we don't have two `.` in the displayed size.

Also, update the `contributing` to increase the minimum Node version required.
2023-10-03 10:39:38 +02:00
O Teerasak Vichadee
26853787da feat(291): add double click request to open rename modal 2023-10-03 11:28:27 +07:00
Anoop M D
712319ff34 Merge pull request #296 from Its-treason/bugfix/modal-text-color
fix: Color in environment delete modal
2023-10-03 04:36:21 +05:30
Its-treason
7a80247fc5 fix: Color in environment delete modal 2023-10-03 00:48:49 +02:00
Its-treason
ea9e294c54 feat(#245): Update tab design + Remove CSP 2023-10-02 19:50:01 +02:00
Anoop M D
55315b5b88 Merge pull request #288 from Cibico99/feature/xml-format-option-fix
Fix for XML Formatting Options
2023-10-02 20:34:10 +05:30
pedward99
d63e7371fe Fix for XML Formatting Options 2023-10-02 10:39:29 -04:00
Its-treason
dce11d1bd5 Merge branch 'main' of github.com:usebruno/bruno into feature/preview-response-html 2023-10-02 14:34:54 +02:00
Its-treason
e720fed63b feat(#245): Add HTML preview to response 2023-10-02 14:26:24 +02:00
Anoop M D
43f7c2ab86 chore: version bump to v0.17.0 2023-10-02 16:55:22 +05:30
Anoop M D
95532102ba feat(#280): code generator polishing and support url interpolation 2023-10-02 16:52:02 +05:30
Anoop M D
4603ec4d5e Merge pull request #280 from Beedhan/feature/code-generator
Feature/code generator
2023-10-02 15:42:07 +05:30
Anoop M D
cbdd56e577 chore: added some todo's for future code cleanup 2023-10-02 15:38:50 +05:30
Anoop M D
ce77d08408 Merge branch 'main' of github.com:usebruno/bruno 2023-10-02 15:32:39 +05:30
Anoop M D
3a5071412e Merge pull request #277 from not-known-person/feat-/#220-Improvement
Improved feat(#220)
2023-10-02 15:32:26 +05:30
not-known-person
336ad38cb9 Improved feat-/#220 2023-10-02 08:34:39 -04:00
Anoop M D
636e14a385 feat(#253): released cmd+h key back to native mac hide behaviour 2023-10-02 15:28:54 +05:30
Anoop M D
bfc03f5ae4 fix(#279): fixed codemirror crashes resulting in white screen 2023-10-02 15:25:59 +05:30
Beedhan
97200961eb feat: support for variables in url 2023-10-02 14:24:13 +05:45
Anoop M D
7297bb184e chore: updated readme 2023-10-02 13:58:09 +05:30
Beedhan
b74922c8f3 bugfix:sidebar moving when code generator modal is opened 2023-10-02 14:02:54 +05:45
Anoop M D
ce0827308f chore: updated readme 2023-10-02 13:47:14 +05:30
Beedhan
3d0c9cc0ae feat:added code generator to http-requests 2023-10-02 13:58:25 +05:45
Anoop M D
1804454ff0 chore: bump version v0.16.6 2023-10-02 05:26:47 +05:30
Anoop M D
3c710120b9 Merge pull request #274 from lared/bugfix/drag-and-drop-ordering
fix(#154): correct ordering during drag-and-drop
2023-10-02 04:48:43 +05:30
Anoop M D
fcc12fb089 fix(#251, #265): phantoms folders fix on rename/delete needs to be run only on windows 2023-10-02 04:17:35 +05:30
Anoop M D
3d8dee944f Merge pull request #266 from not-known-person/fix-251-265
fixed issue-#251-&-#265,
2023-10-02 04:03:31 +05:30
Anoop M D
78e5cd3c03 Merge pull request #264 from Its-treason/bugfix/insomnia-import-name-collision
fix(#257): Name collision during Insomnia collection import
2023-10-02 03:37:40 +05:30
Its-treason
26d99c7aee fix(#257): Name collision during Insomnia collection import 2023-10-01 23:47:43 +02:00
Anoop M D
77a7318dfb Merge pull request #272 from not-known-person/feat/-#220
added feat/-#220
2023-10-02 02:55:24 +05:30
not-known-person
b83da46f12 added feat/-#220 2023-10-01 21:01:39 -04:00
not-known-person
cf6ec4e84f fixed issue-#251-&-#265 2023-10-01 15:22:05 -04:00
Michał Szymborski
978d810473 fix(#154): correct ordering during drag-and-drop
When dragging and dropping items, to delay changing sequence numbers
until after all the resource-dependent logic has completed, we were
relying on the order of children in redux store (which we then converted
into new seq numbers).

This order of children was however not updated when sequence numbers
changed (for example due to file watch changes). This resulted in a
seemingly random drag-and-drop ordering, which in fact was linked to the
initial order when the collection was loaded.

This change sorts all the items by sequence number prior to reordering,
so that those random jumps no longer happen. As this happens on a deep
clone of the collection, no data gets hurt in the process.

fixes #154
2023-10-01 21:09:11 +02:00
Beedhan
cedcd2cf35 Revert "fix: folder showing after deleting"
This reverts commit ce9cdc5293.
2023-10-02 00:00:03 +05:45
Beedhan
ce9cdc5293 fix: folder showing after deleting 2023-10-01 23:55:39 +05:45
Anoop M D
e83c2da798 chore: version bumped to v0.16.5 2023-10-01 02:17:28 +05:30
Anoop M D
39f148267e Merge pull request #262 from mirkogolze/feature/about-window
#203 #254 #231 add icon to asar content, change styling to allow copying the content
2023-10-01 01:51:32 +05:30
Mirko Golze
f4f093d4db #203 #254 #231
add icon to asar content, change styling to allow copying the content
2023-09-30 21:56:21 +02:00
Anoop M D
3c4ef2f2df Merge pull request #256 from brahma-dev/main
Ensure that adjacent variables are rendered in separate spans.
2023-10-01 00:19:46 +05:30
Anoop M D
e39975cb3c Merge pull request #259 from sadn1ck/fix/gql-docs-build-error
fix(graphql-docs/build): rollup error thrown during build
2023-10-01 00:12:12 +05:30
Anoop M D
b767ccd063 Merge pull request #260 from Its-treason/feature/update-create-collection-form
feat: Update create collection form
2023-09-30 22:09:54 +05:30
Anoop M D
c5adfd8975 Merge pull request #261 from andypiper/bugfix/grammar-typo
Remove rogue apostrophes and capitalise API in text.
2023-09-30 22:05:01 +05:30
Andy Piper
6695d90609 Remove rogue apostrophes and capitalise API in text.
Signed-off-by: Andy Piper <andypiper@users.noreply.github.com>
2023-09-30 15:37:45 +01:00
Its-treason
5c79282a1b feat: Update create collection form
- Remove Name tooltip
- Update Folder Name tooltip
- Move Folder Name input under location
- Update Folder Name validation
  - Now only allow characters for valid system folder names
- Update label htmlFor ids to input ids
2023-09-30 14:35:37 +02:00
Anik Das
64019f8ecf fix(graphql-docs/build): rollup error thrown during build
- during the dts transformation, the css import was not recognized, hence marking it as external in the dts transform didn't throw the error

- "extract: true" in postcss plugin makes sure it gets extracted to the final bundle as well

Signed-off-by: Anik Das <anikdas0811@gmail.com>
2023-09-30 09:49:02 +05:30
Brahma Dev
4c89f31934 Decrease likelihood of any unintentional classname clash. 2023-09-29 21:58:46 +00:00
Brahma Dev
1c53ce91f0 Ignore the randomly generated classname. 2023-09-29 21:42:15 +00:00
Brahma Dev
0b83fbb7ce Add a randomly generated classname to each variable so that CodeMirror does not merge adjacent variables into the same SPAN. 2023-09-29 21:41:54 +00:00
Anoop M D
08ceed86a8 chore: bump version to v0.16.4 2023-09-30 02:48:20 +05:30
Anoop M D
f99918d725 fix(#229): fixed handling of non-JSON/XML content types 2023-09-30 02:44:30 +05:30
Anoop M D
1877dd858e chore: remove unused var 2023-09-30 02:42:10 +05:30
Anoop M D
acff0c379e fix(#236): insomnia import fix 2023-09-30 02:24:31 +05:30
Anoop M D
a849e4fb7b Merge pull request #255 from Its-treason/bugfix/create-collection-modal-validation
Bugfix: Create collection modal validation
2023-09-30 01:53:32 +05:30
Anoop M D
613699fb69 fix(#248): gracefully abort cm header hint when word match fails 2023-09-30 01:44:54 +05:30
Anoop M D
0c4ba71922 feat(#229): added error boundary to capture error trace and allow users to continue using the app 2023-09-30 01:32:05 +05:30
Its-treason
d346bb00eb Merge branch 'main' into bugfix/create-collection-modal-validation 2023-09-29 21:45:01 +02:00
Its-treason
8bb57aa41d fix: location validation in create collection form 2023-09-29 21:44:37 +02:00
Anoop M D
f378f04fc3 Merge pull request #248 from brahma-dev/main
Autocomplete
2023-09-30 00:35:22 +05:30
Alex Costinescu
742d69b03c Re-added missing https require statement. 2023-09-29 15:37:39 +02:00
Alex Costinescu
0e3bc62d9d Changed proxied requests to use https-proxy-agent to handle TCP tunneling. 2023-09-29 15:24:13 +02:00
Brahma Dev
a02d2b9c58 Add standard http headers for autocomplete 2023-09-29 11:59:09 +00:00
Brahma Dev
21edfbc25a Use Codemirror Hint feature for autocomplete 2023-09-29 11:59:01 +00:00
Thomas Pyle
4ba4d8fc27 Merge branch 'main' into bug/correct-result-reporting 2023-09-29 06:53:57 -05:00
Anoop M D
45042cd52a Merge pull request #243 from LesageYann/feat/allow-async-tests
feat: allow test to be asynchrone
2023-09-29 12:42:57 +05:30
Lesage Yann
314e8c17d3 feat: allow test to be asynchrone 2023-09-29 08:55:26 +02:00
Anoop M D
69a7c0e4ce chore: bumped release version to v0.16.3 2023-09-29 12:24:17 +05:30
Anoop M D
626d925ad6 Merge pull request #242 from tpyle/bug/handle-collection-names-with-slashes
Corrects issue when collection names in imports have slashes
2023-09-29 12:20:11 +05:30
Thomas Pyle
2c0ccf769c Corrects issue when collection names in imports have slashes 2023-09-28 21:42:46 -04:00
Thomas Pyle
5f32924ddb Adds some simple unit tests around printRunSummary 2023-09-28 20:58:25 -04:00
Thomas Pyle
9bcf56d689 Handle failed requests and reduce duplication 2023-09-28 20:42:48 -04:00
Anoop M D
516411b9a2 Merge pull request #240 from gkohen/main
Make sure path string does not overflow the dialog
2023-09-29 02:29:01 +05:30
Gabriel Kohen
60e3f3bb6a Make sure path string does not overflow the dialog
See also: #217
2023-09-28 16:22:40 -04:00
163 changed files with 35067 additions and 1326 deletions

View File

@@ -1,29 +1,35 @@
name: Unit Tests
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm i --legacy-peer-deps
- name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query
- name: Build Package bruno-query
run: npm run build --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-js
run: npm run test --workspace=packages/bruno-js
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Check package-lock.json
run: npm ci
- name: Install dependencies
run: npm i --legacy-peer-deps
- name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query
- name: Build Package bruno-query
run: npm run build --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-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-electron
run: npm run test --workspace=packages/bruno-electron

5
.gitignore vendored
View File

@@ -4,7 +4,6 @@
node_modules
yarn.lock
pnpm-lock.yaml
package-lock.json
.pnp
.pnp.js
@@ -41,3 +40,7 @@ yarn-error.log*
/test-results/
/playwright-report/
/playwright/.cache/
#dev editor
bruno.iml
.idea

View File

@@ -1,3 +1,5 @@
**English** | [Русский](/contributing_ru.md)
## Lets make bruno better, together !!
I am happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
@@ -19,7 +21,7 @@ Libraries we use
### Dependencies
You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
### Lets start coding

37
contributing_ru.md Normal file
View File

@@ -0,0 +1,37 @@
[English](/contributing.md) | **Русский**
## Давайте вместе сделаем Бруно лучше!!!
Я рад, что Вы хотите усовершенствовать bruno. Ниже приведены рекомендации по запуску bruno на вашем компьютере.
### Стек
Bruno построен с использованием NextJs и React. Мы также используем electron для поставки десктопной версии ( которая поддерживает локальные коллекции )
Библиотеки, которые мы используем
- CSS - Tailwind
- Редакторы кода - Codemirror
- Управление состоянием - Redux
- Иконки - Tabler Icons
- Формы - formik
- Валидация схем - Yup
- Запросы клиента - axios
- Наблюдатель за файловой системой - chokidar
### Зависимости
Вам потребуется [Node v18.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm
### Приступим к коду
Пожалуйста, обратитесь к [development_ru.md](docs/development_ru.md) для получения инструкций по запуску локальной среды разработки.
### Создание Pull Request
- Пожалуйста, пусть PR будет небольшим и сфокусированным на одной вещи
- Пожалуйста, соблюдайте формат создания веток
- feature/[название функции]: Эта ветка должна содержать изменения для конкретной функции
- Пример: feature/dark-mode
- bugfix/[название ошибки]: Эта ветка должна содержать только исправления для конкретной ошибки
- Пример bugfix/bug-1

View File

@@ -1,9 +1,12 @@
**English** | [Русский](/docs/development_ru.md)
## Development
Bruno is being developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
### Dependencies
* NodeJS v18
- NodeJS v18
### Local Development
@@ -15,7 +18,6 @@ nvm use
npm i --legacy-peer-deps
# build graphql docs
# note: you can for now ignore the error thrown while building the graphql docs
npm run build:graphql-docs
# build bruno query

55
docs/development_ru.md Normal file
View File

@@ -0,0 +1,55 @@
[English](/docs/development.md) | **Русский**
## Разработка
Bruno разрабатывается как десктопное приложение. Необходимо загрузить приложение, запустив приложение nextjs в одном терминале, а затем запустить приложение 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 в подкаталогах
find . -type f -name "package-lock.json" -delete
```
### Тестирование
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```

29822
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,5 @@
},
"overrides": {
"rollup": "3.2.5"
},
"dependencies": {}
}
}

View File

@@ -30,8 +30,11 @@
"graphiql": "^1.5.9",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"handlebars": "^4.7.8",
"httpsnippet": "^3.0.1",
"idb": "^7.0.0",
"immer": "^9.0.15",
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"mousetrap": "^1.6.5",

View File

@@ -119,7 +119,7 @@ export default class CodeEditor extends React.Component {
render() {
return (
<StyledWrapper
className="h-full"
className="h-full w-full"
aria-label="Code Editor"
ref={(node) => {
this._node = node;

View File

@@ -0,0 +1,28 @@
import styled from 'styled-components';
const Wrapper = styled.div`
font-size: 0.8125rem;
.auth-mode-selector {
background: transparent;
.auth-mode-label {
color: ${(props) => props.theme.colors.text.yellow};
}
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
.label-item {
padding: 0.2rem 0.6rem !important;
}
}
.caret {
color: rgb(140, 140, 140);
fill: rgb(140 140 140);
}
`;
export default Wrapper;

View File

@@ -0,0 +1,69 @@
import React, { useRef, forwardRef } from 'react';
import get from 'lodash/get';
import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { useDispatch } from 'react-redux';
import { updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections';
import { humanizeRequestAuthMode } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const AuthMode = ({ collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const authMode = get(collection, 'root.request.auth.mode');
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const onModeChange = (value) => {
dispatch(
updateCollectionAuthMode({
collectionUid: collection.uid,
mode: value
})
);
};
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('basic');
}}
>
Basic Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('bearer');
}}
>
Bearer Token
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('none');
}}
>
No Auth
</div>
</Dropdown>
</div>
</StyledWrapper>
);
};
export default AuthMode;

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
`;
export default Wrapper;

View File

@@ -0,0 +1,71 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BasicAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const basicAuth = get(collection, 'root.request.auth.basic', {});
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleUsernameChange = (username) => {
dispatch(
updateCollectionAuth({
mode: 'basic',
collectionUid: collection.uid,
content: {
username: username,
password: basicAuth.password
}
})
);
};
const handlePasswordChange = (password) => {
dispatch(
updateCollectionAuth({
mode: 'basic',
collectionUid: collection.uid,
content: {
username: basicAuth.username,
password: password
}
})
);
};
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Username</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={basicAuth.username || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleUsernameChange(val)}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={basicAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default BasicAuth;

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
`;
export default Wrapper;

View File

@@ -0,0 +1,46 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BearerAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const bearerToken = get(collection, 'root.request.auth.bearer.token');
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleTokenChange = (token) => {
dispatch(
updateCollectionAuth({
mode: 'bearer',
collectionUid: collection.uid,
content: {
token: token
}
})
);
};
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Token</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={bearerToken}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleTokenChange(val)}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default BearerAuth;

View File

@@ -0,0 +1,5 @@
import styled from 'styled-components';
const Wrapper = styled.div``;
export default Wrapper;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import AuthMode from './AuthMode';
import BearerAuth from './BearerAuth';
import BasicAuth from './BasicAuth';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const Auth = ({ collection }) => {
const authMode = get(collection, 'root.request.auth.mode');
const dispatch = useDispatch();
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const getAuthView = () => {
switch (authMode) {
case 'basic': {
return <BasicAuth collection={collection} />;
}
case 'bearer': {
return <BearerAuth collection={collection} />;
}
}
};
return (
<StyledWrapper className="w-full mt-2">
<div className="flex flex-grow justify-start items-center">
<AuthMode collection={collection} />
</div>
{getAuthView()}
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Auth;

View File

@@ -0,0 +1,56 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
.btn-add-header {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,151 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import {
addCollectionHeader,
updateCollectionHeader,
deleteCollectionHeader
} from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = get(collection, 'root.request.headers', []);
const addHeader = () => {
dispatch(
addCollectionHeader({
collectionUid: collection.uid
})
);
};
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleHeaderValueChange = (e, _header, type) => {
const header = cloneDeep(_header);
switch (type) {
case 'name': {
header.name = e.target.value;
break;
}
case 'value': {
header.value = e.target.value;
break;
}
case 'enabled': {
header.enabled = e.target.checked;
break;
}
}
dispatch(
updateCollectionHeader({
header: header,
collectionUid: collection.uid
})
);
};
const handleRemoveHeader = (header) => {
dispatch(
deleteCollectionHeader({
headerUid: header.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{headers && headers.length
? headers.map((header) => {
return (
<tr key={header.uid}>
<td>
<SingleLineEditor
value={header.name}
theme={storedTheme}
onSave={handleSave}
onChange={(newValue) =>
handleHeaderValueChange(
{
target: {
value: newValue
}
},
header,
'name'
)
}
autocomplete={headerAutoCompleteList}
collection={collection}
/>
</td>
<td>
<SingleLineEditor
value={header.value}
theme={storedTheme}
onSave={handleSave}
onChange={(newValue) =>
handleHeaderValueChange(
{
target: {
value: newValue
}
},
header,
'value'
)
}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={header.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
/>
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header
</button>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Headers;

View File

@@ -19,7 +19,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
},
validationSchema: Yup.object({
enabled: Yup.boolean(),
protocol: Yup.string().oneOf(['http', 'https']),
protocol: Yup.string().oneOf(['http', 'https', 'socks5']),
hostname: Yup.string().max(1024),
port: Yup.number().min(0).max(65535),
auth: Yup.object({
@@ -49,20 +49,19 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
return (
<StyledWrapper>
<h1 className="font-medium mb-3">Proxy Settings</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="ml-4 mb-3 flex items-center">
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="enabled">
Enabled
</label>
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
</div>
<div className="ml-4 mb-3 flex items-center">
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
</label>
<div className="flex items-center">
<label className="flex items-center mr-4">
<label className="flex items-center">
<input
type="radio"
name="protocol"
@@ -73,7 +72,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
/>
http
</label>
<label className="flex items-center">
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
@@ -84,9 +83,20 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
/>
https
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
onChange={formik.handleChange}
className="mr-1"
/>
socks5
</label>
</div>
</div>
<div className="ml-4 mb-3 flex items-center">
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
@@ -106,7 +116,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<div className="text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
@@ -124,7 +134,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
/>
{formik.touched.port && formik.errors.port ? <div className="text-red-500">{formik.errors.port}</div> : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
@@ -136,7 +146,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
/>
</div>
<div>
<div className="ml-4 mb-3 flex items-center">
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
@@ -156,7 +166,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<div className="text-red-500">{formik.errors.auth.username}</div>
) : null}
</div>
<div className="ml-4 mb-3 flex items-center">
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
@@ -178,7 +188,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
</div>
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-md btn-secondary">
<button type="submit" className="submit btn btn-sm btn-secondary">
Save
</button>
</div>

View File

@@ -0,0 +1,13 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
height: inherit;
}
div.title {
color: var(--color-tab-inactive);
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,73 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Script = ({ collection }) => {
const dispatch = useDispatch();
const requestScript = get(collection, 'root.request.script.req', '');
const responseScript = get(collection, 'root.request.script.res', '');
const { storedTheme } = useTheme();
const onRequestScriptEdit = (value) => {
dispatch(
updateCollectionRequestScript({
script: value,
collectionUid: collection.uid
})
);
};
const onResponseScriptEdit = (value) => {
dispatch(
updateCollectionResponseScript({
script: value,
collectionUid: collection.uid
})
);
};
const handleSave = () => {
dispatch(saveCollectionRoot(collection.uid));
};
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<CodeEditor
collection={collection}
value={requestScript || ''}
theme={storedTheme}
onEdit={onRequestScriptEdit}
mode="javascript"
onSave={handleSave}
/>
</div>
<div className="flex-1 mt-6">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<CodeEditor
collection={collection}
value={responseScript || ''}
theme={storedTheme}
onEdit={onResponseScriptEdit}
mode="javascript"
onSave={handleSave}
/>
</div>
<div className="mt-12">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Script;

View File

@@ -1,6 +1,32 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
div.tabs {
div.tab {
padding: 6px 0px;
border: none;
border-bottom: solid 2px transparent;
margin-right: 1.25rem;
color: var(--color-tab-inactive);
cursor: pointer;
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
&.active {
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
}
}
table {
thead,
td {

View File

@@ -0,0 +1,5 @@
import styled from 'styled-components';
const StyledWrapper = styled.div``;
export default StyledWrapper;

View File

@@ -0,0 +1,47 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Tests = ({ collection }) => {
const dispatch = useDispatch();
const tests = get(collection, 'root.request.tests', '');
const { storedTheme } = useTheme();
const onEdit = (value) => {
dispatch(
updateCollectionTests({
tests: value,
collectionUid: collection.uid
})
);
};
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
return (
<StyledWrapper className="w-full flex flex-col h-full">
<CodeEditor
collection={collection}
value={tests || ''}
theme={storedTheme}
onEdit={onEdit}
mode="javascript"
onSave={handleSave}
/>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Tests;

View File

@@ -1,14 +1,29 @@
import React from 'react';
import classnames from 'classnames';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import ProxySettings from './ProxySettings';
import Headers from './Headers';
import Auth from './Auth';
import Script from './Script';
import Test from './Tests';
import StyledWrapper from './StyledWrapper';
const CollectionSettings = ({ collection }) => {
const dispatch = useDispatch();
const tab = collection.settingsSelectedTab;
const setTab = (tab) => {
dispatch(
updateSettingsSelectedTab({
collectionUid: collection.uid,
tab
})
);
};
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
@@ -22,11 +37,52 @@ const CollectionSettings = ({ collection }) => {
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};
return (
<StyledWrapper className="px-4 py-4">
<h1 className="font-semibold mb-4">Collection Settings</h1>
const getTabPanel = (tab) => {
switch (tab) {
case 'headers': {
return <Headers collection={collection} />;
}
case 'auth': {
return <Auth collection={collection} />;
}
case 'script': {
return <Script collection={collection} />;
}
case 'tests': {
return <Test collection={collection} />;
}
case 'proxy': {
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
}
}
};
<ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
active: tabName === tab
});
};
return (
<StyledWrapper className="flex flex-col h-full relative px-4 py-4">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
Headers
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
Auth
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
Script
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => setTab('tests')}>
Tests
</div>
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
Proxy
</div>
</div>
<section className={`flex ${['auth', 'script'].includes(tab) ? '' : 'mt-4'}`}>{getTabPanel(tab)}</section>
</StyledWrapper>
);
};

View File

@@ -0,0 +1,75 @@
import Modal from 'components/Modal/index';
import Portal from 'components/Portal/index';
import { useFormik } from 'formik';
import { copyEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { useEffect, useRef } from 'react';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import * as Yup from 'yup';
const CopyEnvironment = ({ collection, environment, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: environment.name + ' - Copy'
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),
onSubmit: (values) => {
dispatch(copyEnvironment(values.name, environment.uid, collection.uid))
.then(() => {
toast.success('Environment created in collection');
onClose();
})
.catch(() => toast.error('An error occurred while created the environment'));
}
});
useEffect(() => {
if (inputRef && inputRef.current) {
inputRef.current.focus();
}
}, [inputRef]);
const onSubmit = () => {
formik.handleSubmit();
};
return (
<Portal>
<Modal size="sm" title={'Copy Environment'} confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">
New Environment Name
</label>
<input
id="environment-name"
type="text"
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? (
<div className="text-red-500">{formik.errors.name}</div>
) : null}
</div>
</form>
</Modal>
</Portal>
);
};
export default CopyEnvironment;

View File

@@ -17,7 +17,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@@ -1,12 +1,14 @@
import React, { useState } from 'react';
import { IconEdit, IconTrash, IconDatabase } from '@tabler/icons';
import EnvironmentVariables from './EnvironmentVariables';
import RenameEnvironment from '../../RenameEnvironment';
import { IconCopy, IconDatabase, IconEdit, IconTrash } from '@tabler/icons';
import { useState } from 'react';
import CopyEnvironment from '../../CopyEnvironment';
import DeleteEnvironment from '../../DeleteEnvironment';
import RenameEnvironment from '../../RenameEnvironment';
import EnvironmentVariables from './EnvironmentVariables';
const EnvironmentDetails = ({ environment, collection }) => {
const [openEditModal, setOpenEditModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
const [openCopyModal, setOpenCopyModal] = useState(false);
return (
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
@@ -20,6 +22,9 @@ const EnvironmentDetails = ({ environment, collection }) => {
collection={collection}
/>
)}
{openCopyModal && (
<CopyEnvironment onClose={() => setOpenCopyModal(false)} environment={environment} collection={collection} />
)}
<div className="flex">
<div className="flex flex-grow items-center">
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
@@ -27,6 +32,7 @@ const EnvironmentDetails = ({ environment, collection }) => {
</div>
<div className="flex gap-x-4 pl-4">
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
</div>
</div>

View File

@@ -17,7 +17,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@@ -1,6 +1,8 @@
import styled from 'styled-components';
const Wrapper = styled.div`
color: ${(props) => props.theme.text};
&.modal--animate-out {
animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
@@ -20,6 +22,7 @@ const Wrapper = styled.div`
justify-content: center;
overflow-y: auto;
z-index: 10;
background-color: rgba(0, 0, 0, 0.5);
}
.bruno-modal-card {

View File

@@ -61,19 +61,20 @@ const Modal = ({
children,
confirmDisabled,
hideCancel,
hideFooter
hideFooter,
closeModalFadeTimeout = 500
}) => {
const [isClosing, setIsClosing] = useState(false);
const escFunction = (event) => {
const escKeyCode = 27;
if (event.keyCode === escKeyCode) {
closeModal();
closeModal({ type: 'esc' });
}
};
const closeModal = () => {
const closeModal = (args) => {
setIsClosing(true);
setTimeout(() => handleCancel(), 500);
setTimeout(() => handleCancel(args), closeModalFadeTimeout);
};
useEffect(() => {
@@ -94,12 +95,12 @@ const Modal = ({
return (
<StyledWrapper className={classes}>
<div className={`bruno-modal-card modal-${size}`}>
<ModalHeader title={title} handleCancel={() => closeModal()} />
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} />
<ModalContent>{children}</ModalContent>
<ModalFooter
confirmText={confirmText}
cancelText={cancelText}
handleCancel={() => closeModal()}
handleCancel={() => closeModal({ type: 'button' })}
handleSubmit={handleConfirm}
confirmDisabled={confirmDisabled}
hideCancel={hideCancel}
@@ -108,7 +109,12 @@ const Modal = ({
</div>
{/* Clicking on backdrop closes the modal */}
<div className="bruno-modal-backdrop" onClick={() => closeModal()} />
<div
className="bruno-modal-backdrop"
onClick={() => {
closeModal({ type: 'backdrop' });
}}
/>
</StyledWrapper>
);
};

View File

@@ -178,7 +178,7 @@ const AssertionRow = ({
handleAssertionChange(
{
target: {
value: newValue
value: `${operator} ${newValue}`
}
},
assertion,
@@ -197,10 +197,11 @@ const AssertionRow = ({
<input
type="checkbox"
checked={assertion.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
/>
<button onClick={() => handleRemoveAssertion(assertion)}>
<button tabIndex="-1" onClick={() => handleRemoveAssertion(assertion)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>

View File

@@ -4,8 +4,11 @@ const Wrapper = styled.div`
font-size: 0.8125rem;
.auth-mode-selector {
background: ${(props) => props.theme.requestTabPanel.bodyModeSelect.color};
border-radius: 3px;
background: transparent;
.auth-mode-label {
color: ${(props) => props.theme.colors.text.yellow};
}
.dropdown-item {
padding: 0.2rem 0.6rem !important;

View File

@@ -15,8 +15,8 @@ const AuthMode = ({ item, collection }) => {
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
`;
export default Wrapper;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BasicAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUsernameChange = (username) => {
dispatch(
updateAuth({
mode: 'basic',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: username,
password: basicAuth.password
}
})
);
};
const handlePasswordChange = (password) => {
dispatch(
updateAuth({
mode: 'basic',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: basicAuth.username,
password: password
}
})
);
};
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Username</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={basicAuth.username || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleUsernameChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={basicAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default BasicAuth;

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
`;
export default Wrapper;

View File

@@ -0,0 +1,51 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BearerAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const bearerToken = item.draft
? get(item, 'draft.request.auth.bearer.token')
: get(item, 'request.auth.bearer.token');
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleTokenChange = (token) => {
dispatch(
updateAuth({
mode: 'bearer',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
token: token
}
})
);
};
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Token</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={bearerToken}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleTokenChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default BearerAuth;

View File

@@ -1,32 +1,31 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import AuthMode from './AuthMode';
import BearerAuth from './BearerAuth';
import BasicAuth from './BasicAuth';
import StyledWrapper from './StyledWrapper';
const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
const Auth = ({ item, collection }) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const onEdit = (value) => {
// dispatch(
// updateRequestBody({
// content: value,
// itemUid: item.uid,
// collectionUid: collection.uid
// })
// );
const getAuthView = () => {
switch (authMode) {
case 'basic': {
return <BasicAuth collection={collection} item={item} />;
}
case 'bearer': {
return <BearerAuth collection={collection} item={item} />;
}
}
};
if (authMode === 'basic') {
return <div>Basic Auth</div>;
}
if (authMode === 'bearer') {
return <div>Bearer Token</div>;
}
return <StyledWrapper className="w-full">No Auth</StyledWrapper>;
return (
<StyledWrapper className="w-full mt-1">
<div className="flex flex-grow justify-start items-center">
<AuthMode item={item} collection={collection} />
</div>
{getAuthView()}
</StyledWrapper>
);
};
export default RequestBody;
export default Auth;

View File

@@ -116,10 +116,11 @@ const FormUrlEncodedParams = ({ item, collection }) => {
<input
type="checkbox"
checked={param.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParams(param)}>
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>

View File

@@ -6,6 +6,7 @@ import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons'
import { useSelector, useDispatch } from 'react-redux';
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryEditor from 'components/RequestPane/QueryEditor';
import Auth from 'components/RequestPane/Auth';
import GraphQLVariables from 'components/RequestPane/GraphQLVariables';
import RequestHeaders from 'components/RequestPane/RequestHeaders';
import Vars from 'components/RequestPane/Vars';
@@ -32,7 +33,14 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
let { schema, loadSchema, isLoading: isSchemaLoading, error: schemaError } = useGraphqlSchema(url, environment);
const request = item.draft ? item.draft.request : item.request;
let {
schema,
loadSchema,
isLoading: isSchemaLoading,
error: schemaError
} = useGraphqlSchema(url, environment, request, collection);
const loadGqlSchema = () => {
if (!isSchemaLoading) {
@@ -90,6 +98,9 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
case 'headers': {
return <RequestHeaders item={item} collection={collection} />;
}
case 'auth': {
return <Auth item={item} collection={collection} />;
}
case 'vars': {
return <Vars item={item} collection={collection} />;
}
@@ -135,6 +146,9 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
Auth
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
Vars
</div>

View File

@@ -6,7 +6,7 @@ import { simpleHash } from 'utils/common';
const schemaHashPrefix = 'bruno.graphqlSchema';
const useGraphqlSchema = (endpoint, environment) => {
const useGraphqlSchema = (endpoint, environment, request, collection) => {
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
@@ -25,7 +25,7 @@ const useGraphqlSchema = (endpoint, environment) => {
const loadSchema = () => {
setIsLoading(true);
fetchGqlSchema(endpoint, environment)
fetchGqlSchema(endpoint, environment, request, collection)
.then((res) => res.data)
.then((s) => {
if (s && s.data) {

View File

@@ -108,13 +108,10 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
<RequestBodyMode item={item} collection={collection} />
</div>
) : null}
{focusedTab.requestPaneTab === 'auth' ? (
<div className="flex flex-grow justify-end items-center">
<AuthMode item={item} collection={collection} />
</div>
) : null}
</div>
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>
<section
className={`flex w-full ${['script', 'vars', 'auth'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}
>
{getTabPanel(focusedTab.requestPaneTab)}
</section>
</StyledWrapper>

View File

@@ -116,10 +116,11 @@ const MultipartFormParams = ({ item, collection }) => {
<input
type="checkbox"
checked={param.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParams(param)}>
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>

View File

@@ -115,10 +115,11 @@ const QueryParams = ({ item, collection }) => {
<input
type="checkbox"
checked={param.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button onClick={() => handleRemoveParam(param)}>
<button tabIndex="-1" onClick={() => handleRemoveParam(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>

View File

@@ -32,6 +32,50 @@ const Wrapper = styled.div`
position: relative;
top: 1px;
}
.tooltip {
position: relative;
display: inline-block;
cursor: pointer;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
.tooltiptext {
visibility: hidden;
width: auto;
background-color: ${(props) => props.theme.requestTabs.active.bg};
color: ${(props) => props.theme.text};
text-align: center;
border-radius: 4px;
padding: 4px 8px;
position: absolute;
z-index: 1;
bottom: 34px;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
white-space: nowrap;
}
.tooltiptext::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -4px;
border-width: 4px;
border-style: solid;
border-color: ${(props) => props.theme.requestTabs.active.bg} transparent transparent transparent;
}
.shortcut {
font-size: 0.625rem;
}
`;
export default Wrapper;

View File

@@ -5,8 +5,9 @@ import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/sli
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import HttpMethodSelector from './HttpMethodSelector';
import { useTheme } from 'providers/Theme';
import SendIcon from 'components/Icons/Send';
import { IconDeviceFloppy, IconArrowRight } from '@tabler/icons';
import SingleLineEditor from 'components/SingleLineEditor';
import { isMacOS } from 'utils/common/platform';
import StyledWrapper from './StyledWrapper';
const QueryUrl = ({ item, collection, handleRun }) => {
@@ -14,6 +15,8 @@ const QueryUrl = ({ item, collection, handleRun }) => {
const dispatch = useDispatch();
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const isMac = isMacOS();
const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S';
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
@@ -22,7 +25,10 @@ const QueryUrl = ({ item, collection, handleRun }) => {
setMethodSelectorWidth(el.offsetWidth);
}, [method]);
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const onSave = () => {
dispatch(saveRequest(item.uid, collection.uid));
};
const onUrlChange = (value) => {
dispatch(
requestUrlChanged({
@@ -65,7 +71,25 @@ const QueryUrl = ({ item, collection, handleRun }) => {
collection={collection}
/>
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<SendIcon color={theme.requestTabPanel.url.icon} width={22} />
<div
className="tooltip mr-3"
onClick={(e) => {
e.stopPropagation();
if (!item.draft) return;
onSave();
}}
>
<IconDeviceFloppy
color={item.draft ? theme.colors.text.yellow : theme.requestTabs.icon.color}
strokeWidth={1.5}
size={22}
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
/>
<span className="tooltiptext text-xs">
Save <span className="shortcut">({saveShortcut})</span>
</span>
</div>
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
</div>
</div>
</StyledWrapper>

View File

@@ -8,6 +8,8 @@ import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'prov
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const RequestHeaders = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -91,6 +93,7 @@ const RequestHeaders = ({ item, collection }) => {
'name'
)
}
autocomplete={headerAutoCompleteList}
onRun={handleRun}
collection={collection}
/>
@@ -120,10 +123,11 @@ const RequestHeaders = ({ item, collection }) => {
<input
type="checkbox"
checked={header.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
/>
<button onClick={() => handleRemoveHeader(header)}>
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>

View File

@@ -128,10 +128,11 @@ const VarsTable = ({ item, collection, vars, varType }) => {
<input
type="checkbox"
checked={_var.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, _var, 'enabled')}
/>
<button onClick={() => handleRemoveVar(_var)}>
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>

View File

@@ -0,0 +1,30 @@
import React from 'react';
import Modal from 'components/Modal';
const ConfirmRequestClose = ({ onCancel, onCloseWithoutSave, onSaveAndClose }) => {
const _handleCancel = ({ type }) => {
if (type === 'button') {
return onCloseWithoutSave();
}
return onCancel();
};
return (
<Modal
size="sm"
title="Unsaved changes"
confirmText="Save and Close"
cancelText="Close without saving"
handleConfirm={onSaveAndClose}
handleCancel={_handleCancel}
disableEscapeKey={true}
disableCloseOnOutsideClick={true}
closeModalFadeTimeout={150}
>
<div className="font-normal">You have unsaved changes in you request.</div>
</Modal>
);
};
export default ConfirmRequestClose;

View File

@@ -8,7 +8,7 @@ const SpecialTab = ({ handleCloseClick, type }) => {
return (
<>
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Settings</span>
<span className="ml-1">Collection</span>
</>
);
}

View File

@@ -1,14 +1,22 @@
import React from 'react';
import React, { useState } from 'react';
import get from 'lodash/get';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import { findItemInCollection } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import RequestTabNotFound from './RequestTabNotFound';
import ConfirmRequestClose from './ConfirmRequestClose';
import SpecialTab from './SpecialTab';
import { useTheme } from 'providers/Theme';
import darkTheme from 'themes/dark';
import lightTheme from 'themes/light';
const RequestTab = ({ tab, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [showConfirmClose, setShowConfirmClose] = useState(false);
const handleCloseClick = (event) => {
event.stopPropagation();
@@ -21,35 +29,38 @@ const RequestTab = ({ tab, collection }) => {
};
const getMethodColor = (method = '') => {
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
let color = '';
method = method.toLocaleLowerCase();
switch (method) {
case 'get': {
color = 'var(--color-method-get)';
color = theme.request.methods.get;
break;
}
case 'post': {
color = 'var(--color-method-post)';
color = theme.request.methods.post;
break;
}
case 'put': {
color = 'var(--color-method-put)';
color = theme.request.methods.put;
break;
}
case 'delete': {
color = 'var(--color-method-delete)';
color = theme.request.methods.delete;
break;
}
case 'patch': {
color = 'var(--color-method-patch)';
color = theme.request.methods.patch;
break;
}
case 'options': {
color = 'var(--color-method-options)';
color = theme.request.methods.options;
break;
}
case 'head': {
color = 'var(--color-method-head)';
color = theme.request.methods.head;
break;
}
}
@@ -79,6 +90,39 @@ const RequestTab = ({ tab, collection }) => {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
{showConfirmClose && (
<ConfirmRequestClose
onCancel={() => setShowConfirmClose(false)}
onCloseWithoutSave={() => {
dispatch(
deleteRequestDraft({
itemUid: item.uid,
collectionUid: collection.uid
})
);
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
setShowConfirmClose(false);
}}
onSaveAndClose={() => {
dispatch(saveRequest(item.uid, collection.uid))
.then(() => {
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
setShowConfirmClose(false);
})
.catch((err) => {
console.log('err', err);
});
}}
/>
)}
<div className="flex items-baseline tab-label pl-2">
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
{method}
@@ -87,7 +131,14 @@ const RequestTab = ({ tab, collection }) => {
{item.name}
</span>
</div>
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
<div
className="flex px-2 close-icon-container"
onClick={(e) => {
if (!item.draft) return handleCloseClick(e);
setShowConfirmClose(true);
}}
>
{!item.draft ? (
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path

View File

@@ -1,12 +1,13 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
position: absolute;
height: 100%;
z-index: 1;
background-color: ${(props) => props.theme.requestTabPanel.responseOverlayBg};
div.overlay {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
height: 100%;
z-index: 9;
display: flex;
flex-direction: column;
@@ -14,6 +15,11 @@ const StyledWrapper = styled.div`
padding-top: 20%;
overflow: hidden;
text-align: center;
.loading-icon {
transform: scaleY(-1);
animation: rotateCounterClockwise 1s linear infinite;
}
}
`;

View File

@@ -13,17 +13,17 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
};
return (
<StyledWrapper className="mt-4 px-3 w-full">
<StyledWrapper className="px-3 w-full">
<div className="overlay">
<div style={{ marginBottom: 15, fontSize: 26 }}>
<div style={{ display: 'inline-block', fontSize: 24, marginLeft: 5, marginRight: 5 }}>
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>
<StopWatch />
</div>
</div>
<IconRefresh size={24} className="animate-spin" />
<IconRefresh size={24} className="loading-icon" />
<button
onClick={handleCancelRequest}
className="mt-4 uppercase btn-md rounded btn-secondary ease-linear transition-all duration-150"
className="mt-4 uppercase btn-sm rounded btn-secondary ease-linear transition-all duration-150"
type="button"
>
Cancel Request

View File

@@ -13,13 +13,11 @@ const Placeholder = () => {
<div className="px-1 py-2">Send Request</div>
<div className="px-1 py-2">New Request</div>
<div className="px-1 py-2">Edit Environments</div>
<div className="px-1 py-2">Help</div>
</div>
<div className="flex flex-1 flex-col px-1">
<div className="px-1 py-2">Cmd + Enter</div>
<div className="px-1 py-2">Cmd + B</div>
<div className="px-1 py-2">Cmd + E</div>
<div className="px-1 py-2">Cmd + H</div>
</div>
</div>
</StyledWrapper>

View File

@@ -1,9 +1,27 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
display: grid;
grid-template-columns: 100%;
grid-template-rows: 1.25rem calc(100% - 1.25rem);
/* This is a hack to force Codemirror to use all available space */
> div {
position: relative;
}
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 220px);
position: absolute;
top: 0;
bottom: 0;
height: 100%;
width: 100%;
}
div[role='tablist'] {
.active {
color: ${(props) => props.theme.colors.text.yellow};
}
}
`;

View File

@@ -3,12 +3,57 @@ import CodeEditor from 'components/CodeEditor';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import classnames from 'classnames';
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
import StyledWrapper from './StyledWrapper';
import { useState } from 'react';
import { useMemo } from 'react';
const QueryResult = ({ item, collection, value, width, disableRunEventListener, mode }) => {
const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers }) => {
const { storedTheme } = useTheme();
const [tab, setTab] = useState('preview');
const dispatch = useDispatch();
const contentType = getContentType(headers);
const mode = getCodeMirrorModeBasedOnContentType(contentType);
const formatResponse = (data, mode) => {
if (!data) {
return '';
}
if (mode.includes('json')) {
return safeStringifyJSON(data, true);
}
if (mode.includes('xml')) {
let parsed = safeParseXML(data, { collapseContent: true });
if (typeof parsed === 'string') {
return parsed;
}
return safeStringifyJSON(parsed, true);
}
if (['text', 'html'].includes(mode)) {
if (typeof data === 'string') {
return data;
}
return safeStringifyJSON(data);
}
// final fallback
if (typeof data === 'string') {
return data;
}
return safeStringifyJSON(data);
};
const value = formatResponse(data, mode);
const onRun = () => {
if (disableRunEventListener) {
@@ -17,18 +62,58 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener,
dispatch(sendRequest(item, collection.uid));
};
return (
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
<div className="h-full">
<CodeEditor
collection={collection}
theme={storedTheme}
onRun={onRun}
value={value || ''}
mode={mode}
readOnly
const getTabClassname = (tabName) => {
return classnames(`select-none ${tabName}`, {
active: tabName === tab,
'cursor-pointer': tabName !== tab
});
};
const getTabs = () => {
if (!mode.includes('html')) {
return null;
}
return (
<>
<div className={getTabClassname('raw')} role="tab" onClick={() => setTab('raw')}>
Raw
</div>
<div className={getTabClassname('preview')} role="tab" onClick={() => setTab('preview')}>
Preview
</div>
</>
);
};
const activeResult = useMemo(() => {
if (
tab === 'preview' &&
mode.includes('html') &&
item.requestSent &&
item.requestSent.url &&
typeof data === 'string'
) {
// Add the Base tag to the head so content loads properly. This also needs the correct CSP settings
const webViewSrc = data.replace('<head>', `<head><base href="${item.requestSent.url}">`);
return (
<webview
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
webpreferences="disableDialogs=true, javascript=yes"
className="h-full bg-white"
/>
);
}
return <CodeEditor collection={collection} theme={storedTheme} onRun={onRun} value={value} mode={mode} readOnly />;
}, [tab, collection, storedTheme, onRun, value, mode]);
return (
<StyledWrapper className="px-3 w-full h-full" style={{ maxWidth: width }}>
<div className="flex justify-end gap-2 text-xs" role="tablist">
{getTabs()}
</div>
{activeResult}
</StyledWrapper>
);
};

View File

@@ -7,7 +7,7 @@ const ResponseSize = ({ size }) => {
if (size > 1024) {
// size is greater than 1kb
let kb = Math.floor(size / 1024);
let decimal = ((size % 1024) / 1024).toFixed(2) * 100;
let decimal = Math.round(((size % 1024) / 1024).toFixed(2) * 100);
sizeToDisplay = kb + '.' + decimal + 'KB';
} else {
sizeToDisplay = size + 'B';

View File

@@ -16,7 +16,7 @@ const TestResults = ({ results, assertionResults }) => {
return (
<StyledWrapper className="flex flex-col px-3">
<div className="py-2 font-medium test-summary">
<div className="pb-2 font-medium test-summary">
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
</div>
<ul className="">

View File

@@ -2,7 +2,6 @@ import React from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { getContentType, formatResponse } from 'utils/common';
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryResult from './QueryResult';
import Overlay from './Overlay';
@@ -41,8 +40,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
item={item}
collection={collection}
width={rightPaneWidth}
value={response.data ? formatResponse(response) : ''}
mode={getContentType(response.headers)}
data={response.data}
headers={response.headers}
/>
);
}
@@ -62,15 +61,15 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
}
};
if (isLoading) {
if (isLoading && !item.response) {
return (
<StyledWrapper className="flex h-full relative">
<StyledWrapper className="flex flex-col h-full relative">
<Overlay item={item} collection={collection} />
</StyledWrapper>
);
}
if (response.state !== 'success') {
if (!item.response) {
return (
<StyledWrapper className="flex h-full relative">
<Placeholder />
@@ -93,10 +92,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
});
};
const isJson = (headers) => {
return getContentType(headers) === 'application/ld+json';
};
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center px-3 tabs" role="tablist">
@@ -120,7 +115,10 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
</div>
) : null}
</div>
<section className="flex flex-grow mt-5">{getTabPanel(focusedTab.responsePaneTab)}</section>
<section className={`flex flex-grow relative ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}>
{isLoading ? <Overlay item={item} collection={collection} /> : null}
{getTabPanel(focusedTab.responsePaneTab)}
</section>
</StyledWrapper>
);
};

View File

@@ -33,7 +33,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
collection={collection}
width={rightPaneWidth}
disableRunEventListener={true}
value={responseReceived && responseReceived.data ? safeStringifyJSON(responseReceived.data, true) : ''}
data={responseReceived.data}
headers={responseReceived.headers}
/>
);
}

View File

@@ -18,7 +18,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@@ -0,0 +1,21 @@
import CodeEditor from 'components/CodeEditor/index';
import { HTTPSnippet } from 'httpsnippet';
import { useTheme } from 'providers/Theme/index';
import { buildHarRequest } from 'utils/codegenerator/har';
const CodeView = ({ language, item }) => {
const { storedTheme } = useTheme();
const { target, client, language: lang } = language;
let snippet = '';
try {
snippet = new HTTPSnippet(buildHarRequest(item.request)).convert(target, client);
} catch (e) {
console.error(e);
snippet = 'Error generating code snippet';
}
return <CodeEditor readOnly value={snippet} theme={storedTheme} mode={lang} />;
};
export default CodeView;

View File

@@ -0,0 +1,38 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
margin-inline: -1rem;
margin-block: -1.5rem;
background-color: ${(props) => props.theme.collection.environment.settings.bg};
.generate-code-sidebar {
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
min-height: 400px;
}
.generate-code-item {
min-width: 150px;
display: block;
position: relative;
cursor: pointer;
padding: 8px 10px;
border-left: solid 2px transparent;
text-decoration: none;
&:hover {
text-decoration: none;
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
}
}
.active {
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
&:hover {
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,149 @@
import Modal from 'components/Modal/index';
import { useState } from 'react';
import CodeView from './CodeView';
import StyledWrapper from './StyledWrapper';
import { isValidUrl } from 'utils/url/index';
import get from 'lodash/get';
import handlebars from 'handlebars';
import { findEnvironmentInCollection } from 'utils/collections';
const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => {
if (!url || !url.length || typeof url !== 'string') {
return;
}
const template = handlebars.compile(url, { noEscape: true });
return template({
...envVars,
...collectionVariables,
process: {
env: {
...processEnvVars
}
}
});
};
const languages = [
{
name: 'HTTP',
target: 'http',
client: 'http1.1'
},
{
name: 'JavaScript-Fetch',
target: 'javascript',
client: 'fetch'
},
{
name: 'Javascript-jQuery',
target: 'javascript',
client: 'jquery'
},
{
name: 'Javascript-axios',
target: 'javascript',
client: 'axios'
},
{
name: 'Python-Python3',
target: 'python',
client: 'python3'
},
{
name: 'Python-Requests',
target: 'python',
client: 'requests'
},
{
name: 'PHP',
target: 'php',
client: 'curl'
},
{
name: 'Shell-curl',
target: 'shell',
client: 'curl'
},
{
name: 'Shell-httpie',
target: 'shell',
client: 'httpie'
}
];
const GenerateCodeItem = ({ collection, item, onClose }) => {
const url = get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
let envVars = {};
if (environment) {
const vars = get(environment, 'variables', []);
envVars = vars.reduce((acc, curr) => {
acc[curr.name] = curr.value;
return acc;
}, {});
}
const interpolatedUrl = interpolateUrl({
url,
envVars,
collectionVariables: collection.collectionVariables,
processEnvVars: collection.processEnvVariables
});
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
return (
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
<StyledWrapper>
<div className="flex w-full">
<div>
<div className="generate-code-sidebar">
{languages &&
languages.length &&
languages.map((language) => (
<div
key={language.name}
className={
language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item'
}
onClick={() => setSelectedLanguage(language)}
>
<span className="capitalize">{language.name}</span>
</div>
))}
</div>
</div>
<div className="flex-grow p-4">
{isValidUrl(interpolatedUrl) ? (
<CodeView
language={selectedLanguage}
item={{
...item,
request:
item.request.url !== ''
? {
...item.request,
url: interpolatedUrl
}
: {
...item.draft.request,
url: interpolatedUrl
}
}}
/>
) : (
<div className="flex flex-col justify-center items-center w-full">
<div className="text-center">
<h1 className="text-2xl font-bold">Invalid URL: {interpolatedUrl}</h1>
<p className="text-gray-500">Please check the URL and try again</p>
</div>
</div>
)}
</div>
</div>
</StyledWrapper>
</Modal>
);
};
export default GenerateCodeItem;

View File

@@ -17,7 +17,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@@ -25,13 +25,13 @@ const Wrapper = styled.div`
color: ${(props) => props.theme.request.methods.delete};
}
.method-patch {
color: ${(props) => props.theme.request.methods.put};
color: ${(props) => props.theme.request.methods.patch};
}
.method-options {
color: ${(props) => props.theme.request.methods.put};
color: ${(props) => props.theme.request.methods.options};
}
.method-head {
color: ${(props) => props.theme.request.methods.put};
color: ${(props) => props.theme.request.methods.head};
}
`;

View File

@@ -15,7 +15,8 @@ const RequestMethod = ({ item }) => {
'method-put': method === 'put',
'method-delete': method === 'delete',
'method-patch': method === 'patch',
'method-head': method === 'head'
'method-head': method === 'head',
'method-options': method == 'options'
});
};

View File

@@ -16,11 +16,12 @@ import RenameCollectionItem from './RenameCollectionItem';
import CloneCollectionItem from './CloneCollectionItem';
import DeleteCollectionItem from './DeleteCollectionItem';
import RunCollectionItem from './RunCollectionItem';
import GenerateCodeItem from './GenerateCodeItem';
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
import { getDefaultRequestPaneTab } from 'utils/collections';
import { hideHomePage } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
const CollectionItem = ({ item, collection, searchText }) => {
@@ -32,6 +33,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
@@ -86,33 +88,51 @@ const CollectionItem = ({ item, collection, searchText }) => {
});
const handleClick = (event) => {
if (isItemARequest(item)) {
if (itemIsOpenedInTabs(item, tabs)) {
switch (event.button) {
case 0: // left click
if (isItemARequest(item)) {
dispatch(hideHomePage());
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(
focusTab({
uid: item.uid
})
);
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item)
})
);
return;
}
dispatch(
focusTab({
uid: item.uid
collectionFolderClicked({
itemUid: item.uid,
collectionUid: collection.uid
})
);
} else {
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item)
})
);
}
dispatch(hideHomePage());
} else {
dispatch(
collectionFolderClicked({
itemUid: item.uid,
collectionUid: collection.uid
})
);
return;
case 2: // right click
const _menuDropdown = dropdownTippyRef.current;
if (_menuDropdown) {
let menuDropdownBehavior = 'show';
if (_menuDropdown.state.isShown) {
menuDropdownBehavior = 'hide';
}
_menuDropdown[menuDropdownBehavior]();
}
return;
}
};
const handleDoubleClick = (event) => {
setRenameItemModalOpen(true);
};
let indents = range(item.depth);
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const isFolder = isItemAFolder(item);
@@ -142,7 +162,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
const sortFolderItems = (items = []) => {
return items.sort((a, b) => a.name.localeCompare(b.name));
};
const handleGenerateCode = (e) => {
e.stopPropagation();
dropdownTippyRef.current.hide();
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) {
setGenerateCodeItemModalOpen(true);
} else {
toast.error('URL is required');
}
};
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
@@ -166,13 +194,17 @@ const CollectionItem = ({ item, collection, searchText }) => {
{runCollectionModalOpen && (
<RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />
)}
{generateCodeItemModalOpen && (
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
)}
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
<div className="flex items-center h-full w-full">
{indents && indents.length
? indents.map((i) => {
return (
<div
onClick={handleClick}
onMouseUp={handleClick}
onDoubleClick={handleDoubleClick}
className="indent-block"
key={i}
style={{
@@ -187,7 +219,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
})
: null}
<div
onClick={handleClick}
onMouseUp={handleClick}
onDoubleClick={handleDoubleClick}
className="flex flex-grow items-center h-full overflow-hidden"
style={{
paddingLeft: 8
@@ -264,6 +297,16 @@ const CollectionItem = ({ item, collection, searchText }) => {
Clone
</div>
)}
{!isFolder && item.type === 'http-request' && (
<div
className="dropdown-item"
onClick={(e) => {
handleGenerateCode(e);
}}
>
Generate Code
</div>
)}
<div
className="dropdown-item delete-item"
onClick={(e) => {

View File

@@ -31,7 +31,7 @@ const CollectionProperties = ({ collection, onClose }) => {
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Location&nbsp;:</td>
<td className="py-2 px-2">{collection.pathname}</td>
<td className="py-2 px-2 break-all">{collection.pathname}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Environments&nbsp;:</td>

View File

@@ -16,7 +16,7 @@ const RenameCollection = ({ collection, onClose }) => {
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('name is required')
}),

View File

@@ -16,7 +16,7 @@ import CollectionItem from './CollectionItem';
import RemoveCollection from './RemoveCollection';
import CollectionProperties from './CollectionProperties';
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections';
import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections';
import exportCollection from 'utils/collections/export';
import RenameCollection from './RenameCollection';
@@ -64,12 +64,26 @@ const Collection = ({ collection, searchText }) => {
});
const handleClick = (event) => {
dispatch(collectionClicked(collection.uid));
const _menuDropdown = menuDropdownTippyRef.current;
switch (event.button) {
case 0: // left click
dispatch(collectionClicked(collection.uid));
return;
case 2: // right click
if (_menuDropdown) {
let menuDropdownBehavior = 'show';
if (_menuDropdown.state.isShown) {
menuDropdownBehavior = 'hide';
}
_menuDropdown[menuDropdownBehavior]();
}
return;
}
};
const handleExportClick = () => {
const collectionCopy = cloneDeep(collection);
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
exportCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
};
const [{ isOver }, drop] = useDrop({
@@ -119,7 +133,7 @@ const Collection = ({ collection, searchText }) => {
<CollectionProperties collection={collection} onClose={() => setCollectionPropertiesModal(false)} />
)}
<div className="flex py-1 collection-name items-center" ref={drop}>
<div className="flex flex-grow items-center overflow-hidden" onClick={handleClick}>
<div className="flex flex-grow items-center overflow-hidden" onMouseUp={handleClick}>
<IconChevronRight
size={16}
strokeWidth={2}

View File

@@ -1,21 +1,61 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { IconSearch, IconFolders } from '@tabler/icons';
import { useDispatch, useSelector } from 'react-redux';
import {
IconSearch,
IconFolders,
IconArrowsSort,
IconSortAscendingLetters,
IconSortDescendingLetters
} from '@tabler/icons';
import Collection from '../Collections/Collection';
import CreateCollection from '../CreateCollection';
import StyledWrapper from './StyledWrapper';
import CreateOrOpenCollection from './CreateOrOpenCollection';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
// todo: move this to a separate folder
// the coding convention is to keep all the components in a folder named after the component
const CollectionsBadge = () => {
const dispatch = useDispatch();
const { collections } = useSelector((state) => state.collections);
const { collectionSortOrder } = useSelector((state) => state.collections);
const sortCollectionOrder = () => {
let order;
switch (collectionSortOrder) {
case 'default':
order = 'alphabetical';
break;
case 'alphabetical':
order = 'reverseAlphabetical';
break;
case 'reverseAlphabetical':
order = 'default';
break;
}
dispatch(sortCollections({ order }));
};
return (
<div className="items-center mt-2 relative">
<div className="collections-badge flex items-center pl-2 pr-2 py-1 select-none">
<span className="mr-2">
<IconFolders size={18} strokeWidth={1.5} />
</span>
<span>Collections</span>
<div className="collections-badge flex items-center justify-between px-2">
<div className="flex items-center py-1 select-none">
<span className="mr-2">
<IconFolders size={18} strokeWidth={1.5} />
</span>
<span>Collections</span>
</div>
{collections.length >= 1 && (
<button onClick={() => sortCollectionOrder()}>
{collectionSortOrder == 'default' ? (
<IconArrowsSort size={18} strokeWidth={1.5} />
) : collectionSortOrder == 'alphabetical' ? (
<IconSortAscendingLetters size={18} strokeWidth={1.5} />
) : (
<IconSortDescendingLetters size={18} strokeWidth={1.5} />
)}
</button>
)}
</div>
</div>
);

View File

@@ -21,14 +21,15 @@ const CreateCollection = ({ onClose }) => {
},
validationSchema: Yup.object({
collectionName: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.required('collection name is required'),
collectionFolderName: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
.required('folder name is required'),
collectionLocation: Yup.string().required('location is required')
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
}),
onSubmit: (values) => {
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
@@ -43,7 +44,10 @@ const CreateCollection = ({ onClose }) => {
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
formik.setFieldValue('collectionLocation', dirPath);
// When the user closes the diolog without selecting anything dirPath will be false
if (typeof dirPath === 'string') {
formik.setFieldValue('collectionLocation', dirPath);
}
})
.catch((error) => {
formik.setFieldValue('collectionLocation', '');
@@ -63,9 +67,8 @@ const CreateCollection = ({ onClose }) => {
<Modal size="sm" title="Create Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="collectionName" className="flex items-center">
<span className="font-semibold">Name</span>
<Tooltip text="Name of the collection" tooltipId="collection-name" />
<label htmlFor="collection-name" className="flex items-center font-semibold">
Name
</label>
<input
id="collection-name"
@@ -84,9 +87,37 @@ const CreateCollection = ({ onClose }) => {
<div className="text-red-500">{formik.errors.collectionName}</div>
) : null}
<label htmlFor="collectionFolderName" className="flex items-center mt-3">
<label htmlFor="collection-location" className="block font-semibold mt-3">
Location
</label>
<input
id="collection-location"
type="text"
name="collectionLocation"
readOnly={true}
className="block textbox mt-2 w-full cursor-pointer"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionLocation || ''}
onClick={browse}
/>
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
<div className="text-red-500">{formik.errors.collectionLocation}</div>
) : null}
<div className="mt-1">
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
Browse
</span>
</div>
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
<span className="font-semibold">Folder Name</span>
<Tooltip text="Name of the folder where your collection is stored" tooltipId="collection-folder-name" />
<Tooltip
text="This folder will be created under the selected location"
tooltipId="collection-folder-name-tooltip"
/>
</label>
<input
id="collection-folder-name"
@@ -103,34 +134,6 @@ const CreateCollection = ({ onClose }) => {
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
) : null}
<>
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
Location
</label>
<input
id="collection-location"
type="text"
name="collectionLocation"
readOnly={true}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionLocation || ''}
onClick={browse}
/>
</>
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
<div className="text-red-500">{formik.errors.collectionLocation}</div>
) : null}
<div className="mt-1">
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
Browse
</span>
</div>
</div>
</form>
</Modal>

View File

@@ -16,7 +16,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
},
validationSchema: Yup.object({
collectionLocation: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.max(500, 'must be 500 characters or less')
.required('name is required')
}),

View File

@@ -16,7 +16,7 @@ const NewFolder = ({ collection, item, onClose }) => {
},
validationSchema: Yup.object({
folderName: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.required('name is required')
.test({
name: 'folderName',

View File

@@ -25,12 +25,15 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
},
validationSchema: Yup.object({
requestName: Yup.string()
.min(1, 'must be atleast 1 characters')
.min(1, 'must be at least 1 character')
.required('name is required')
.test({
name: 'requestName',
message: 'The request name "index" is reserved in bruno',
test: (value) => value && !value.trim().toLowerCase().includes('index')
message: `The request names - collection and folder is reserved in bruno`,
test: (value) => {
const trimmedValue = value.trim().toLowerCase();
return !['collection', 'folder'].includes(trimmedValue);
}
})
}),
onSubmit: (values) => {

View File

@@ -18,6 +18,7 @@ const TitleBar = () => {
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
const dispatch = useDispatch();
const { ipcRenderer } = window;
const handleImportCollection = (collection) => {
setImportedCollection(collection);
@@ -50,6 +51,10 @@ const TitleBar = () => {
);
};
const openDevTools = () => {
ipcRenderer.invoke('renderer:open-devtools');
};
return (
<StyledWrapper className="px-2 py-2">
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
@@ -104,6 +109,15 @@ const TitleBar = () => {
>
Import Collection
</div>
<div
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
openDevTools();
}}
>
Devtools
</div>
</Dropdown>
</div>
</div>

View File

@@ -96,27 +96,16 @@ const Sidebar = () => {
/>
</div>
<div className="pl-1" style={{ position: 'relative', top: '3px' }}>
{storedTheme === 'dark' ? (
<GitHubButton
href="https://github.com/usebruno/bruno"
data-color-scheme="no-preference: dark; light: dark; dark: light;"
data-show-count="true"
aria-label="Star usebruno/bruno on GitHub"
>
Star
</GitHubButton>
) : (
<GitHubButton
href="https://github.com/usebruno/bruno"
data-color-scheme="no-preference: light; light: light; dark: light;"
data-show-count="true"
aria-label="Star usebruno/bruno on GitHub"
>
Star
</GitHubButton>
)}
<GitHubButton
href="https://github.com/usebruno/bruno"
data-color-scheme={storedTheme}
data-show-count="true"
aria-label="Star usebruno/bruno on GitHub"
>
Star
</GitHubButton>
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.16.2</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.22.1</div>
</div>
</div>
</div>

View File

@@ -9,6 +9,40 @@ const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODE
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
CodeMirror.registerHelper('hint', 'anyword', (editor, options) => {
const word = /[\w$-]+/;
const wordlist = (options && options.autocomplete) || [];
let cur = editor.getCursor(),
curLine = editor.getLine(cur.line);
let end = cur.ch,
start = end;
while (start && word.test(curLine.charAt(start - 1))) --start;
let curWord = start != end && curLine.slice(start, end);
// Check if curWord is a valid string before proceeding
if (typeof curWord !== 'string' || curWord.length < 3) {
return null; // Abort the hint
}
const list = (options && options.list) || [];
const re = new RegExp(word.source, 'g');
for (let dir = -1; dir <= 1; dir += 2) {
let line = cur.line,
endLine = Math.min(Math.max(line + dir * 500, editor.firstLine()), editor.lastLine()) + dir;
for (; line != endLine; line += dir) {
let text = editor.getLine(line),
m;
while ((m = re.exec(text))) {
if (line == cur.line && curWord.length < 3) continue;
list.push(...wordlist.filter((el) => el.toLowerCase().startsWith(curWord.toLowerCase())));
}
}
}
return { list: [...new Set(list)], from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end) };
});
CodeMirror.commands.autocomplete = (cm, hint, options) => {
cm.showHint({ hint, ...options });
};
}
class SingleLineEditor extends Component {
@@ -32,6 +66,7 @@ class SingleLineEditor extends Component {
variables: getAllVariables(this.props.collection)
},
scrollbarStyle: null,
tabindex: 0,
extraKeys: {
Enter: () => {
if (this.props.onRun) {
@@ -70,9 +105,19 @@ class SingleLineEditor extends Component {
},
'Cmd-F': () => {},
'Ctrl-F': () => {},
Tab: () => {}
// Tabbing disabled to make tabindex work
Tab: false,
'Shift-Tab': false
}
});
if (this.props.autocomplete) {
this.editor.on('keyup', (cm, event) => {
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) {
/*Enter - do not open autocomplete list just after item has been selected in it*/
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
}
});
}
this.editor.setValue(this.props.value || '');
this.editor.on('change', this._onEdit);
this.addOverlay();

View File

@@ -87,7 +87,7 @@ const VariablesEditor = ({ collection }) => {
<EnvVariables collection={collection} theme={reactInspectorTheme} />
<div className="mt-8 muted text-xs">
Note: As of today, collection variables can only be set via the api -{' '}
Note: As of today, collection variables can only be set via the API -{' '}
<span className="font-medium">getVar()</span> and <span className="font-medium">setVar()</span>. <br />
In the next release, we will add a UI to set and modify collection variables.
</div>

View File

@@ -54,7 +54,7 @@ const Welcome = () => {
<Bruno width={50} />
</div>
<div className="text-xl font-semibold select-none">bruno</div>
<div className="mt-4">Opensource IDE for exploring and testing api's</div>
<div className="mt-4">Opensource IDE for exploring and testing APIs</div>
<div className="uppercase font-semibold heading mt-10">Collections</div>
<div className="mt-4 flex items-center collection-options select-none">

View File

@@ -129,6 +129,24 @@ const GlobalStyle = createGlobalStyle`
}
}
@keyframes rotateClockwise {
0% {
transform: scaleY(-1) rotate(0deg);
}
100% {
transform: scaleY(-1) rotate(360deg);
}
}
@keyframes rotateCounterClockwise {
0% {
transform: scaleY(-1) rotate(360deg);
}
100% {
transform: scaleY(-1) rotate(0deg);
}
}
// codemirror
.CodeMirror {
.cm-variable-valid {

View File

@@ -14,24 +14,25 @@ const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODE
if (!SERVER_RENDERED) {
require('codemirror/mode/javascript/javascript');
require('codemirror/mode/xml/xml');
require('codemirror/addon/scroll/simplescrollbars');
require('codemirror/addon/comment/comment');
require('codemirror/addon/dialog/dialog');
require('codemirror/addon/edit/closebrackets');
require('codemirror/addon/edit/matchbrackets');
require('codemirror/addon/fold/brace-fold');
require('codemirror/addon/fold/foldgutter');
require('codemirror/addon/mode/overlay');
require('codemirror/addon/hint/show-hint');
require('codemirror/keymap/sublime');
require('codemirror/addon/comment/comment');
require('codemirror/addon/edit/closebrackets');
require('codemirror/addon/lint/lint');
require('codemirror/addon/mode/overlay');
require('codemirror/addon/scroll/simplescrollbars');
require('codemirror/addon/search/jump-to-line');
require('codemirror/addon/search/search');
require('codemirror/addon/search/searchcursor');
require('codemirror/addon/search/jump-to-line');
require('codemirror/addon/dialog/dialog');
require('codemirror/keymap/sublime');
require('codemirror-graphql/hint');
require('codemirror-graphql/lint');
require('codemirror-graphql/info');
require('codemirror-graphql/jump');
require('codemirror-graphql/lint');
require('codemirror-graphql/mode');
require('utils/codemirror/brunoVarInfo');

View File

@@ -0,0 +1,44 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidMount() {
// Add a global error event listener to capture client-side errors
window.onerror = (message, source, lineno, colno, error) => {
this.setState({ hasError: true, error });
};
}
componentDidCatch(error, errorInfo) {
console.log({ error, errorInfo });
}
render() {
if (this.state.hasError) {
return (
<div className="flex items-center justify-center p-10">
<div className="bg-white rounded-lg shadow-lg p-4 w-full">
<h1 className="text-2xl font-semibold text-red-600 mb-2">Oops! Something went wrong</h1>
<p className="text-red-600 mb-2">{this.state.error && this.state.error.toString()}</p>
{this.state.error && this.state.error.stack && (
<pre className="bg-gray-100 p-2 rounded-lg overflow-auto">{this.state.error.stack}</pre>
)}
<button
className="bg-red-500 text-white px-4 py-2 mt-4 rounded hover:bg-red-600 transition"
onClick={() => {
this.setState({ hasError: false, error: null });
}}
>
Close
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -7,6 +7,7 @@ import { PreferencesProvider } from 'providers/Preferences';
import ReduxStore from 'providers/ReduxStore';
import ThemeProvider from 'providers/Theme/index';
import ErrorBoundary from './ErrorBoundary';
import '../styles/app.scss';
import '../styles/globals.css';
@@ -14,6 +15,7 @@ import 'tailwindcss/dist/tailwind.min.css';
import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css';
import 'react-tooltip/dist/react-tooltip.css';
import '@usebruno/graphql-docs/dist/esm/index.css';
function SafeHydrate({ children }) {
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
@@ -41,23 +43,25 @@ function MyApp({ Component, pageProps }) {
}
return (
<SafeHydrate>
<NoSsr>
<Provider store={ReduxStore}>
<ThemeProvider>
<ToastProvider>
<AppProvider>
<PreferencesProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</PreferencesProvider>
</AppProvider>
</ToastProvider>
</ThemeProvider>
</Provider>
</NoSsr>
</SafeHydrate>
<ErrorBoundary>
<SafeHydrate>
<NoSsr>
<Provider store={ReduxStore}>
<ThemeProvider>
<ToastProvider>
<AppProvider>
<PreferencesProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</PreferencesProvider>
</AppProvider>
</ToastProvider>
</ThemeProvider>
</Provider>
</NoSsr>
</SafeHydrate>
</ErrorBoundary>
);
}

View File

@@ -1,6 +1,7 @@
import React, { useEffect } from 'react';
import useTelemetry from './useTelemetry';
import useCollectionTreeSync from './useCollectionTreeSync';
import useCollectionNextAction from './useCollectionNextAction';
import { useDispatch } from 'react-redux';
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
@@ -10,6 +11,7 @@ export const AppContext = React.createContext();
export const AppProvider = (props) => {
useTelemetry();
useCollectionTreeSync();
useCollectionNextAction();
const dispatch = useDispatch();

View File

@@ -0,0 +1,35 @@
import React, { useEffect } from 'react';
import get from 'lodash/get';
import each from 'lodash/each';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { getDefaultRequestPaneTab, findItemInCollectionByPathname } from 'utils/collections/index';
import { hideHomePage } from 'providers/ReduxStore/slices/app';
import { updateNextAction } from 'providers/ReduxStore/slices/collections/index';
import { useSelector, useDispatch } from 'react-redux';
const useCollectionNextAction = () => {
const collections = useSelector((state) => state.collections.collections);
const dispatch = useDispatch();
useEffect(() => {
each(collections, (collection) => {
if (collection.nextAction && collection.nextAction.type === 'OPEN_REQUEST') {
const item = findItemInCollectionByPathname(collection, get(collection, 'nextAction.payload.pathname'));
if (item) {
dispatch(updateNextAction(collection.uid, null));
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item.type)
})
);
dispatch(hideHomePage());
}
}
});
}, [collections, each, dispatch, updateNextAction, hideHomePage, addTab]);
};
export default useCollectionNextAction;

View File

@@ -7,7 +7,6 @@ import SaveRequest from 'components/RequestPane/SaveRequest';
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
import NetworkError from 'components/ResponsePane/NetworkError';
import NewRequest from 'components/Sidebar/NewRequest';
import BrunoSupport from 'components/BrunoSupport';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
@@ -22,7 +21,6 @@ export const HotkeysProvider = (props) => {
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
const [showBrunoSupportModal, setShowBrunoSupportModal] = useState(false);
const getCurrentCollectionItems = () => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
@@ -53,7 +51,8 @@ export const HotkeysProvider = (props) => {
if (item && item.uid) {
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
} else {
setShowSaveRequestModal(true);
// todo: when ephermal requests go live
// setShowSaveRequestModal(true);
}
}
}
@@ -133,18 +132,6 @@ export const HotkeysProvider = (props) => {
};
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
// help (ctrl/cmd + h)
useEffect(() => {
Mousetrap.bind(['command+h', 'ctrl+h'], (e) => {
setShowBrunoSupportModal(true);
return false; // this stops the event bubbling
});
return () => {
Mousetrap.unbind(['command+h', 'ctrl+h']);
};
}, [setShowNewRequestModal]);
// close tab hotkey
useEffect(() => {
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
@@ -164,7 +151,6 @@ export const HotkeysProvider = (props) => {
return (
<HotkeysContext.Provider {...props} value="hotkey">
{showBrunoSupportModal && <BrunoSupport onClose={() => setShowBrunoSupportModal(false)} />}
{showSaveRequestModal && (
<SaveRequest items={getCurrentCollectionItems()} onClose={() => setShowSaveRequestModal(false)} />
)}

View File

@@ -12,7 +12,6 @@ import {
getItemsToResequence,
moveCollectionItemToRootOfCollection,
findCollectionByUid,
recursivelyGetAllItemUids,
transformRequestToSaveToFilesystem,
findParentItemInCollection,
findEnvironmentInCollection,
@@ -22,11 +21,12 @@ import {
} from 'utils/collections';
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
import { waitForNextTick } from 'utils/common';
import { getDirectoryName } from 'utils/common/platform';
import { getDirectoryName, isWindowsOS } from 'utils/common/platform';
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
import {
updateLastAction,
updateNextAction,
resetRunResults,
requestCancelled,
responseReceived,
@@ -39,6 +39,7 @@ import {
createCollection as _createCollection,
renameCollection as _renameCollection,
removeCollection as _removeCollection,
sortCollections as _sortCollections,
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
} from './index';
@@ -81,8 +82,35 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
itemSchema
.validate(itemToSave)
.then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave))
.then(() => toast.success('Request saved successfully'))
.then(resolve)
.catch(reject);
.catch((err) => {
toast.error('Failed to save request!');
reject(err);
});
});
};
export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
console.log(collection.root);
return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}
const { ipcRenderer } = window;
ipcRenderer
.invoke('renderer:save-collection-root', collection.pathname, collection.root)
.then(() => toast.success('Collection Settings saved successfully'))
.then(resolve)
.catch((err) => {
toast.error('Failed to save collection settings!');
reject(err);
});
});
};
@@ -145,6 +173,11 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
.catch((err) => console.log(err));
};
// todo: this can be directly put inside the collections/index.js file
// the coding convention is to put only actions that need ipc in this file
export const sortCollections = (order) => (dispatch) => {
dispatch(_sortCollections(order));
};
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -262,7 +295,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
}
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
ipcRenderer
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
.then(() => {
// In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
// But in windows we don't get those events, so we need to update the state manually
// This looks like an issue in our watcher library chokidar
// GH: https://github.com/usebruno/bruno/issues/251
if (isWindowsOS()) {
dispatch(_renameItem({ newName, itemUid, collectionUid }));
}
resolve();
})
.catch(reject);
});
};
@@ -346,7 +391,16 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
ipcRenderer
.invoke('renderer:delete-item', item.pathname, item.type)
.then(() => resolve())
.then(() => {
// In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state
// But in windows we don't get those events, so we need to update the state manually
// This looks like an issue in our watcher library chokidar
// GH: https://github.com/usebruno/bruno/issues/265
if (isWindowsOS()) {
dispatch(_deleteItem({ itemUid, collectionUid }));
}
resolve();
})
.catch((error) => reject(error));
}
return;
@@ -569,6 +623,19 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
// the useCollectionNextAction() will track this and open the new request in a new tab
// once the request is created
dispatch(
updateNextAction({
nextAction: {
type: 'OPEN_REQUEST',
payload: {
pathname: fullName
}
},
collectionUid
})
);
} else {
return reject(new Error('Duplicate request names are not allowed under the same folder'));
}
@@ -586,6 +653,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
// the useCollectionNextAction() will track this and open the new request in a new tab
// once the request is created
dispatch(
updateNextAction({
nextAction: {
type: 'OPEN_REQUEST',
payload: {
pathname: fullName
}
},
collectionUid
})
);
} else {
return reject(new Error('Duplicate request names are not allowed under the same folder'));
}
@@ -620,6 +701,37 @@ export const addEnvironment = (name, collectionUid) => (dispatch, getState) => {
});
};
export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (!collection) {
return reject(new Error('Collection not found'));
}
const baseEnv = findEnvironmentInCollection(collection, baseEnvUid);
if (!collection) {
return reject(new Error('Environmnent not found'));
}
ipcRenderer
.invoke('renderer:copy-environment', collection.pathname, name, baseEnv.variables)
.then(
dispatch(
updateLastAction({
collectionUid,
lastAction: {
type: 'ADD_ENVIRONMENT',
payload: name
}
})
)
)
.then(resolve)
.catch(reject);
});
};
export const renameEnvironment = (newName, environmentUid, collectionUid) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const state = getState();

View File

@@ -7,6 +7,8 @@ import concat from 'lodash/concat';
import filter from 'lodash/filter';
import each from 'lodash/each';
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import set from 'lodash/set';
import { createSlice } from '@reduxjs/toolkit';
import { splitOnFirst } from 'utils/url';
import {
@@ -28,7 +30,8 @@ import { getSubdirectoriesFromRoot, getDirectoryName } from 'utils/common/platfo
const PATH_SEPARATOR = path.sep;
const initialState = {
collections: []
collections: [],
collectionSortOrder: 'default'
};
export const collectionsSlice = createSlice({
@@ -39,13 +42,21 @@ export const collectionsSlice = createSlice({
const collectionUids = map(state.collections, (c) => c.uid);
const collection = action.payload;
collection.settingsSelectedTab = 'headers';
// TODO: move this to use the nextAction approach
// last action is used to track the last action performed on the collection
// this is optional
// this is used in scenarios where we want to know the last action performed on the collection
// and take some extra action based on that
// for example, when a env is created, we want to auto select it the env modal
collection.importedAt = new Date().getTime();
collection.lastAction = null;
// an improvement over the above approach.
// this defines an action that need to be performed next and is executed vy the useCollectionNextAction()
collection.nextAction = null;
collapseCollection(collection);
addDepth(collection.items);
if (!collectionUids.includes(collection.uid)) {
@@ -70,6 +81,20 @@ export const collectionsSlice = createSlice({
removeCollection: (state, action) => {
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
},
sortCollections: (state, action) => {
state.collectionSortOrder = action.payload.order;
switch (action.payload.order) {
case 'default':
state.collections = state.collections.sort((a, b) => a.importedAt - b.importedAt);
break;
case 'alphabetical':
state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name));
break;
case 'reverseAlphabetical':
state.collections = state.collections.sort((a, b) => b.name.localeCompare(a.name));
break;
}
},
updateLastAction: (state, action) => {
const { collectionUid, lastAction } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -78,6 +103,23 @@ export const collectionsSlice = createSlice({
collection.lastAction = lastAction;
}
},
updateNextAction: (state, action) => {
const { collectionUid, nextAction } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.nextAction = nextAction;
}
},
updateSettingsSelectedTab: (state, action) => {
const { collectionUid, tab } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.settingsSelectedTab = tab;
}
},
collectionUnlinkEnvFileEvent: (state, action) => {
const { data: environment, meta } = action.payload;
const collection = findCollectionByUid(state.collections, meta.collectionUid);
@@ -229,6 +271,17 @@ export const collectionsSlice = createSlice({
}
}
},
deleteRequestDraft: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && item.draft) {
item.draft = null;
}
}
},
newEphemeralHttpRequest: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -307,6 +360,31 @@ export const collectionsSlice = createSlice({
}
}
},
updateAuth: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
}
item.draft.request.auth = item.draft.request.auth || {};
switch (action.payload.mode) {
case 'bearer':
item.draft.request.auth.mode = 'bearer';
item.draft.request.auth.bearer = action.payload.content;
break;
case 'basic':
item.draft.request.auth.mode = 'basic';
item.draft.request.auth.basic = action.payload.content;
break;
}
}
}
},
addQueryParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -865,10 +943,100 @@ export const collectionsSlice = createSlice({
}
}
},
updateCollectionAuthMode: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.auth.mode', action.payload.mode);
}
},
updateCollectionAuth: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
switch (action.payload.mode) {
case 'bearer':
set(collection, 'root.request.auth.bearer', action.payload.content);
break;
case 'basic':
set(collection, 'root.request.auth.basic', action.payload.content);
break;
}
}
},
updateCollectionRequestScript: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.script.req', action.payload.script);
}
},
updateCollectionResponseScript: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.script.res', action.payload.script);
}
},
updateCollectionTests: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
set(collection, 'root.request.tests', action.payload.tests);
}
},
addCollectionHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const headers = get(collection, 'root.request.headers', []);
headers.push({
uid: uuid(),
name: '',
value: '',
description: '',
enabled: true
});
set(collection, 'root.request.headers', headers);
}
},
updateCollectionHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const headers = get(collection, 'root.request.headers', []);
const header = find(headers, (h) => h.uid === action.payload.header.uid);
if (header) {
header.name = action.payload.header.name;
header.value = action.payload.header.value;
header.description = action.payload.header.description;
header.enabled = action.payload.header.enabled;
}
}
},
deleteCollectionHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
let headers = get(collection, 'root.request.headers', []);
headers = filter(headers, (h) => h.uid !== action.payload.headerUid);
set(collection, 'root.request.headers', headers);
}
},
collectionAddFileEvent: (state, action) => {
const file = action.payload.file;
const isCollectionRoot = file.meta.collectionRoot ? true : false;
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
if (isCollectionRoot) {
if (collection) {
collection.root = file.data;
}
console.log('collectionAddFileEvent', file);
return;
}
if (collection) {
const dirname = getDirectoryName(file.meta.pathname);
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
@@ -953,6 +1121,12 @@ export const collectionsSlice = createSlice({
const { file } = action.payload;
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
// check and update collection root
if (collection && file.meta.collectionRoot) {
collection.root = file.data;
return;
}
if (collection) {
const item = findItemInCollection(collection, file.data.uid);
@@ -1049,7 +1223,6 @@ export const collectionsSlice = createSlice({
const { cancelTokenUid } = action.payload;
item.requestUid = requestUid;
item.requestState = 'queued';
item.response = null;
item.cancelTokenUid = cancelTokenUid;
}
@@ -1155,7 +1328,10 @@ export const {
brunoConfigUpdateEvent,
renameCollection,
removeCollection,
sortCollections,
updateLastAction,
updateNextAction,
updateSettingsSelectedTab,
collectionUnlinkEnvFileEvent,
saveEnvironment,
selectEnvironment,
@@ -1168,10 +1344,12 @@ export const {
requestCancelled,
responseReceived,
saveRequest,
deleteRequestDraft,
newEphemeralHttpRequest,
collectionClicked,
collectionFolderClicked,
requestUrlChanged,
updateAuth,
addQueryParam,
updateQueryParam,
deleteQueryParam,
@@ -1199,6 +1377,14 @@ export const {
addVar,
updateVar,
deleteVar,
addCollectionHeader,
updateCollectionHeader,
deleteCollectionHeader,
updateCollectionAuthMode,
updateCollectionAuth,
updateCollectionRequestScript,
updateCollectionResponseScript,
updateCollectionTests,
collectionAddFileEvent,
collectionAddDirectoryEvent,
collectionChangeFileEvent,

View File

@@ -53,3 +53,12 @@ body::-webkit-scrollbar-thumb,
background-color: #cdcdcd;
border-radius: 5rem;
}
/*
* todo: this will be supported in the future to be changed via applying a theme
* making all the checkboxes and radios bigger
* input[type='checkbox'],
* input[type='radio'] {
* transform: scale(1.1);
* }
*/

View File

@@ -9,13 +9,20 @@ const darkTheme = {
green: 'rgb(11 178 126)',
danger: '#f06f57',
muted: '#9d9d9d',
purple: '#cd56d6'
purple: '#cd56d6',
yellow: '#f59e0b'
},
bg: {
danger: '#d03544'
}
},
input: {
bg: 'rgb(65, 65, 65)',
border: 'rgb(65, 65, 65)',
focusBorder: 'rgb(65, 65, 65)'
},
variables: {
bg: 'rgb(48, 48, 49)',
@@ -79,7 +86,11 @@ const darkTheme = {
get: '#8cd656',
post: '#cd56d6',
put: '#d69956',
delete: '#f06f57'
delete: '#f06f57',
// customize these colors if needed
patch: '#d69956',
options: '#d69956',
head: '#d69956'
}
},
@@ -98,7 +109,8 @@ const darkTheme = {
responseSendIcon: '#555',
responseStatus: '#ccc',
responseOk: '#8cd656',
responseError: '#f06f57'
responseError: '#f06f57',
responseOverlayBg: 'rgba(30, 30, 30, 0.6)'
},
collection: {

View File

@@ -9,13 +9,20 @@ const lightTheme = {
green: '#047857',
danger: 'rgb(185, 28, 28)',
muted: '#4b5563',
purple: '#8e44ad'
purple: '#8e44ad',
yellow: '#d97706'
},
bg: {
danger: '#dc3545'
}
},
input: {
bg: 'white',
border: '#ccc',
focusBorder: '#8b8b8b'
},
menubar: {
bg: 'rgb(44, 44, 44)'
},
@@ -79,7 +86,11 @@ const lightTheme = {
get: 'rgb(5, 150, 105)',
post: '#8e44ad',
put: '#ca7811',
delete: 'rgb(185, 28, 28)'
delete: 'rgb(185, 28, 28)',
// customize these colors if needed
patch: '#ca7811',
options: '#ca7811',
head: '#ca7811'
}
},
@@ -98,7 +109,8 @@ const lightTheme = {
responseSendIcon: 'rgb(209, 213, 219)',
responseStatus: 'rgb(117 117 117)',
responseOk: '#047857',
responseError: 'rgb(185, 28, 28)'
responseError: 'rgb(185, 28, 28)',
responseOverlayBg: 'rgba(255, 255, 255, 0.6)'
},
collection: {

View File

@@ -0,0 +1,71 @@
const createContentType = (mode) => {
switch (mode) {
case 'json':
return 'application/json';
case 'xml':
return 'application/xml';
case 'formUrlEncoded':
return 'application/x-www-form-urlencoded';
case 'multipartForm':
return 'multipart/form-data';
default:
return 'application/json';
}
};
const createHeaders = (headers, mode) => {
const contentType = createContentType(mode);
const headersArray = headers
.filter((header) => header.enabled)
.map((header) => {
return {
name: header.name,
value: header.value
};
});
const headerNames = headersArray.map((header) => header.name);
if (!headerNames.includes('Content-Type')) {
return [...headersArray, { name: 'Content-Type', value: contentType }];
}
return headersArray;
};
const createQuery = (queryParams = []) => {
return queryParams.map((param) => {
return {
name: param.name,
value: param.value
};
});
};
const createPostData = (body) => {
const contentType = createContentType(body.mode);
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
return {
mimeType: contentType,
params: body[body.mode]
.filter((param) => param.enabled)
.map((param) => ({ name: param.name, value: param.value }))
};
} else {
return {
mimeType: contentType,
text: body[body.mode]
};
}
};
export const buildHarRequest = (request) => {
return {
method: request.method,
url: request.url,
httpVersion: 'HTTP/1.1',
cookies: [],
headers: createHeaders(request.headers, request.body.mode),
queryString: createQuery(request.params),
postData: createPostData(request.body),
headersSize: 0,
bodySize: 0
};
};

View File

@@ -66,8 +66,7 @@ if (!SERVER_RENDERED) {
if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) {
return;
}
if (target.className !== 'cm-variable-valid') {
if (!target.classList.contains('cm-variable-valid')) {
return;
}

View File

@@ -129,24 +129,28 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
if (draggedItemParent) {
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
} else {
collection.items = sortBy(collection.items, (item) => item.seq);
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
}
if (targetItem.type === 'folder') {
targetItem.items = targetItem.items || [];
targetItem.items = sortBy(targetItem.items || [], (item) => item.seq);
targetItem.items.push(draggedItem);
draggedItem.pathname = path.join(targetItem.pathname, draggedItem.filename);
} else {
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
if (targetItemParent) {
targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq);
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
} else {
collection.items = sortBy(collection.items, (item) => item.seq);
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
@@ -162,7 +166,9 @@ export const moveCollectionItemToRootOfCollection = (collection, draggedItem) =>
return;
}
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
collection.items = sortBy(collection.items, (item) => item.seq);
collection.items.push(draggedItem);
if (draggedItem.type == 'folder') {
draggedItem.pathname = path.join(collection.pathname, draggedItem.name);
@@ -203,7 +209,7 @@ export const getItemsToResequence = (parent, collection) => {
return itemsToResequence;
};
export const transformCollectionToSaveToIdb = (collection, options = {}) => {
export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => {
const copyHeaders = (headers) => {
return map(headers, (header) => {
return {
@@ -281,6 +287,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
},
auth: {
mode: get(si.draft.request, 'auth.mode', 'none'),
basic: {
username: get(si.draft.request, 'auth.basic.username', ''),
password: get(si.draft.request, 'auth.basic.password', '')
},
bearer: {
token: get(si.draft.request, 'auth.bearer.token', '')
}
},
script: si.draft.request.script,
vars: si.draft.request.vars,
assertions: si.draft.request.assertions,
@@ -303,6 +319,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
},
auth: {
mode: get(si.request, 'auth.mode', 'none'),
basic: {
username: get(si.request, 'auth.basic.username', ''),
password: get(si.request, 'auth.basic.password', '')
},
bearer: {
token: get(si.request, 'auth.bearer.token', '')
}
},
script: si.request.script,
vars: si.request.vars,
assertions: si.request.assertions,
@@ -351,6 +377,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
url: _item.request.url,
params: [],
headers: [],
auth: _item.request.auth,
body: _item.request.body,
script: _item.request.script,
vars: _item.request.vars,

View File

@@ -25,10 +25,11 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
stream.eat('}');
let found = pathFoundInVariables(word, variables);
if (found) {
return 'variable-valid';
return 'variable-valid random-' + (Math.random() + 1).toString(36).substring(9);
} else {
return 'variable-invalid';
return 'variable-invalid random-' + (Math.random() + 1).toString(36).substring(9);
}
// Random classname added so adjacent variables are not rendered in the same SPAN by CodeMirror.
}
word += ch;
}
@@ -41,3 +42,25 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
});
};
export const getCodeMirrorModeBasedOnContentType = (contentType) => {
if (!contentType || typeof contentType !== 'string') {
return 'application/text';
}
if (contentType.includes('json')) {
return 'application/ld+json';
} else if (contentType.includes('xml')) {
return 'application/xml';
} else if (contentType.includes('html')) {
return 'application/html';
} else if (contentType.includes('text')) {
return 'application/text';
} else if (contentType.includes('application/edn')) {
return 'application/xml';
} else if (contentType.includes('yaml')) {
return 'application/yaml';
} else {
return 'application/text';
}
};

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