Compare commits

..

159 Commits

Author SHA1 Message Date
liyasthomas
aae2dac588 build: lock codemirror 2022-01-24 13:00:38 +05:30
liyasthomas
10a54d14c2 Merge branch 'fix/urlencoded' 2022-01-24 10:09:31 +05:30
liyasthomas
9475a162f7 chore: clean up 2022-01-24 10:00:07 +05:30
liyasthomas
758460210a build: bump deps 2022-01-24 05:44:10 +05:30
liyasthomas
1350919c78 docs: updated copyright year 2022-01-24 05:41:24 +05:30
Andrew Bastin
ffe659673d refactor: body type transition to ruleset based system 2022-01-23 23:32:05 +05:30
liyasthomas
f24f97b6ef feat: working parameters 2022-01-23 16:26:29 +05:30
Andrew Bastin
7861c0006f fix: urlencoded not switching to formdata 2022-01-23 14:13:02 +05:30
liyasthomas
b57b948107 fix: urlencoded formdata key value params ui
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-01-23 04:45:05 +05:30
kyteinsky
6205b5f163 fix: multipart form data sorting (#2067)
* fix: multipart form data sorting

  place all the file types in multipart form data
  body in the last to avoid errors due to map key
  being placed after file type data

* refactor: fp-ify formdata file sort implementation

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-01-23 02:30:39 +05:30
Andrew Bastin
3bb65f2115 fix: json linting not working on raw body + small ts corrections 2022-01-22 22:16:27 +05:30
liyasthomas
1ba89a0f0b fix: url encoded binding 2022-01-21 23:21:23 +05:30
Andrew Bastin
73cccf73df refactor: urlencoded key value pair system 2022-01-21 19:32:05 +05:30
Andrew Bastin
09269b3cec fix: urlencoded form evaluation issues 2022-01-21 18:23:15 +05:30
Andrew Bastin
0efbc58b26 fix: urlencoded body params being improper 2022-01-21 17:43:28 +05:30
liyasthomas
238e41ccda refactor: updated firebase rules 2022-01-21 13:13:40 +05:30
Andrew Bastin
5043606701 refactor: move collection data structures and utility functions to hopp/data 2022-01-21 00:21:17 +05:30
Balázs Úr
6b0494ddba chore(i18n): updated Hungarian translation (#2073) 2022-01-20 06:10:37 +05:30
liyasthomas
ed73f4179a chore(i18n): updated translations 2022-01-19 19:39:14 +05:30
Rishabh Agarwal
647c347eb1 Refactoring Lenses Using Vue Composables (#1995) 2022-01-17 21:00:48 +05:30
liyasthomas
ddff126aaa chore(deps): bump 2022-01-17 19:20:30 +05:30
liyasthomas
91d0b52222 chore(i18n): updated translations 2022-01-17 17:02:15 +05:30
liyasthomas
38fddccc5b Merge branch 'refactor/import-export' 2022-01-17 14:42:56 +05:30
Andrew Bastin
1719bb7bc9 fix: tiny issues with importers 2022-01-14 21:12:29 +05:30
liyasthomas
588d1119b5 Merge branch 'refactor/import-export' of https://github.com/hoppscotch/hoppscotch into refactor/import-export 2022-01-14 20:41:18 +05:30
liyasthomas
e323b4355e feat: ui for new importer 2022-01-14 20:40:54 +05:30
Andrew Bastin
f4d98b9f43 fix: env var parsing in importers and param import issues in postman 2022-01-14 15:03:41 +05:30
liyasthomas
67934fd19a chore: cleanup 2022-01-14 08:27:39 +05:30
liyasthomas
396804a4d6 chore(i18n): updated translations 2022-01-14 06:33:36 +05:30
Andrew Bastin
bcadb142fd feat: revamped insomnia importer 2022-01-14 04:40:27 +05:30
Andrew Bastin
aeb848399a refactor: revamped postman collection importer 2022-01-13 02:32:22 +05:30
liyasthomas
ffb063ad94 feat: target my collections importer ui 2022-01-12 07:41:36 +05:30
liyasthomas
6537a51854 fix: gist hoppscotch importer 2022-01-11 19:32:45 +05:30
liyasthomas
04d73a855d chore: split url and file importer steps 2022-01-11 16:04:42 +05:30
liyasthomas
f6fc3782b5 feat: ui for new importer 2022-01-10 22:02:35 +05:30
Andrew Bastin
05ca0c0966 refactor: port postman and insomnia importers 2022-01-10 21:16:36 +05:30
Andrew Bastin
9e6a3883ac feat: add support to openapi importer for yaml 2022-01-10 20:13:05 +05:30
liyasthomas
7f7338faeb chore(deps): bump 2022-01-10 10:29:31 +05:30
liyasthomas
b40b8070c1 chore: update steps ui 2022-01-09 11:13:42 +05:30
Andrew Bastin
24fabc8b7b feat: correct typing and rename newopenapi importer to openapi 2022-01-07 23:46:20 +05:30
Andrew Bastin
a9bd52d154 feat: introduce oauth parsing to openapi 2022-01-07 23:40:06 +05:30
Andrew Bastin
4cec5a7bb4 feat: initial new openapi importer implementation 2022-01-06 22:47:25 +05:30
baiy
720d54f91d fix: chinese pinyin input - fix #2039 (#2057) 2022-01-06 20:48:36 +05:30
Andrew Bastin
e9e791ce90 feat: add typing for importer errors 2022-01-06 19:35:33 +05:30
liyasthomas
d6ec3158bc feat: import hoppscotch collections from file 2022-01-06 19:35:33 +05:30
liyasthomas
fac82200d1 refactor: minor ui improvements to import steps 2022-01-06 19:35:33 +05:30
liyasthomas
d5866fbb3b fix: split importers list 2022-01-06 19:35:33 +05:30
liyasthomas
660479f8a6 chore: init steps ui 2022-01-06 19:35:33 +05:30
Andrew Bastin
7dace3f9b4 fix: typo in defineImporter 2022-01-06 19:35:33 +05:30
Andrew Bastin
8ac38d7517 feat: steps system metadata 2022-01-06 19:35:33 +05:30
Andrew Bastin
5175a86145 feat: steps system infra 2022-01-06 19:35:33 +05:30
liyasthomas
a9292eed9e fix: collection parser for hopp importer 2022-01-06 19:35:33 +05:30
Andrew Bastin
681a957611 refactor: import/export new architecture 2022-01-06 19:35:33 +05:30
liyasthomas
b6e05d42f3 feat: init new import and export ui 2022-01-06 19:35:33 +05:30
Andrew Bastin
ac979239e8 refactor: add safety coersion for loading rest requests from external source 2022-01-05 20:06:33 +05:30
Andrew Bastin
137d562c86 feat: add function to safely generate rest request that maintains type contracts 2022-01-05 16:13:13 +05:30
liyasthomas
ffd1acdfae chore: lint 2022-01-05 10:17:23 +05:30
liyasthomas
dd7aebaf94 docs: update readme 2022-01-05 09:37:13 +05:30
Andrew Bastin
86ef1a4e14 fix: apply sandbox to HTML preview iframe 2022-01-05 09:18:16 +05:30
liyasthomas
914d20ad37 feat: duplicate team requests 2022-01-04 21:42:55 +05:30
liyasthomas
f17cdff3e1 fix: history search 2022-01-04 05:57:31 +05:30
liyasthomas
75ab7fdb00 chore(deps): bump 2022-01-03 11:34:42 +05:30
Andrew Bastin
6e86e27437 fix: aggregate envs in har generation 2022-01-03 11:28:49 +05:30
Andrew Bastin
67baf74edc fix: add body env resolution for har generation 2022-01-03 11:17:02 +05:30
Andrew Bastin
c3aedac77e fix: issues with har generation 2022-01-03 10:49:34 +05:30
liyasthomas
41e39e2141 chore(deps): bump 2022-01-03 06:44:14 +05:30
liyasthomas
2b4155e969 refactor: improve ui consistency 2022-01-02 07:00:17 +05:30
liyasthomas
bb5acf97c6 feat: keyboard shortcuts for themes 2022-01-01 19:32:34 +05:30
liyasthomas
78ad61f153 fix: better error state 2022-01-01 16:15:21 +05:30
Andrew Bastin
2ec401c766 refactor: lazy compute codegen code and error handling 2022-01-01 16:09:47 +05:30
Andrew Bastin
314c0968b1 fix: envinput crash on debounce handle editor undefined 2022-01-01 15:44:28 +05:30
liyasthomas
4770b86948 fix: stop storing form data files in local session 2022-01-01 12:37:39 +05:30
liyasthomas
312bc98c53 feat: better file input chooser 2022-01-01 01:03:13 +05:30
liyasthomas
e56dcf5d7a refactor: better logic for graphql headers key-value <=> bulk editor 2021-12-31 23:47:48 +05:30
liyasthomas
4ecd69cc5a fix: race condition mutating defaultRESTRequest 2021-12-31 23:25:46 +05:30
Andrew Bastin
b78b2d0e78 fix: bulk headers not resetting on clear content 2021-12-31 21:36:10 +05:30
Andrew Bastin
5f0d1ae52f refactor: params should respect the data model 2021-12-31 21:35:09 +05:30
Andrew Bastin
da5e7fdde5 fix: codemirror cache value didnt update when the view is not mounted 2021-12-31 21:32:00 +05:30
Andrew Bastin
9454a983da fix: snapping when user typing in bulk editor with empty lines in between 2021-12-31 20:42:16 +05:30
Andrew Bastin
ca131697b6 refactor: headers system to respect data model 2021-12-31 20:42:16 +05:30
Andrew Bastin
50e30ee4ea fix: remove old codegen code 2021-12-31 20:37:07 +05:30
Andrew Bastin
aa4fedb71c fix: correct indentation on codegen 2021-12-31 20:37:07 +05:30
Andrew Bastin
f8bbf6613f feat: add build step to allow for nullish coalescing and optional chaining 2021-12-31 20:37:07 +05:30
liyasthomas
07171396ad feat: show error prompt on fail code generation 2021-12-31 20:36:12 +05:30
Andrew Bastin
c493a53ece refactor: update codegen modal to use the new code generator 2021-12-31 20:36:12 +05:30
Andrew Bastin
22676957c3 feat: httpsnippet based codegen implementation 2021-12-31 20:36:12 +05:30
Andrew Bastin
1be2c12062 feat: implement HAR conversion 2021-12-31 20:36:12 +05:30
liyasthomas
1baf73a710 chore(deps): bump 2021-12-31 20:11:05 +05:30
liyasthomas
b343789554 refactor: sort classes 2021-12-31 20:05:39 +05:30
liyasthomas
80956fbd27 refactor: minor ui improvements 2021-12-31 18:39:31 +05:30
liyasthomas
4791590869 fix: minor ui improvements 2021-12-31 13:42:17 +05:30
liyasthomas
a0f7201fae feat: group history by date and time 2021-12-31 12:39:23 +05:30
liyasthomas
2de73ae7b4 Merge branch 'i18n' 2021-12-30 16:27:51 +05:30
Rakshit Arora
93fa3bcd51 feat: socketio bearer authentication (#1999)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2021-12-30 16:25:32 +05:30
Aram Virabyan
065fc95c19 i18n: Update RU translations (#2050)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2021-12-30 16:20:19 +05:30
liyasthomas
3baebb4ec8 Merge branch 'main' into i18n 2021-12-30 16:14:54 +05:30
liyasthomas
dbb0582613 fix: full height HTML preview pane 2021-12-29 12:21:38 +05:30
liyasthomas
a1decaf6ad feat: syntax highlighting for cURL and bulk editors 2021-12-29 08:05:07 +05:30
Anwarul Islam
923c24e3bb Bug/body missing onimport (#2048)
Co-authored-by: liyasthomas <liyascthomas@gmail.com>
2021-12-29 06:46:40 +05:30
liyasthomas
724e1e4c8f fix: trim leading and trailing whitespaces in URL - fixed #2046 2021-12-28 09:36:16 +05:30
Liyas Thomas
ce94fa54ee fix: resolved #2044 (#2045) 2021-12-28 08:55:24 +05:30
Andrew Bastin
6fdeffe9d6 feat: reintroduce html highlighting to the editor 2021-12-27 13:17:25 +05:30
liyasthomas
253170f7ac chore(deps): bump 2021-12-26 20:22:12 +05:30
liyasthomas
579c5e5b78 feat: add install extension buttons on interceptor menu 2021-12-25 20:51:17 +05:30
Satoshi Jek
1dca0fd9ad i18n: Update zh-CN translations (#2042) 2021-12-24 17:06:40 +05:30
liyasthomas
10cb433e80 chore(ui): improve ui consistency 2021-12-24 08:12:35 +05:30
liyasthomas
e89d033bfe feat: add status page link to footer 2021-12-23 19:55:39 +05:30
Anwarul Islam
1eb9eb911e feat: import cURL on paste (#2037)
* feat: import cURL on paste

* feat: import cURL on paste

* feat: pasting cURL command on url field does not paste it in

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-12-23 01:27:06 +05:30
liyasthomas
10586e5288 feat: interceptor selector in bottom bar 2021-12-22 12:45:11 +05:30
liyasthomas
889b59e1e4 feat: add reason phrase to http status code 2021-12-21 21:42:31 +05:30
liyasthomas
3e8ff8ebc9 chore(cleanup): remove console logs 2021-12-21 13:03:04 +05:30
liyasthomas
a517b571d6 refactor: improve import export scripting in environment and graphql 2021-12-21 12:57:23 +05:30
Andrew Bastin
43dace6248 fix: interceptor toggle issues 2021-12-21 11:30:39 +05:30
Andrew Bastin
ba3df75a23 refactor: collections import export 2021-12-21 11:03:26 +05:30
liyasthomas
ba3d3430c0 chore(ui): improve ui consistency 2021-12-21 07:45:14 +05:30
liyasthomas
9c209bb009 Merge branch 'main' of https://github.com/hoppscotch/hoppscotch 2021-12-21 06:41:16 +05:30
liyasthomas
2c2bc141e8 fix: move search from header to footer - fixed #2033 2021-12-21 06:40:39 +05:30
Anwarul Islam
daa617f277 feat: Import Insomnia collection to Hoppscotch (#2031)
* feat: Import Insomnia collection to Hoppscotch

* feat: Import Insomnia collection to Hoppscotch
2021-12-20 21:38:55 +05:30
liyasthomas
4de953fe57 fix: input interfering menu shortcuts 2021-12-20 17:58:05 +05:30
liyasthomas
f94ee7c73f refactor: remove shortcut hints in mobile devices 2021-12-20 14:40:38 +05:30
liyasthomas
fac1288ef2 chore(deps): bump 2021-12-20 00:23:42 +05:30
liyasthomas
827f2157fa feat: quick shortcut key for menu items 2021-12-20 00:22:31 +05:30
liyasthomas
39d6b1bfeb chore: minor ui improvements 2021-12-18 19:51:57 +05:30
liyasthomas
76cbd99df8 feat: init backend for drag and drop requests on team collections - fixed #2005 2021-12-18 18:38:27 +05:30
liyasthomas
11fe908017 feat: init drag and drop requests on team collections 2021-12-18 17:10:20 +05:30
liyasthomas
d305168dc3 chore: updated types 2021-12-17 23:05:36 +05:30
Edwin Fajardo
f174086281 feat: (authentication) Api key based authentication [#2000] (#2021)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2021-12-17 21:32:29 +05:30
Andrew Bastin
156011f2cd feat: stop extension status polling when a status is resolved 2021-12-17 15:14:50 +05:30
Andrew Bastin
655e6dc2a4 refactor: move user agent checks to separate file 2021-12-17 15:10:06 +05:30
Andrew Bastin
2f50cff404 fix: extension status being inaccurate when settings page is reloaded 2021-12-17 14:52:30 +05:30
Andrew Bastin
a38c05febb feat: add utility composable for polling values 2021-12-17 14:51:18 +05:30
Andrew Bastin
65ee2ebcb2 refactor: update color-mode usage to new composable 2021-12-17 14:51:18 +05:30
Andrew Bastin
6e53adb5e7 refactor: rewrite settings page using setup scripts 2021-12-17 14:48:36 +05:30
Andrew Bastin
c7fd68b0d8 fix: better type checking for nuxt-color mode uses 2021-12-17 14:48:36 +05:30
Andrew Bastin
af35d68dfe fix: better volar type inference for template bindings on refs 2021-12-17 14:47:49 +05:30
Andrew Bastin
1a1baa715d feat: disable sentry if telemetry is off 2021-12-16 18:02:01 +05:30
liyasthomas
29b5913fac chore(deps): bump 2021-12-16 16:26:01 +05:30
liyasthomas
49c0069c68 feat: init sentry 2021-12-16 15:54:32 +05:30
liyasthomas
34fe94215c refactor: updated types 2021-12-16 15:15:54 +05:30
liyasthomas
8ca059cf4a fix: broken render due to regression in store 2021-12-16 01:41:36 +05:30
liyasthomas
daffc3fe0e refactor: minor ui improvements 2021-12-15 23:06:35 +05:30
Andrew Bastin
78ed95bcaa fix: lint error on json on empty string 2021-12-15 22:50:35 +05:30
Andrew Bastin
e4f40bce53 fix: empty string linter overflow issues 2021-12-15 22:37:03 +05:30
Andrew Bastin
0022efe844 fix: one of line setup error with json linting 2021-12-15 22:11:46 +05:30
liyasthomas
a2757a0335 refactor: minor ui improvements 2021-12-14 22:41:07 +05:30
liyasthomas
70555cd4a6 fix: add missing imports 2021-12-14 07:28:42 +05:30
Andrew Bastin
141697f9da fix: revamp broken network tests 2021-12-13 17:05:10 +05:30
Andrew Bastin
188c9ae5ca fix: remove debug statements 2021-12-13 16:58:09 +05:30
Andrew Bastin
71b4b6e366 refactor: network strategy rewrite 2021-12-13 16:58:09 +05:30
liyasthomas
870b493b80 refactor: remove absolute classes 2021-12-13 12:26:24 +05:30
liyasthomas
e31dc6e08d chore: updated keyboard shortcuts 2021-12-13 08:53:12 +05:30
liyasthomas
5a9d08a3f1 chore(deps): bump 2021-12-12 21:15:34 +05:30
liyasthomas
d997f88fbe Merge branch 'feat/cm-env-tooltip' 2021-12-12 21:11:58 +05:30
liyasthomas
4e0bb1a243 refactor: environment highlighing in editors - improve #1834 2021-12-12 21:10:05 +05:30
Andrew Bastin
fe5fe03b3c fix: reactivity issues 2021-12-12 20:36:49 +05:30
liyasthomas
534fe8030f fix: subscription streams 2021-12-12 06:19:58 +05:30
liyasthomas
67002a204e feat: highlight environment variables in codemirror 2021-12-11 22:19:44 +05:30
liyasthomas
8a7f3927b1 refactor: init environment highlighter 2021-12-11 07:26:18 +05:30
liyasthomas
50a57433d0 feat: init environment variables tooltip for codemirror 2021-12-10 09:27:25 +05:30
262 changed files with 14070 additions and 11153 deletions

26
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,26 @@
<!--
Thanks for creating this pull request 🤗
Please make sure that the pull request is limited to one type (docs, feature, etc.) and keep it as small as possible. You can open multiple prs instead of opening a huge one.
-->
<!-- If this pull request closes an issue, please mention the issue number below -->
Closes # <!-- Issue # here -->
### Description
<!-- Add a brief description of the pull request -->
<!-- You can also choose to add a list of changes and if they have been completed or not by using the markdown to-do list syntax
- [ ] Not Completed
- [x] Completed
-->
### Checks
<!-- Make sure your pull request passes the CI checks and do check the following fields as needed - -->
- [ ] My pull request adheres to the code style of this project
- [ ] My code requires changes to the documentation
- [ ] I have updated the documentation as required
- [ ] All the tests have passed
### Additional Information
<!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behavior, etc. -->

View File

@@ -1,833 +1,3 @@
# Changelog
## [v1.12.0](https://github.com/hoppscotch/hoppscotch/tree/v1.12.0) (2020-05-27)
[Full Changelog](https://github.com/hoppscotch/hoppscotch/compare/v1.10.0...v1.12.0)
## [v1.10.0](https://github.com/hoppscotch/hoppscotch/tree/v1.10.0) (2020-04-10)
[Full Changelog](https://github.com/hoppscotch/hoppscotch/compare/v1.9.9...v1.10.0)
## [v1.9.9](https://github.com/liyasthomas/postwoman/tree/v1.9.9) (2020-07-30)
[Full Changelog](https://github.com/liyasthomas/postwoman/compare/v1.9.7...v1.9.9)
**Fixed bugs:**
- TextDecoder.decode\(\) TypeError hangs the whole app [\#1032](https://github.com/liyasthomas/postwoman/issues/1032)
- response content doesn't fit to the text area when resizing [\#970](https://github.com/liyasthomas/postwoman/issues/970)
- typing into headers input fields [\#912](https://github.com/liyasthomas/postwoman/issues/912)
- Environment variable template \(\<\<foo\>\>\) appears urlencoded \(%3C%3Cfoo%3E%3E\) [\#896](https://github.com/liyasthomas/postwoman/issues/896)
- TypeError: Cannot read property 'startsWith' of undefined - after getting 401 response [\#894](https://github.com/liyasthomas/postwoman/issues/894)
- When deleting the header, the key is not updated [\#886](https://github.com/liyasthomas/postwoman/issues/886)
- Cannot introduce query parameters in URL for WebSocket [\#873](https://github.com/liyasthomas/postwoman/issues/873)
- Response content-type as `text/html` with content in `json` cause content area display empty [\#869](https://github.com/liyasthomas/postwoman/issues/869)
- Proxy privacy policy link [\#865](https://github.com/liyasthomas/postwoman/issues/865)
**Closed issues:**
- Collections | Request UI Issue [\#1028](https://github.com/liyasthomas/postwoman/issues/1028)
- JSON not showing up in the correct format [\#1023](https://github.com/liyasthomas/postwoman/issues/1023)
- ignore duplicates in history [\#1022](https://github.com/liyasthomas/postwoman/issues/1022)
- change history menu [\#1021](https://github.com/liyasthomas/postwoman/issues/1021)
- integrate parameters with history [\#1020](https://github.com/liyasthomas/postwoman/issues/1020)
- Why some Chrome do not have the ability to install PWA? [\#1015](https://github.com/liyasthomas/postwoman/issues/1015)
- Shall we have the team management ability and some public documents? [\#1014](https://github.com/liyasthomas/postwoman/issues/1014)
- I have edit this config, but it is not available to login. [\#1013](https://github.com/liyasthomas/postwoman/issues/1013)
- User login is disabled after i run it on our local server. [\#1012](https://github.com/liyasthomas/postwoman/issues/1012)
- postwoman google login doesn't work behind ingress or reverse proxy [\#1009](https://github.com/liyasthomas/postwoman/issues/1009)
- Compile error [\#1006](https://github.com/liyasthomas/postwoman/issues/1006)
- Postman Web is now out. It might be great to find a USP for Postwoman [\#1000](https://github.com/liyasthomas/postwoman/issues/1000)
- Saving response data in env variable [\#984](https://github.com/liyasthomas/postwoman/issues/984)
- contentType 无法使用 form-date 上传文件 [\#983](https://github.com/liyasthomas/postwoman/issues/983)
- Currently completely broken [\#980](https://github.com/liyasthomas/postwoman/issues/980)
- localhost request error [\#979](https://github.com/liyasthomas/postwoman/issues/979)
- Installing postwoman locally [\#969](https://github.com/liyasthomas/postwoman/issues/969)
- Do I install NodeJS for my online environment [\#968](https://github.com/liyasthomas/postwoman/issues/968)
- Collections and Environment Module [\#967](https://github.com/liyasthomas/postwoman/issues/967)
- Textarea display problem in super hi-dpi [\#965](https://github.com/liyasthomas/postwoman/issues/965)
- TypeError: Cannot read property 'value' of undefined - when logged in [\#961](https://github.com/liyasthomas/postwoman/issues/961)
- Enable user-select on websocket and other realtime message logs [\#951](https://github.com/liyasthomas/postwoman/issues/951)
- POST requet error [\#947](https://github.com/liyasthomas/postwoman/issues/947)
- Unable to fetch schema from localhost GraphQL server. [\#940](https://github.com/liyasthomas/postwoman/issues/940)
- Support downloading binary responses [\#929](https://github.com/liyasthomas/postwoman/issues/929)
- Integrate PostWoman In our Webapp [\#918](https://github.com/liyasthomas/postwoman/issues/918)
- proxy issue [\#911](https://github.com/liyasthomas/postwoman/issues/911)
- Button to cancel requests [\#909](https://github.com/liyasthomas/postwoman/issues/909)
- How to upload a file with a post request [\#908](https://github.com/liyasthomas/postwoman/issues/908)
- Cant Import Postman Global Environment Variables [\#907](https://github.com/liyasthomas/postwoman/issues/907)
- Postwoman Docker Container behind Reverse Proxy [\#906](https://github.com/liyasthomas/postwoman/issues/906)
- `pw.response` seems not to work [\#905](https://github.com/liyasthomas/postwoman/issues/905)
- Could postman add Sign in with LDAP server [\#901](https://github.com/liyasthomas/postwoman/issues/901)
- Collections & Environments not synced [\#900](https://github.com/liyasthomas/postwoman/issues/900)
- Add authentication to MQTT [\#898](https://github.com/liyasthomas/postwoman/issues/898)
- Labels are lost when using requests from collections [\#897](https://github.com/liyasthomas/postwoman/issues/897)
- Handle JSON Parameter list validation [\#891](https://github.com/liyasthomas/postwoman/issues/891)
- Nuxt fatal error [\#883](https://github.com/liyasthomas/postwoman/issues/883)
- Cannot connect my local websocket server [\#880](https://github.com/liyasthomas/postwoman/issues/880)
- Environments not synced after edit [\#877](https://github.com/liyasthomas/postwoman/issues/877)
- Can't find Desktop app link anywhere [\#872](https://github.com/liyasthomas/postwoman/issues/872)
- Show request completion time [\#871](https://github.com/liyasthomas/postwoman/issues/871)
- Make docs on self-hosting Postwoman [\#868](https://github.com/liyasthomas/postwoman/issues/868)
**Merged pull requests:**
- Add trailing backslash to generated cURL code for easier paste-and-execute [\#1033](https://github.com/liyasthomas/postwoman/pull/1033) ([ushuz](https://github.com/ushuz))
- Update zh-CN.json [\#1031](https://github.com/liyasthomas/postwoman/pull/1031) ([hantianwei](https://github.com/hantianwei))
- Bump firebase from 7.17.0 to 7.17.1 [\#1026](https://github.com/liyasthomas/postwoman/pull/1026) ([dependabot[bot]](https://github.com/apps/dependabot))
- Update zh-CN.json [\#1024](https://github.com/liyasthomas/postwoman/pull/1024) ([hantianwei](https://github.com/hantianwei))
- Bump @nuxtjs/gtm from 2.3.0 to 2.3.2 [\#1019](https://github.com/liyasthomas/postwoman/pull/1019) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump firebase from 7.16.1 to 7.17.0 [\#1018](https://github.com/liyasthomas/postwoman/pull/1018) ([dependabot[bot]](https://github.com/apps/dependabot))
- Fix bugs with the renderer mixins [\#1008](https://github.com/liyasthomas/postwoman/pull/1008) ([AndrewBastin](https://github.com/AndrewBastin))
- Bump eslint from 7.4.0 to 7.5.0 [\#1005](https://github.com/liyasthomas/postwoman/pull/1005) ([dependabot[bot]](https://github.com/apps/dependabot))
- Add Collections section in Docs page [\#1004](https://github.com/liyasthomas/postwoman/pull/1004) ([liyasthomas](https://github.com/liyasthomas))
- Bump lodash from 4.17.15 to 4.17.19 in /functions [\#999](https://github.com/liyasthomas/postwoman/pull/999) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump @nuxtjs/google-analytics from 2.3.0 to 2.4.0 [\#998](https://github.com/liyasthomas/postwoman/pull/998) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump firebase from 7.16.0 to 7.16.1 [\#997](https://github.com/liyasthomas/postwoman/pull/997) ([dependabot[bot]](https://github.com/apps/dependabot))
- Fixed broken network requests in GraphQL [\#995](https://github.com/liyasthomas/postwoman/pull/995) ([AndrewBastin](https://github.com/AndrewBastin))
- fix: replaceWithJSON used wrong commit name [\#993](https://github.com/liyasthomas/postwoman/pull/993) ([perseveringman](https://github.com/perseveringman))
- ⬆️ Bump @nuxtjs/toast from 3.3.0 to 3.3.1 [\#992](https://github.com/liyasthomas/postwoman/pull/992) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump start-server-and-test from 1.11.1 to 1.11.2 [\#991](https://github.com/liyasthomas/postwoman/pull/991) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump @nuxtjs/axios from 5.11.0 to 5.12.0 [\#990](https://github.com/liyasthomas/postwoman/pull/990) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump firebase from 7.15.5 to 7.16.0 [\#989](https://github.com/liyasthomas/postwoman/pull/989) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump start-server-and-test from 1.11.0 to 1.11.1 [\#988](https://github.com/liyasthomas/postwoman/pull/988) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump sass-loader from 9.0.1 to 9.0.2 [\#986](https://github.com/liyasthomas/postwoman/pull/986) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump cypress from 4.9.0 to 4.10.0 [\#985](https://github.com/liyasthomas/postwoman/pull/985) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump ace-builds from 1.4.11 to 1.4.12 [\#982](https://github.com/liyasthomas/postwoman/pull/982) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump vuefire from 2.2.2 to 2.2.3 [\#981](https://github.com/liyasthomas/postwoman/pull/981) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump eslint from 7.3.1 to 7.4.0 [\#978](https://github.com/liyasthomas/postwoman/pull/978) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump sass-loader from 9.0.0 to 9.0.1 [\#977](https://github.com/liyasthomas/postwoman/pull/977) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump graphql from 15.2.0 to 15.3.0 [\#976](https://github.com/liyasthomas/postwoman/pull/976) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump nuxt-i18n from 6.13.0 to 6.13.1 [\#975](https://github.com/liyasthomas/postwoman/pull/975) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump sass-loader from 8.0.2 to 9.0.0 [\#973](https://github.com/liyasthomas/postwoman/pull/973) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump nuxt-i18n from 6.12.2 to 6.13.0 [\#972](https://github.com/liyasthomas/postwoman/pull/972) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump graphql from 15.1.0 to 15.2.0 [\#966](https://github.com/liyasthomas/postwoman/pull/966) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump @nuxtjs/sitemap from 2.3.2 to 2.4.0 [\#963](https://github.com/liyasthomas/postwoman/pull/963) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump firebase from 7.15.4 to 7.15.5 [\#962](https://github.com/liyasthomas/postwoman/pull/962) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump eslint from 7.3.0 to 7.3.1 [\#958](https://github.com/liyasthomas/postwoman/pull/958) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump cypress from 4.8.0 to 4.9.0 [\#957](https://github.com/liyasthomas/postwoman/pull/957) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump firebase from 7.15.3 to 7.15.4 [\#956](https://github.com/liyasthomas/postwoman/pull/956) ([dependabot[bot]](https://github.com/apps/dependabot))
- Binary Responses & Response Lenses [\#955](https://github.com/liyasthomas/postwoman/pull/955) ([AndrewBastin](https://github.com/AndrewBastin))
- Improving SEO [\#954](https://github.com/liyasthomas/postwoman/pull/954) ([liyasthomas](https://github.com/liyasthomas))
- Isolate Netlify, Firebase and Helper functions + Import from absolute… [\#953](https://github.com/liyasthomas/postwoman/pull/953) ([liyasthomas](https://github.com/liyasthomas))
- Added ability to select text in realtime log [\#952](https://github.com/liyasthomas/postwoman/pull/952) ([AndrewBastin](https://github.com/AndrewBastin))
- ⬆️ Bump firebase from 7.15.1 to 7.15.3 [\#950](https://github.com/liyasthomas/postwoman/pull/950) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump eslint from 7.2.0 to 7.3.0 [\#949](https://github.com/liyasthomas/postwoman/pull/949) ([dependabot[bot]](https://github.com/apps/dependabot))
- Revert "⬆️ Bump nuxt from 2.12.2 to 2.13.0" [\#946](https://github.com/liyasthomas/postwoman/pull/946) ([liyasthomas](https://github.com/liyasthomas))
- ⬆️ Bump nuxt from 2.12.2 to 2.13.0 [\#942](https://github.com/liyasthomas/postwoman/pull/942) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump @nuxtjs/sitemap from 2.3.1 to 2.3.2 [\#939](https://github.com/liyasthomas/postwoman/pull/939) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump graphql from 14.6.0 to 15.1.0 [\#938](https://github.com/liyasthomas/postwoman/pull/938) ([dependabot[bot]](https://github.com/apps/dependabot))
- Updated readme [\#937](https://github.com/liyasthomas/postwoman/pull/937) ([liyasthomas](https://github.com/liyasthomas))
- ⬆️ Bump graphql-language-service-interface from 2.3.3 to 2.4.0 [\#936](https://github.com/liyasthomas/postwoman/pull/936) ([dependabot[bot]](https://github.com/apps/dependabot))
- ⬆️ Bump firebase from 7.15.0 to 7.15.1 [\#935](https://github.com/liyasthomas/postwoman/pull/935) ([dependabot[bot]](https://github.com/apps/dependabot))
- Transpiled ES5 code to ES6/ES7 [\#934](https://github.com/liyasthomas/postwoman/pull/934) ([liyasthomas](https://github.com/liyasthomas))
- Create Dependabot config file [\#932](https://github.com/liyasthomas/postwoman/pull/932) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Hide download response button for non-JSON responses [\#931](https://github.com/liyasthomas/postwoman/pull/931) ([AndrewBastin](https://github.com/AndrewBastin))
- chore\(deps-dev\): bump cypress from 4.7.0 to 4.8.0 [\#928](https://github.com/liyasthomas/postwoman/pull/928) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- fix: environment and collection sync issue with firebase [\#926](https://github.com/liyasthomas/postwoman/pull/926) ([myussufz](https://github.com/myussufz))
- chore\(deps\): bump @nuxtjs/axios from 5.10.3 to 5.11.0 [\#925](https://github.com/liyasthomas/postwoman/pull/925) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump eslint from 7.1.0 to 7.2.0 [\#924](https://github.com/liyasthomas/postwoman/pull/924) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- GraphQL response options only visible when a response is shown [\#923](https://github.com/liyasthomas/postwoman/pull/923) ([AndrewBastin](https://github.com/AndrewBastin))
- chore\(deps\): bump firebase from 7.14.6 to 7.15.0 [\#922](https://github.com/liyasthomas/postwoman/pull/922) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/sitemap from 2.3.0 to 2.3.1 [\#921](https://github.com/liyasthomas/postwoman/pull/921) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Added ability to download GraphQL responses [\#920](https://github.com/liyasthomas/postwoman/pull/920) ([AndrewBastin](https://github.com/AndrewBastin))
- chore\(deps\): bump nuxt-i18n from 6.12.1 to 6.12.2 [\#919](https://github.com/liyasthomas/postwoman/pull/919) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/gtm from 2.2.3 to 2.3.0 [\#917](https://github.com/liyasthomas/postwoman/pull/917) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Cancel Request from the Keyboard [\#916](https://github.com/liyasthomas/postwoman/pull/916) ([AndrewBastin](https://github.com/AndrewBastin))
- Cancellable Requests [\#915](https://github.com/liyasthomas/postwoman/pull/915) ([AndrewBastin](https://github.com/AndrewBastin))
- chore\(deps\): bump nuxt-i18n from 6.12.0 to 6.12.1 [\#914](https://github.com/liyasthomas/postwoman/pull/914) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.14.5 to 7.14.6 [\#913](https://github.com/liyasthomas/postwoman/pull/913) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
## [v1.9.7](https://github.com/liyasthomas/postwoman/tree/v1.9.7) (2020-05-12)
[Full Changelog](https://github.com/liyasthomas/postwoman/compare/v1.9.5...v1.9.7)
**Fixed bugs:**
- Empty header in headers list results in SyntaxError: Failed to execute 'setRequestHeader' [\#765](https://github.com/liyasthomas/postwoman/issues/765)
- Getting cannot read value of undefined [\#731](https://github.com/liyasthomas/postwoman/issues/731)
- Environment variables in collections [\#642](https://github.com/liyasthomas/postwoman/issues/642)
**Closed issues:**
- Import/Export collections from private github repos to share among teams. [\#867](https://github.com/liyasthomas/postwoman/issues/867)
- Unable to use postwoman with latest docker image from docker hub [\#866](https://github.com/liyasthomas/postwoman/issues/866)
- Access to nonexistent routes will not be redirect to page 404 [\#849](https://github.com/liyasthomas/postwoman/issues/849)
- Error: Network Error. Check console for details. [\#827](https://github.com/liyasthomas/postwoman/issues/827)
- 'Documentation Generated' response stacking past top of page if submit clicked enough times/fast enough [\#826](https://github.com/liyasthomas/postwoman/issues/826)
- The UI could use some improvements [\#825](https://github.com/liyasthomas/postwoman/issues/825)
- Postwoman won't build, produces 'FATAL Nuxt build error' [\#824](https://github.com/liyasthomas/postwoman/issues/824)
- Improve contrast of UI components in all themes [\#819](https://github.com/liyasthomas/postwoman/issues/819)
- Add an option to hide and/or collapse the right panel [\#818](https://github.com/liyasthomas/postwoman/issues/818)
- Docker Cannot log in normally in the container [\#817](https://github.com/liyasthomas/postwoman/issues/817)
- Body in Request [\#815](https://github.com/liyasthomas/postwoman/issues/815)
- How to run postwoman under reverse proxy [\#812](https://github.com/liyasthomas/postwoman/issues/812)
- Call local support [\#811](https://github.com/liyasthomas/postwoman/issues/811)
- feature [\#810](https://github.com/liyasthomas/postwoman/issues/810)
- support response json array [\#805](https://github.com/liyasthomas/postwoman/issues/805)
- socket binnery support [\#801](https://github.com/liyasthomas/postwoman/issues/801)
- Ability to join and leave rooms in Socket.IO connection [\#796](https://github.com/liyasthomas/postwoman/issues/796)
- How can I synchronize my data on local? [\#794](https://github.com/liyasthomas/postwoman/issues/794)
- I cant login [\#792](https://github.com/liyasthomas/postwoman/issues/792)
- Unresolved merge conflict in index.vue.orig [\#786](https://github.com/liyasthomas/postwoman/issues/786)
- You send data my request to Google [\#780](https://github.com/liyasthomas/postwoman/issues/780)
- I have question by \#750, it's closed,but my problem is still.... [\#770](https://github.com/liyasthomas/postwoman/issues/770)
- Add Format Body option [\#767](https://github.com/liyasthomas/postwoman/issues/767)
- Body scroll after modal is open [\#766](https://github.com/liyasthomas/postwoman/issues/766)
- text.match is not a function [\#764](https://github.com/liyasthomas/postwoman/issues/764)
- Request : Copy response headers [\#763](https://github.com/liyasthomas/postwoman/issues/763)
- The accordion \(expand\) labels are out of place on mobile [\#762](https://github.com/liyasthomas/postwoman/issues/762)
- why does the graphql case can't be saved? [\#761](https://github.com/liyasthomas/postwoman/issues/761)
- Mobile responsiveness issues [\#760](https://github.com/liyasthomas/postwoman/issues/760)
- Allow importing environment variables via Postman environment json files [\#759](https://github.com/liyasthomas/postwoman/issues/759)
- Report abuse: liyasthomas/postwoman \(Contact Links\) [\#754](https://github.com/liyasthomas/postwoman/issues/754)
- Report abuse: liyasthomas/postwoman \(Contact Links\) [\#753](https://github.com/liyasthomas/postwoman/issues/753)
- Request headers kept same after changing request type [\#752](https://github.com/liyasthomas/postwoman/issues/752)
- I used it to post test,but response network error [\#750](https://github.com/liyasthomas/postwoman/issues/750)
- Add compatibility for postman collections & environments [\#746](https://github.com/liyasthomas/postwoman/issues/746)
- Improve documentation on how to use environments [\#742](https://github.com/liyasthomas/postwoman/issues/742)
- Add GraphQL syntax highlighting [\#741](https://github.com/liyasthomas/postwoman/issues/741)
- Broken link to translations branch [\#737](https://github.com/liyasthomas/postwoman/issues/737)
- Add docker Images for all Tags [\#722](https://github.com/liyasthomas/postwoman/issues/722)
- Provide post-build resources and default setting files [\#714](https://github.com/liyasthomas/postwoman/issues/714)
- Theme lacks of contrast [\#709](https://github.com/liyasthomas/postwoman/issues/709)
- Postwoman raiase a connection error when communicate with localhost. [\#708](https://github.com/liyasthomas/postwoman/issues/708)
- CORS issue when hosting [\#707](https://github.com/liyasthomas/postwoman/issues/707)
- Add a description to the request or collection when saving it [\#706](https://github.com/liyasthomas/postwoman/issues/706)
- Error: Network Error. Check console for details [\#704](https://github.com/liyasthomas/postwoman/issues/704)
- Save collections on account sync turn on [\#679](https://github.com/liyasthomas/postwoman/issues/679)
- Tests should be saved together with requests [\#643](https://github.com/liyasthomas/postwoman/issues/643)
- npm modules of postwoman's vue components [\#384](https://github.com/liyasthomas/postwoman/issues/384)
- \[request\] VS code extension [\#313](https://github.com/liyasthomas/postwoman/issues/313)
- remove prerequest \<\< \>\> bindings when pre-request script is toggled off [\#234](https://github.com/liyasthomas/postwoman/issues/234)
- Dynamic Headers [\#91](https://github.com/liyasthomas/postwoman/issues/91)
**Merged pull requests:**
- chore\(deps\): bump @nuxtjs/sitemap from 2.2.1 to 2.3.0 [\#864](https://github.com/liyasthomas/postwoman/pull/864) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- docs: add sboulema as a contributor [\#863](https://github.com/liyasthomas/postwoman/pull/863) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- Allow importing environment variables via Postman environment json files [\#862](https://github.com/liyasthomas/postwoman/pull/862) ([sboulema](https://github.com/sboulema))
- chore\(deps\): bump nuxt-i18n from 6.11.0 to 6.11.1 [\#861](https://github.com/liyasthomas/postwoman/pull/861) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Produce valid output when showing/copying as code [\#857](https://github.com/liyasthomas/postwoman/pull/857) ([Hydrophobefireman](https://github.com/Hydrophobefireman))
- dotenv [\#856](https://github.com/liyasthomas/postwoman/pull/856) ([liyasthomas](https://github.com/liyasthomas))
- Save Collections/Environments on enabling sync [\#854](https://github.com/liyasthomas/postwoman/pull/854) ([sboulema](https://github.com/sboulema))
- chore\(deps\): bump firebase from 7.14.2 to 7.14.3 [\#853](https://github.com/liyasthomas/postwoman/pull/853) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Environment variables in collections [\#851](https://github.com/liyasthomas/postwoman/pull/851) ([sboulema](https://github.com/sboulema))
- Add format body option [\#847](https://github.com/liyasthomas/postwoman/pull/847) ([sboulema](https://github.com/sboulema))
- Save GraphQL Docs [\#846](https://github.com/liyasthomas/postwoman/pull/846) ([AndrewBastin](https://github.com/AndrewBastin))
- chore\(deps-dev\): bump node-sass from 4.14.0 to 4.14.1 [\#844](https://github.com/liyasthomas/postwoman/pull/844) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Remove not-deleted index.vue merge file [\#842](https://github.com/liyasthomas/postwoman/pull/842) ([AndrewBastin](https://github.com/AndrewBastin))
- URL Path Parameters \#834 [\#840](https://github.com/liyasthomas/postwoman/pull/840) ([sboulema](https://github.com/sboulema))
- chore\(deps\): remove stale dependency [\#839](https://github.com/liyasthomas/postwoman/pull/839) ([jamesgeorge007](https://github.com/jamesgeorge007))
- GraphQL Query Editor Syntax Highlighting [\#838](https://github.com/liyasthomas/postwoman/pull/838) ([AndrewBastin](https://github.com/AndrewBastin))
- chore\(deps-dev\): bump lint-staged from 10.2.1 to 10.2.2 [\#837](https://github.com/liyasthomas/postwoman/pull/837) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(store\): better code structure [\#835](https://github.com/liyasthomas/postwoman/pull/835) ([jameslahm](https://github.com/jameslahm))
- chore\(deps\): bump @nuxtjs/axios from 5.10.2 to 5.10.3 [\#832](https://github.com/liyasthomas/postwoman/pull/832) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt-i18n from 6.10.1 to 6.11.0 [\#831](https://github.com/liyasthomas/postwoman/pull/831) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump lint-staged from 10.2.0 to 10.2.1 [\#830](https://github.com/liyasthomas/postwoman/pull/830) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Add ability to navigate through message history [\#828](https://github.com/liyasthomas/postwoman/pull/828) ([jinyus](https://github.com/jinyus))
- chore\(config\): delete render option [\#823](https://github.com/liyasthomas/postwoman/pull/823) ([jameslahm](https://github.com/jameslahm))
- chore\(deps-dev\): bump cypress from 4.4.1 to 4.5.0 [\#822](https://github.com/liyasthomas/postwoman/pull/822) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump lint-staged from 10.1.7 to 10.2.0 [\#821](https://github.com/liyasthomas/postwoman/pull/821) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Realtime SocketIO support for json user input [\#820](https://github.com/liyasthomas/postwoman/pull/820) ([feydan](https://github.com/feydan))
- chore\(deps\): bump @nuxtjs/axios from 5.10.1 to 5.10.2 [\#816](https://github.com/liyasthomas/postwoman/pull/816) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Modify responseType by Object\(json\) or Array\(json5\) [\#813](https://github.com/liyasthomas/postwoman/pull/813) ([shtakai](https://github.com/shtakai))
- chore\(deps\): bump nuxt-i18n from 6.9.2 to 6.10.1 [\#809](https://github.com/liyasthomas/postwoman/pull/809) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.14.1 to 7.14.2 [\#808](https://github.com/liyasthomas/postwoman/pull/808) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump node-sass from 4.13.1 to 4.14.0 [\#807](https://github.com/liyasthomas/postwoman/pull/807) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/sitemap from 2.2.0 to 2.2.1 [\#806](https://github.com/liyasthomas/postwoman/pull/806) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/axios from 5.10.0 to 5.10.1 [\#803](https://github.com/liyasthomas/postwoman/pull/803) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt-i18n from 6.9.1 to 6.9.2 [\#802](https://github.com/liyasthomas/postwoman/pull/802) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump lint-staged from 10.1.6 to 10.1.7 [\#800](https://github.com/liyasthomas/postwoman/pull/800) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump prettier from 2.0.4 to 2.0.5 [\#799](https://github.com/liyasthomas/postwoman/pull/799) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/axios from 5.9.7 to 5.10.0 [\#798](https://github.com/liyasthomas/postwoman/pull/798) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump cypress from 4.4.0 to 4.4.1 [\#797](https://github.com/liyasthomas/postwoman/pull/797) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Listen to all events in socket.io connection [\#795](https://github.com/liyasthomas/postwoman/pull/795) ([konradkalemba](https://github.com/konradkalemba))
- Fix postman import with empty url [\#791](https://github.com/liyasthomas/postwoman/pull/791) ([Nikita240](https://github.com/Nikita240))
- chore\(deps-dev\): bump lint-staged from 10.1.5 to 10.1.6 [\#789](https://github.com/liyasthomas/postwoman/pull/789) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump lint-staged from 10.1.3 to 10.1.5 [\#787](https://github.com/liyasthomas/postwoman/pull/787) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.14.0 to 7.14.1 [\#782](https://github.com/liyasthomas/postwoman/pull/782) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump yargs-parser from 18.1.2 to 18.1.3 [\#781](https://github.com/liyasthomas/postwoman/pull/781) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump start-server-and-test from 1.10.11 to 1.11.0 [\#778](https://github.com/liyasthomas/postwoman/pull/778) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt-i18n from 6.8.1 to 6.9.1 [\#776](https://github.com/liyasthomas/postwoman/pull/776) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump ace-builds from 1.4.9 to 1.4.11 [\#775](https://github.com/liyasthomas/postwoman/pull/775) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump cypress from 4.3.0 to 4.4.0 [\#774](https://github.com/liyasthomas/postwoman/pull/774) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.13.2 to 7.14.0 [\#758](https://github.com/liyasthomas/postwoman/pull/758) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump husky from 4.2.3 to 4.2.5 [\#757](https://github.com/liyasthomas/postwoman/pull/757) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump lint-staged from 10.1.2 to 10.1.3 [\#756](https://github.com/liyasthomas/postwoman/pull/756) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Fix indicator if extension is installed [\#748](https://github.com/liyasthomas/postwoman/pull/748) ([levrik](https://github.com/levrik))
- Remove support for legacy extensions [\#747](https://github.com/liyasthomas/postwoman/pull/747) ([levrik](https://github.com/levrik))
- Fix GQL introspection query not sent through extension [\#745](https://github.com/liyasthomas/postwoman/pull/745) ([levrik](https://github.com/levrik))
- chore\(deps-dev\): bump prettier from 2.0.2 to 2.0.4 [\#744](https://github.com/liyasthomas/postwoman/pull/744) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/sitemap from 2.1.0 to 2.2.0 [\#743](https://github.com/liyasthomas/postwoman/pull/743) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump lint-staged from 10.1.1 to 10.1.2 [\#740](https://github.com/liyasthomas/postwoman/pull/740) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt-i18n from 6.8.0 to 6.8.1 [\#736](https://github.com/liyasthomas/postwoman/pull/736) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump ace-builds from 1.4.8 to 1.4.9 [\#735](https://github.com/liyasthomas/postwoman/pull/735) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.13.1 to 7.13.2 [\#734](https://github.com/liyasthomas/postwoman/pull/734) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump vue-virtual-scroll-list from 1.4.6 to 1.4.7 [\#733](https://github.com/liyasthomas/postwoman/pull/733) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt-i18n from 6.7.2 to 6.8.0 [\#732](https://github.com/liyasthomas/postwoman/pull/732) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt from 2.12.1 to 2.12.2 [\#729](https://github.com/liyasthomas/postwoman/pull/729) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt-i18n from 6.7.1 to 6.7.2 [\#728](https://github.com/liyasthomas/postwoman/pull/728) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump lint-staged from 10.1.0 to 10.1.1 [\#727](https://github.com/liyasthomas/postwoman/pull/727) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump cypress from 4.2.0 to 4.3.0 [\#726](https://github.com/liyasthomas/postwoman/pull/726) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump lint-staged from 10.0.10 to 10.1.0 [\#725](https://github.com/liyasthomas/postwoman/pull/725) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt-i18n from 6.7.0 to 6.7.1 [\#724](https://github.com/liyasthomas/postwoman/pull/724) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/axios from 5.9.6 to 5.9.7 [\#723](https://github.com/liyasthomas/postwoman/pull/723) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump lint-staged from 10.0.9 to 10.0.10 [\#721](https://github.com/liyasthomas/postwoman/pull/721) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/axios from 5.9.5 to 5.9.6 [\#719](https://github.com/liyasthomas/postwoman/pull/719) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/sitemap from 2.0.1 to 2.1.0 [\#718](https://github.com/liyasthomas/postwoman/pull/718) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.13.0 to 7.13.1 [\#717](https://github.com/liyasthomas/postwoman/pull/717) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump yargs-parser from 18.1.1 to 18.1.2 [\#713](https://github.com/liyasthomas/postwoman/pull/713) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt-i18n from 6.6.1 to 6.7.0 [\#712](https://github.com/liyasthomas/postwoman/pull/712) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt from 2.12.0 to 2.12.1 [\#711](https://github.com/liyasthomas/postwoman/pull/711) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.12.0 to 7.13.0 [\#710](https://github.com/liyasthomas/postwoman/pull/710) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Updating the UI and style files [\#705](https://github.com/liyasthomas/postwoman/pull/705) ([liyasthomas](https://github.com/liyasthomas))
- chore\(deps-dev\): bump lint-staged from 10.0.8 to 10.0.9 [\#703](https://github.com/liyasthomas/postwoman/pull/703) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Improving performance [\#702](https://github.com/liyasthomas/postwoman/pull/702) ([liyasthomas](https://github.com/liyasthomas))
- :package: Updating packages [\#701](https://github.com/liyasthomas/postwoman/pull/701) ([liyasthomas](https://github.com/liyasthomas))
- chore\(deps-dev\): bump prettier from 2.0.1 to 2.0.2 [\#700](https://github.com/liyasthomas/postwoman/pull/700) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump prettier from 1.19.1 to 2.0.1 [\#697](https://github.com/liyasthomas/postwoman/pull/697) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
## [v1.9.5](https://github.com/liyasthomas/postwoman/tree/v1.9.5) (2020-03-22)
[Full Changelog](https://github.com/liyasthomas/postwoman/compare/v1.9.0...v1.9.5)
**Fixed bugs:**
- Test script is not run after failing request [\#644](https://github.com/liyasthomas/postwoman/issues/644)
- \[HELP\] Auth permission denied [\#621](https://github.com/liyasthomas/postwoman/issues/621)
- Can't login on Brave Browser [\#607](https://github.com/liyasthomas/postwoman/issues/607)
**Closed issues:**
- \[UI/UX\] - Change place of Send button [\#696](https://github.com/liyasthomas/postwoman/issues/696)
- Support preview of JSON:API's "application/vnd.api+json" Content-Type [\#694](https://github.com/liyasthomas/postwoman/issues/694)
- Report Portal integration [\#691](https://github.com/liyasthomas/postwoman/issues/691)
- Docs request: how to prevent secrets from leaving local storage wrt. sync. [\#686](https://github.com/liyasthomas/postwoman/issues/686)
- \[bug\] - Can't make a request to HTTP [\#676](https://github.com/liyasthomas/postwoman/issues/676)
- Looking forward to that the postwoman Compatible 'swagger ' at next version [\#675](https://github.com/liyasthomas/postwoman/issues/675)
- Error: Network Error. Check console for details. [\#673](https://github.com/liyasthomas/postwoman/issues/673)
- \[Bug\] - Can't login to Github and Google [\#661](https://github.com/liyasthomas/postwoman/issues/661)
- A question that has been raised but not resolved [\#658](https://github.com/liyasthomas/postwoman/issues/658)
- An unknown error occurred whilst the proxy was processing your request. [\#656](https://github.com/liyasthomas/postwoman/issues/656)
- Running app from downloaded zip fails to compile [\#651](https://github.com/liyasthomas/postwoman/issues/651)
- Environment variable in path won't update [\#641](https://github.com/liyasthomas/postwoman/issues/641)
- Info: The current domain is not authorized for OAuth operations Error [\#637](https://github.com/liyasthomas/postwoman/issues/637)
- A suggestion for UI [\#635](https://github.com/liyasthomas/postwoman/issues/635)
- How to use postwoman for local development and testing [\#634](https://github.com/liyasthomas/postwoman/issues/634)
- How to debug localhost \(cors\) [\#630](https://github.com/liyasthomas/postwoman/issues/630)
- Support SocketIO connections on Realtime page [\#611](https://github.com/liyasthomas/postwoman/issues/611)
- Requests to local API returning error response [\#608](https://github.com/liyasthomas/postwoman/issues/608)
- Why does the URL input field display only one line [\#604](https://github.com/liyasthomas/postwoman/issues/604)
- Parameter list not showing JSON object fields \(force raw?\) [\#597](https://github.com/liyasthomas/postwoman/issues/597)
- Add setting to disable scroll animations [\#592](https://github.com/liyasthomas/postwoman/issues/592)
- Bigger URL and/or Path input field [\#581](https://github.com/liyasthomas/postwoman/issues/581)
- Ability to connect to a MQTT broker [\#342](https://github.com/liyasthomas/postwoman/issues/342)
- \[request\] Offline cross-platform native build [\#267](https://github.com/liyasthomas/postwoman/issues/267)
- On Save Update existing API [\#204](https://github.com/liyasthomas/postwoman/issues/204)
- Import and export environments from JSON [\#190](https://github.com/liyasthomas/postwoman/issues/190)
- Fast URL entry [\#62](https://github.com/liyasthomas/postwoman/issues/62)
**Merged pull requests:**
- Add application/vnd.api+json [\#695](https://github.com/liyasthomas/postwoman/pull/695) ([allthesignals](https://github.com/allthesignals))
- Fix raw input \(JSON\) [\#693](https://github.com/liyasthomas/postwoman/pull/693) ([leomp12](https://github.com/leomp12))
- chore\(deps\): bump firebase from 7.11.0 to 7.12.0 [\#689](https://github.com/liyasthomas/postwoman/pull/689) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump vuefire from 2.2.1 to 2.2.2 [\#688](https://github.com/liyasthomas/postwoman/pull/688) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump cypress from 4.1.0 to 4.2.0 [\#685](https://github.com/liyasthomas/postwoman/pull/685) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump start-server-and-test from 1.10.10 to 1.10.11 [\#684](https://github.com/liyasthomas/postwoman/pull/684) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt-i18n from 6.6.0 to 6.6.1 [\#683](https://github.com/liyasthomas/postwoman/pull/683) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt from 2.11.0 to 2.12.0 [\#682](https://github.com/liyasthomas/postwoman/pull/682) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Fix setting default raw params [\#681](https://github.com/liyasthomas/postwoman/pull/681) ([leomp12](https://github.com/leomp12))
- Fix handling content type and raw input [\#678](https://github.com/liyasthomas/postwoman/pull/678) ([leomp12](https://github.com/leomp12))
- \[Snyk\] Security upgrade yargs-parser from 18.1.0 to 18.1.1 [\#674](https://github.com/liyasthomas/postwoman/pull/674) ([snyk-bot](https://github.com/snyk-bot))
- chore\(deps-dev\): bump start-server-and-test from 1.10.9 to 1.10.10 [\#672](https://github.com/liyasthomas/postwoman/pull/672) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.10.0 to 7.11.0 [\#671](https://github.com/liyasthomas/postwoman/pull/671) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ✅ Updating tests [\#669](https://github.com/liyasthomas/postwoman/pull/669) ([liyasthomas](https://github.com/liyasthomas))
- Updating tests [\#668](https://github.com/liyasthomas/postwoman/pull/668) ([liyasthomas](https://github.com/liyasthomas))
- APIs [\#667](https://github.com/liyasthomas/postwoman/pull/667) ([liyasthomas](https://github.com/liyasthomas))
- Insecure Websocket connection issue while connecting to MQTT broker. [\#666](https://github.com/liyasthomas/postwoman/pull/666) ([rahulnpadalkar](https://github.com/rahulnpadalkar))
- Improving performance [\#664](https://github.com/liyasthomas/postwoman/pull/664) ([liyasthomas](https://github.com/liyasthomas))
- Feature/mqtt [\#663](https://github.com/liyasthomas/postwoman/pull/663) ([liyasthomas](https://github.com/liyasthomas))
- Added Support for MQTT [\#662](https://github.com/liyasthomas/postwoman/pull/662) ([rahulnpadalkar](https://github.com/rahulnpadalkar))
- chore\(deps\): bump yargs-parser from 18.0.0 to 18.1.0 [\#660](https://github.com/liyasthomas/postwoman/pull/660) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump start-server-and-test from 1.10.8 to 1.10.9 [\#659](https://github.com/liyasthomas/postwoman/pull/659) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Added icon slot to tabs [\#657](https://github.com/liyasthomas/postwoman/pull/657) ([liyasthomas](https://github.com/liyasthomas))
- Refactor/ui [\#655](https://github.com/liyasthomas/postwoman/pull/655) ([liyasthomas](https://github.com/liyasthomas))
- even [\#654](https://github.com/liyasthomas/postwoman/pull/654) ([liyasthomas](https://github.com/liyasthomas))
- chore\(deps\): bump yargs-parser from 17.0.0 to 18.0.0 [\#653](https://github.com/liyasthomas/postwoman/pull/653) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.9.3 to 7.10.0 [\#652](https://github.com/liyasthomas/postwoman/pull/652) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Added the ability to prettify GraphQL queries [\#650](https://github.com/liyasthomas/postwoman/pull/650) ([AndrewBastin](https://github.com/AndrewBastin))
- Add http/https support to socketio url valid [\#648](https://github.com/liyasthomas/postwoman/pull/648) ([moonrailgun](https://github.com/moonrailgun))
- Refactor/ui [\#647](https://github.com/liyasthomas/postwoman/pull/647) ([liyasthomas](https://github.com/liyasthomas))
- Even [\#646](https://github.com/liyasthomas/postwoman/pull/646) ([liyasthomas](https://github.com/liyasthomas))
- Run tests even after failed request [\#645](https://github.com/liyasthomas/postwoman/pull/645) ([liyasthomas](https://github.com/liyasthomas))
- Feature: add socket io support [\#640](https://github.com/liyasthomas/postwoman/pull/640) ([moonrailgun](https://github.com/moonrailgun))
- Removed linting for the collection docs import editor [\#639](https://github.com/liyasthomas/postwoman/pull/639) ([AndrewBastin](https://github.com/AndrewBastin))
- Moving or renaming files [\#638](https://github.com/liyasthomas/postwoman/pull/638) ([liyasthomas](https://github.com/liyasthomas))
- Refactor/ui [\#636](https://github.com/liyasthomas/postwoman/pull/636) ([liyasthomas](https://github.com/liyasthomas))
- Updated messages for when GraphQL Get Schema fails [\#633](https://github.com/liyasthomas/postwoman/pull/633) ([AndrewBastin](https://github.com/AndrewBastin))
- Minor GraphQL page improvements [\#631](https://github.com/liyasthomas/postwoman/pull/631) ([dmitryyankowski](https://github.com/dmitryyankowski))
- Ignore empty GQL Variable Strings [\#629](https://github.com/liyasthomas/postwoman/pull/629) ([AndrewBastin](https://github.com/AndrewBastin))
- chore\(deps-dev\): bump cypress from 4.0.2 to 4.1.0 [\#628](https://github.com/liyasthomas/postwoman/pull/628) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump nuxt-i18n from 6.5.0 to 6.6.0 [\#627](https://github.com/liyasthomas/postwoman/pull/627) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.9.1 to 7.9.3 [\#626](https://github.com/liyasthomas/postwoman/pull/626) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/google-tag-manager from 2.3.1 to 2.3.2 [\#625](https://github.com/liyasthomas/postwoman/pull/625) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- test: purge travis [\#623](https://github.com/liyasthomas/postwoman/pull/623) ([yubathom](https://github.com/yubathom))
- Added shortcut to quickly run the GraphQL query [\#620](https://github.com/liyasthomas/postwoman/pull/620) ([AndrewBastin](https://github.com/AndrewBastin))
- docs: add dmitryyankowski as a contributor [\#619](https://github.com/liyasthomas/postwoman/pull/619) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- Link multiple auth providers [\#618](https://github.com/liyasthomas/postwoman/pull/618) ([liyasthomas](https://github.com/liyasthomas))
- Add --staged parameter to pretty-quick pre-commit [\#617](https://github.com/liyasthomas/postwoman/pull/617) ([dmitryyankowski](https://github.com/dmitryyankowski))
- :bug: FIxed URI not updating on Clear content, minor formData improve… [\#612](https://github.com/liyasthomas/postwoman/pull/612) ([liyasthomas](https://github.com/liyasthomas))
- Update proxy information. [\#610](https://github.com/liyasthomas/postwoman/pull/610) ([NBTX](https://github.com/NBTX))
- Fixed install extension toast appearing even when extension is installed [\#609](https://github.com/liyasthomas/postwoman/pull/609) ([AndrewBastin](https://github.com/AndrewBastin))
- chore\(deps-dev\): bump lint-staged from 10.0.7 to 10.0.8 [\#606](https://github.com/liyasthomas/postwoman/pull/606) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- JSON linting in the code editor [\#605](https://github.com/liyasthomas/postwoman/pull/605) ([AndrewBastin](https://github.com/AndrewBastin))
- Added regex to handle url parts [\#603](https://github.com/liyasthomas/postwoman/pull/603) ([JacobAnavisca](https://github.com/JacobAnavisca))
- GraphQL page improvements, and more [\#602](https://github.com/liyasthomas/postwoman/pull/602) ([dmitryyankowski](https://github.com/dmitryyankowski))
- I18n [\#601](https://github.com/liyasthomas/postwoman/pull/601) ([liyasthomas](https://github.com/liyasthomas))
- feat\(i18n\): add Korean [\#600](https://github.com/liyasthomas/postwoman/pull/600) ([9j](https://github.com/9j))
- Improve page load/unload experience \(remove FOUCs\) [\#599](https://github.com/liyasthomas/postwoman/pull/599) ([NBTX](https://github.com/NBTX))
- Feature: Add prettier/pretty-quick formatting w/ Husky pre-commit [\#596](https://github.com/liyasthomas/postwoman/pull/596) ([dmitryyankowski](https://github.com/dmitryyankowski))
## [v1.9.0](https://github.com/liyasthomas/postwoman/tree/v1.9.0) (2020-02-24)
[Full Changelog](https://github.com/liyasthomas/postwoman/compare/v1.8.0...v1.9.0)
**Fixed bugs:**
- Auto Theme Selection is kinda difficult to see [\#563](https://github.com/liyasthomas/postwoman/issues/563)
- Can't send request to localhost via Chrome extention [\#560](https://github.com/liyasthomas/postwoman/issues/560)
- Validation for duplicate collection ignores letter case [\#547](https://github.com/liyasthomas/postwoman/issues/547)
- Build failed [\#327](https://github.com/liyasthomas/postwoman/issues/327)
- Fixed typo in translation file for Auto theme [\#556](https://github.com/liyasthomas/postwoman/pull/556) ([AndrewBastin](https://github.com/AndrewBastin))
**Closed issues:**
- don't run [\#577](https://github.com/liyasthomas/postwoman/issues/577)
- Get correct response data but occurs with error "Cannot read property 'value' of undefined" [\#575](https://github.com/liyasthomas/postwoman/issues/575)
- firebase_app\_\_WEBPACK_IMPORTED_MODULE_2\_\_\_default.a.firestore is not a function [\#558](https://github.com/liyasthomas/postwoman/issues/558)
- Disable SSL cert for websockets [\#557](https://github.com/liyasthomas/postwoman/issues/557)
- relative module not found during start [\#552](https://github.com/liyasthomas/postwoman/issues/552)
- Feature Request: Subfolders [\#540](https://github.com/liyasthomas/postwoman/issues/540)
- Feature request: Keyboard shortcuts for folder creation [\#539](https://github.com/liyasthomas/postwoman/issues/539)
- Add max-height and overflow: auto to "parameter list" textarea [\#532](https://github.com/liyasthomas/postwoman/issues/532)
- Friendly minded GraphQL [\#468](https://github.com/liyasthomas/postwoman/issues/468)
- multipart/form-data support [\#434](https://github.com/liyasthomas/postwoman/issues/434)
- IE Support [\#386](https://github.com/liyasthomas/postwoman/issues/386)
- ⏬ Import a Postman's Collection [\#333](https://github.com/liyasthomas/postwoman/issues/333)
- Implement pre-request and post-request scripts \(and request chaining\) [\#218](https://github.com/liyasthomas/postwoman/issues/218)
- Environment management and configuration [\#147](https://github.com/liyasthomas/postwoman/issues/147)
**Merged pull requests:**
- POST request body editor reacts to the content type [\#594](https://github.com/liyasthomas/postwoman/pull/594) ([AndrewBastin](https://github.com/AndrewBastin))
- Fix variablesJSONString store default for GraphQL page [\#593](https://github.com/liyasthomas/postwoman/pull/593) ([dmitryyankowski](https://github.com/dmitryyankowski))
- Environment Mangement [\#591](https://github.com/liyasthomas/postwoman/pull/591) ([JacobAnavisca](https://github.com/JacobAnavisca))
- GraphQL Query Autocompletion [\#590](https://github.com/liyasthomas/postwoman/pull/590) ([AndrewBastin](https://github.com/AndrewBastin))
- Refactor/lint [\#589](https://github.com/liyasthomas/postwoman/pull/589) ([liyasthomas](https://github.com/liyasthomas))
- Even [\#588](https://github.com/liyasthomas/postwoman/pull/588) ([liyasthomas](https://github.com/liyasthomas))
- chore\(deps\): bump firebase from 7.9.0 to 7.9.1 [\#587](https://github.com/liyasthomas/postwoman/pull/587) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Even [\#586](https://github.com/liyasthomas/postwoman/pull/586) ([liyasthomas](https://github.com/liyasthomas))
- chore\(deps\): bump firebase from 7.8.2 to 7.9.0 [\#585](https://github.com/liyasthomas/postwoman/pull/585) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump vue-virtual-scroll-list from 1.4.5 to 1.4.6 [\#584](https://github.com/liyasthomas/postwoman/pull/584) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Adapt extension check to new extensions [\#583](https://github.com/liyasthomas/postwoman/pull/583) ([levrik](https://github.com/levrik))
- Update link to extension repo in README [\#582](https://github.com/liyasthomas/postwoman/pull/582) ([levrik](https://github.com/levrik))
- Even [\#579](https://github.com/liyasthomas/postwoman/pull/579) ([liyasthomas](https://github.com/liyasthomas))
- Refactor/lint [\#578](https://github.com/liyasthomas/postwoman/pull/578) ([liyasthomas](https://github.com/liyasthomas))
- chore\(deps\): bump vue-virtual-scroll-list from 1.4.4 to 1.4.5 [\#576](https://github.com/liyasthomas/postwoman/pull/576) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Postman collection parsing [\#574](https://github.com/liyasthomas/postwoman/pull/574) ([JacobAnavisca](https://github.com/JacobAnavisca))
- Unify Chrome and Firefox extensions [\#573](https://github.com/liyasthomas/postwoman/pull/573) ([levrik](https://github.com/levrik))
- fix: drop the toast which doesn't show up [\#572](https://github.com/liyasthomas/postwoman/pull/572) ([jamesgeorge007](https://github.com/jamesgeorge007))
- :sparkles: Native share + updated meta description [\#571](https://github.com/liyasthomas/postwoman/pull/571) ([liyasthomas](https://github.com/liyasthomas))
- chore\(deps\): bump firebase from 7.8.1 to 7.8.2 [\#570](https://github.com/liyasthomas/postwoman/pull/570) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump cypress from 4.0.1 to 4.0.2 [\#569](https://github.com/liyasthomas/postwoman/pull/569) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Added create collection and save request syncs [\#568](https://github.com/liyasthomas/postwoman/pull/568) ([JacobAnavisca](https://github.com/JacobAnavisca))
- Moved common headers to a separate file [\#566](https://github.com/liyasthomas/postwoman/pull/566) ([AndrewBastin](https://github.com/AndrewBastin))
- chore\(deps-dev\): bump cypress from 4.0.0 to 4.0.1 [\#565](https://github.com/liyasthomas/postwoman/pull/565) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump yargs-parser from 16.1.0 to 17.0.0 [\#564](https://github.com/liyasthomas/postwoman/pull/564) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump cypress from 3.8.3 to 4.0.0 [\#562](https://github.com/liyasthomas/postwoman/pull/562) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump firebase from 7.8.0 to 7.8.1 [\#561](https://github.com/liyasthomas/postwoman/pull/561) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore: use typeof as an operator and make use of localizable strings [\#559](https://github.com/liyasthomas/postwoman/pull/559) ([jamesgeorge007](https://github.com/jamesgeorge007))
- Support for Formdata [\#555](https://github.com/liyasthomas/postwoman/pull/555) ([liyasthomas](https://github.com/liyasthomas))
- chore\(deps\): bump @nuxtjs/pwa from 3.0.0-beta.19 to 3.0.0-beta.20 [\#554](https://github.com/liyasthomas/postwoman/pull/554) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/axios from 5.9.4 to 5.9.5 [\#553](https://github.com/liyasthomas/postwoman/pull/553) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Added toggle to decide whether extensions should be used [\#551](https://github.com/liyasthomas/postwoman/pull/551) ([AndrewBastin](https://github.com/AndrewBastin))
- Show Ctrl instead of Command for shortcuts non-Apple platforms [\#549](https://github.com/liyasthomas/postwoman/pull/549) ([AndrewBastin](https://github.com/AndrewBastin))
- fix\(chore\): Take letter casing into account while checking for duplicate collection [\#548](https://github.com/liyasthomas/postwoman/pull/548) ([jamesgeorge007](https://github.com/jamesgeorge007))
- update e2e tests [\#546](https://github.com/liyasthomas/postwoman/pull/546) ([yubathom](https://github.com/yubathom))
- chore\(deps\): bump @nuxtjs/axios from 5.9.3 to 5.9.4 [\#545](https://github.com/liyasthomas/postwoman/pull/545) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Refactor [\#543](https://github.com/liyasthomas/postwoman/pull/543) ([liyasthomas](https://github.com/liyasthomas))
- chore\(deps\): bump firebase from 7.7.0 to 7.8.0 [\#542](https://github.com/liyasthomas/postwoman/pull/542) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump graphql from 14.5.8 to 14.6.0 [\#541](https://github.com/liyasthomas/postwoman/pull/541) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- i18n [\#538](https://github.com/liyasthomas/postwoman/pull/538) ([liyasthomas](https://github.com/liyasthomas))
- Modification of French translations [\#537](https://github.com/liyasthomas/postwoman/pull/537) ([thomasbnt](https://github.com/thomasbnt))
- Even [\#535](https://github.com/liyasthomas/postwoman/pull/535) ([liyasthomas](https://github.com/liyasthomas))
- Updated GraphQL Query Variable Editor [\#534](https://github.com/liyasthomas/postwoman/pull/534) ([AndrewBastin](https://github.com/AndrewBastin))
- Updating spanish translation [\#529](https://github.com/liyasthomas/postwoman/pull/529) ([liyasthomas](https://github.com/liyasthomas))
- even merge [\#528](https://github.com/liyasthomas/postwoman/pull/528) ([liyasthomas](https://github.com/liyasthomas))
## [v1.8.0](https://github.com/liyasthomas/postwoman/tree/v1.8.0) (2020-01-28)
[Full Changelog](https://github.com/liyasthomas/postwoman/compare/v1.5.0...v1.8.0)
**Fixed bugs:**
- Warn the user if name field was left blank while creating a new collection [\#515](https://github.com/liyasthomas/postwoman/issues/515)
- Multiple collections with the same name shouldn't exist [\#509](https://github.com/liyasthomas/postwoman/issues/509)
- GraphQL String variables are null [\#497](https://github.com/liyasthomas/postwoman/issues/497)
- Post request body is empty [\#473](https://github.com/liyasthomas/postwoman/issues/473)
**Closed issues:**
- Allow importing Postman collections [\#524](https://github.com/liyasthomas/postwoman/issues/524)
- Request descriptions [\#511](https://github.com/liyasthomas/postwoman/issues/511)
- Sync collection with a cloud storage \(e.g: Google drive\) [\#507](https://github.com/liyasthomas/postwoman/issues/507)
- Ability to run all requests of a folder/collection [\#498](https://github.com/liyasthomas/postwoman/issues/498)
- Change import/export collection on requests page icon [\#495](https://github.com/liyasthomas/postwoman/issues/495)
- Application contains many hard-coded strings that aren't translatable [\#488](https://github.com/liyasthomas/postwoman/issues/488)
- import cURL error [\#477](https://github.com/liyasthomas/postwoman/issues/477)
- move to postwoman org [\#475](https://github.com/liyasthomas/postwoman/issues/475)
- Create standalone vue components of the request builder. [\#474](https://github.com/liyasthomas/postwoman/issues/474)
- ULR parsing and var auto creation [\#469](https://github.com/liyasthomas/postwoman/issues/469)
- What about additional loaders: + Pug, TypeScript, SASS, material-vue ? [\#467](https://github.com/liyasthomas/postwoman/issues/467)
- \[suggestion\] - Tests tab [\#465](https://github.com/liyasthomas/postwoman/issues/465)
- cookie not found support [\#443](https://github.com/liyasthomas/postwoman/issues/443)
- Feature Request: Consumer Driven Contract Testing [\#420](https://github.com/liyasthomas/postwoman/issues/420)
- Feature Request: Support OAuth2/OIDC [\#337](https://github.com/liyasthomas/postwoman/issues/337)
- Enable running proxy as a backend for Request Capture [\#325](https://github.com/liyasthomas/postwoman/issues/325)
- Label doesn't change when switching between collection requests [\#291](https://github.com/liyasthomas/postwoman/issues/291)
- Add DB cache [\#26](https://github.com/liyasthomas/postwoman/issues/26)
**Merged pull requests:**
- Enhancements [\#531](https://github.com/liyasthomas/postwoman/pull/531) ([jamesgeorge007](https://github.com/jamesgeorge007))
- Merge pull request \#530 from liyasthomas/feature/post-request-tests [\#530](https://github.com/liyasthomas/postwoman/pull/530) ([liyasthomas](https://github.com/liyasthomas))
- Refactor [\#523](https://github.com/liyasthomas/postwoman/pull/523) ([liyasthomas](https://github.com/liyasthomas))
- chore\(deps\): bump nuxt-i18n from 6.4.1 to 6.5.0 [\#522](https://github.com/liyasthomas/postwoman/pull/522) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump v-tooltip from 2.0.2 to 2.0.3 [\#521](https://github.com/liyasthomas/postwoman/pull/521) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump cypress from 3.8.2 to 3.8.3 [\#520](https://github.com/liyasthomas/postwoman/pull/520) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Feature/post request tests [\#518](https://github.com/liyasthomas/postwoman/pull/518) ([nickpalenchar](https://github.com/nickpalenchar))
- Validations for edit and create collections activity [\#516](https://github.com/liyasthomas/postwoman/pull/516) ([jamesgeorge007](https://github.com/jamesgeorge007))
- Auth [\#513](https://github.com/liyasthomas/postwoman/pull/513) ([liyasthomas](https://github.com/liyasthomas))
- Support for Google Chrome Extension [\#512](https://github.com/liyasthomas/postwoman/pull/512) ([AndrewBastin](https://github.com/AndrewBastin))
- Validate duplicate collections [\#510](https://github.com/liyasthomas/postwoman/pull/510) ([jamesgeorge007](https://github.com/jamesgeorge007))
- GraphQL query validation based on schema [\#508](https://github.com/liyasthomas/postwoman/pull/508) ([AndrewBastin](https://github.com/AndrewBastin))
- Lint and refactor [\#506](https://github.com/liyasthomas/postwoman/pull/506) ([liyasthomas](https://github.com/liyasthomas))
- Syntax Error marking in GraphQL query editor [\#505](https://github.com/liyasthomas/postwoman/pull/505) ([AndrewBastin](https://github.com/AndrewBastin))
- Merge pull request \#504 from liyasthomas/dependabot/npm_and_yarn/node-sass-4.13.1 [\#504](https://github.com/liyasthomas/postwoman/pull/504) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump @nuxtjs/axios from 5.9.2 to 5.9.3 [\#503](https://github.com/liyasthomas/postwoman/pull/503) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps-dev\): bump sass-loader from 8.0.1 to 8.0.2 [\#502](https://github.com/liyasthomas/postwoman/pull/502) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- chore\(deps\): bump ace-builds from 1.4.7 to 1.4.8 [\#501](https://github.com/liyasthomas/postwoman/pull/501) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Refactoring proxy handling to be done in strategies [\#500](https://github.com/liyasthomas/postwoman/pull/500) ([AndrewBastin](https://github.com/AndrewBastin))
- 💚 Fixed \#497 [\#499](https://github.com/liyasthomas/postwoman/pull/499) ([pushrbx](https://github.com/pushrbx))
- Feat/firefox strategy [\#496](https://github.com/liyasthomas/postwoman/pull/496) ([liyasthomas](https://github.com/liyasthomas))
- Firefox Extension compatibility [\#494](https://github.com/liyasthomas/postwoman/pull/494) ([AndrewBastin](https://github.com/AndrewBastin))
- i18n Japanese: Added new translations [\#492](https://github.com/liyasthomas/postwoman/pull/492) ([reefqi037](https://github.com/reefqi037))
- Merge pull request \#491 from liyasthomas/i18n [\#491](https://github.com/liyasthomas/postwoman/pull/491) ([liyasthomas](https://github.com/liyasthomas))
- Replaced hard-coded strings with localizable strings [\#490](https://github.com/liyasthomas/postwoman/pull/490) ([liyasthomas](https://github.com/liyasthomas))
- Network Strategies [\#487](https://github.com/liyasthomas/postwoman/pull/487) ([AndrewBastin](https://github.com/AndrewBastin))
- chore\(oauth\): Added method signatures as per JSDoc conventions [\#486](https://github.com/liyasthomas/postwoman/pull/486) ([jamesgeorge007](https://github.com/jamesgeorge007))
- chore: Minor tweaks [\#485](https://github.com/liyasthomas/postwoman/pull/485) ([jamesgeorge007](https://github.com/jamesgeorge007))
- ⬆️ Bump cypress from 3.8.1 to 3.8.2 [\#483](https://github.com/liyasthomas/postwoman/pull/483) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump sass-loader from 8.0.0 to 8.0.1 [\#482](https://github.com/liyasthomas/postwoman/pull/482) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump @nuxtjs/google-analytics from 2.2.2 to 2.2.3 [\#481](https://github.com/liyasthomas/postwoman/pull/481) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- GraphQL Type Highlight and Links [\#479](https://github.com/liyasthomas/postwoman/pull/479) ([AndrewBastin](https://github.com/AndrewBastin))
- OAuth 2.0/OIDC Access Token Retrieval Support [\#476](https://github.com/liyasthomas/postwoman/pull/476) ([reefqi037](https://github.com/reefqi037))
## [v1.5.0](https://github.com/liyasthomas/postwoman/tree/v1.5.0) (2020-01-04)
[Full Changelog](https://github.com/liyasthomas/postwoman/compare/v1.0.0...v1.5.0)
**Fixed bugs:**
- WebSocket page freezes when pasting long URL [\#471](https://github.com/liyasthomas/postwoman/issues/471)
- API Documentation won't be generated [\#456](https://github.com/liyasthomas/postwoman/issues/456)
- Sharing Requests via link is not working [\#435](https://github.com/liyasthomas/postwoman/issues/435)
- URL input text is so stutters [\#412](https://github.com/liyasthomas/postwoman/issues/412)
- Save to collections after deleting all the collections causes an error page [\#390](https://github.com/liyasthomas/postwoman/issues/390)
- Bearer token doesnt work with CORS when only authorization header is allowed [\#353](https://github.com/liyasthomas/postwoman/issues/353)
- Make the UI more compact [\#314](https://github.com/liyasthomas/postwoman/issues/314)
- Allow reserved characters on websocket URI [\#289](https://github.com/liyasthomas/postwoman/issues/289)
- Unable to edit or delete row in history table [\#281](https://github.com/liyasthomas/postwoman/issues/281)
- Allow url request with `/` at eol [\#275](https://github.com/liyasthomas/postwoman/issues/275)
- \[request\] localhost support [\#274](https://github.com/liyasthomas/postwoman/issues/274)
- Code generation for Fetch request type of some methods \(POST, PUT, PATCH\) won't be shown [\#268](https://github.com/liyasthomas/postwoman/issues/268)
- \[BUG\] \[UI\] \[Mobile\] Get results not scrollable. [\#266](https://github.com/liyasthomas/postwoman/issues/266)
- POSTing large raw JSON packets [\#265](https://github.com/liyasthomas/postwoman/issues/265)
**Closed issues:**
- Can WSDL be implemented, similar to SoapUI? [\#461](https://github.com/liyasthomas/postwoman/issues/461)
- Module not found: Error: Can't resolve '../.postwoman/version.json' [\#457](https://github.com/liyasthomas/postwoman/issues/457)
- \* ../.postwoman/version.json in ./node_modules/babel-loader/lib??ref--2-0!./node_modules/vue-loader/lib??vue-loader-options!./layouts/default.vue?vue&type=script&lang=js& friendly-errors 11:12:37 [\#448](https://github.com/liyasthomas/postwoman/issues/448)
- Raw Request Body should be supported to format the JSON string [\#446](https://github.com/liyasthomas/postwoman/issues/446)
- npm run dev module was not found: ../.postwoman/version.json [\#442](https://github.com/liyasthomas/postwoman/issues/442)
- graphql and websocket work, but http and https do not [\#441](https://github.com/liyasthomas/postwoman/issues/441)
- Can I test localhost? [\#433](https://github.com/liyasthomas/postwoman/issues/433)
- No 'Access-Control-Allow-Origin' [\#426](https://github.com/liyasthomas/postwoman/issues/426)
- When uninstall the PWA the "install PWA" link in postwoman.io isn't appear anymore [\#419](https://github.com/liyasthomas/postwoman/issues/419)
- Toggling options will reset the UI to English [\#417](https://github.com/liyasthomas/postwoman/issues/417)
- Ability to send Binary data using Postwoman [\#415](https://github.com/liyasthomas/postwoman/issues/415)
- Oh my dear god why don't we just wrap it in electron [\#413](https://github.com/liyasthomas/postwoman/issues/413)
- UI improvement suggestion for request method drop down [\#409](https://github.com/liyasthomas/postwoman/issues/409)
- Can I share a request with my team? [\#408](https://github.com/liyasthomas/postwoman/issues/408)
- Does it not support the post method? [\#403](https://github.com/liyasthomas/postwoman/issues/403)
- Post can't send request [\#401](https://github.com/liyasthomas/postwoman/issues/401)
- \[Bug\] fix header icons overlap [\#399](https://github.com/liyasthomas/postwoman/issues/399)
- Custom request method [\#398](https://github.com/liyasthomas/postwoman/issues/398)
- Improve translate for pt-BR \(i18n\) [\#395](https://github.com/liyasthomas/postwoman/issues/395)
- Raw input disabled is not working properly [\#394](https://github.com/liyasthomas/postwoman/issues/394)
- Input area is not clearly to identify for users because the dark mode [\#393](https://github.com/liyasthomas/postwoman/issues/393)
- \[UX\] Setting to make sidebar buttons small [\#389](https://github.com/liyasthomas/postwoman/issues/389)
- \[UX\] Improve responsive breaking points [\#388](https://github.com/liyasthomas/postwoman/issues/388)
- \[UX\] Hide history/collections [\#387](https://github.com/liyasthomas/postwoman/issues/387)
- Clearing shortcut overrides browser default [\#374](https://github.com/liyasthomas/postwoman/issues/374)
- Proxy server default configuration [\#373](https://github.com/liyasthomas/postwoman/issues/373)
- Intent to translate [\#367](https://github.com/liyasthomas/postwoman/issues/367)
- \[request\]: CLI possibilities [\#363](https://github.com/liyasthomas/postwoman/issues/363)
- Feature request: OAuth header support/integration [\#358](https://github.com/liyasthomas/postwoman/issues/358)
- Static builds on releases [\#352](https://github.com/liyasthomas/postwoman/issues/352)
- fix:SSE onclose handle [\#349](https://github.com/liyasthomas/postwoman/issues/349)
- i18n support [\#348](https://github.com/liyasthomas/postwoman/issues/348)
- Separate layers in dockerfile to improve image build [\#339](https://github.com/liyasthomas/postwoman/issues/339)
- Internal server environment usage requirements [\#336](https://github.com/liyasthomas/postwoman/issues/336)
- Server Sent Events [\#329](https://github.com/liyasthomas/postwoman/issues/329)
- Generate API documentation [\#326](https://github.com/liyasthomas/postwoman/issues/326)
- \[Request\] Use responses for next request? [\#324](https://github.com/liyasthomas/postwoman/issues/324)
- Auth info on WebSocket connections [\#321](https://github.com/liyasthomas/postwoman/issues/321)
- Set response panel to fullscreen [\#320](https://github.com/liyasthomas/postwoman/issues/320)
- Graphql support [\#312](https://github.com/liyasthomas/postwoman/issues/312)
- Keyboard shortcuts [\#302](https://github.com/liyasthomas/postwoman/issues/302)
- File/binary request body support [\#298](https://github.com/liyasthomas/postwoman/issues/298)
- Make response body area expandable [\#294](https://github.com/liyasthomas/postwoman/issues/294)
- It's possible to tab into read only and non-form elements [\#287](https://github.com/liyasthomas/postwoman/issues/287)
- Change cursor to disabled on disabled inputs [\#286](https://github.com/liyasthomas/postwoman/issues/286)
- Hover Styling on Inputs [\#285](https://github.com/liyasthomas/postwoman/issues/285)
- Focus Styles on Buttons [\#284](https://github.com/liyasthomas/postwoman/issues/284)
- Mobile can't see console for request errors [\#283](https://github.com/liyasthomas/postwoman/issues/283)
- Missing Focus on Inputs [\#279](https://github.com/liyasthomas/postwoman/issues/279)
- Download the request result into a file. [\#278](https://github.com/liyasthomas/postwoman/issues/278)
- Improve UI Contrast [\#277](https://github.com/liyasthomas/postwoman/issues/277)
- Duplicated query string in generated code [\#272](https://github.com/liyasthomas/postwoman/issues/272)
- Query parameters are duplicated [\#271](https://github.com/liyasthomas/postwoman/issues/271)
- Generated code is incorrect [\#269](https://github.com/liyasthomas/postwoman/issues/269)
- \[UI\] \[UX\] Allow app to take width of browser [\#236](https://github.com/liyasthomas/postwoman/issues/236)
- Extend syntax highlighting with ACE for pre-request script textarea [\#235](https://github.com/liyasthomas/postwoman/issues/235)
- Store pre-request scripts in history [\#233](https://github.com/liyasthomas/postwoman/issues/233)
- Lacking documentation and wiki [\#232](https://github.com/liyasthomas/postwoman/issues/232)
- Store the time spent on fetching a response [\#225](https://github.com/liyasthomas/postwoman/issues/225)
- I can't send POST method [\#210](https://github.com/liyasthomas/postwoman/issues/210)
- Cache view [\#188](https://github.com/liyasthomas/postwoman/issues/188)
- Handling request failures when build number is obtained from GitHub [\#122](https://github.com/liyasthomas/postwoman/issues/122)
**Merged pull requests:**
- ⬆️ Bump @nuxtjs/axios from 5.9.0 to 5.9.2 [\#472](https://github.com/liyasthomas/postwoman/pull/472) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Added variables to graphql page. [\#464](https://github.com/liyasthomas/postwoman/pull/464) ([pushrbx](https://github.com/pushrbx))
- i18n [\#463](https://github.com/liyasthomas/postwoman/pull/463) ([liyasthomas](https://github.com/liyasthomas))
- ⬆️ Bump cypress from 3.8.0 to 3.8.1 [\#460](https://github.com/liyasthomas/postwoman/pull/460) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- i18n\(de-DE\): improve some translations [\#459](https://github.com/liyasthomas/postwoman/pull/459) ([gabschne](https://github.com/gabschne))
- bn-BD i18n [\#455](https://github.com/liyasthomas/postwoman/pull/455) ([hmtanbir](https://github.com/hmtanbir))
- API documentation page [\#451](https://github.com/liyasthomas/postwoman/pull/451) ([liyasthomas](https://github.com/liyasthomas))
- ⬆️ Bump @nuxtjs/axios from 5.8.0 to 5.9.0 [\#450](https://github.com/liyasthomas/postwoman/pull/450) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump nuxt from 2.10.2 to 2.11.0 [\#449](https://github.com/liyasthomas/postwoman/pull/449) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Various UI tweaks [\#439](https://github.com/liyasthomas/postwoman/pull/439) ([liyasthomas](https://github.com/liyasthomas))
- i18n [\#438](https://github.com/liyasthomas/postwoman/pull/438) ([liyasthomas](https://github.com/liyasthomas))
- Burmese translation added [\#437](https://github.com/liyasthomas/postwoman/pull/437) ([ZattWine](https://github.com/ZattWine))
- chore: stick to Vue.js best practices [\#432](https://github.com/liyasthomas/postwoman/pull/432) ([jamesgeorge007](https://github.com/jamesgeorge007))
- Styled select input [\#431](https://github.com/liyasthomas/postwoman/pull/431) ([liyasthomas](https://github.com/liyasthomas))
- Bumped dependencies and Improved UI contrast [\#430](https://github.com/liyasthomas/postwoman/pull/430) ([liyasthomas](https://github.com/liyasthomas))
- ⬆️ Bump cypress from 3.7.0 to 3.8.0 [\#429](https://github.com/liyasthomas/postwoman/pull/429) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- I18n [\#428](https://github.com/liyasthomas/postwoman/pull/428) ([liyasthomas](https://github.com/liyasthomas))
- I18n Japanese translation added [\#427](https://github.com/liyasthomas/postwoman/pull/427) ([reefqi037](https://github.com/reefqi037))
- Even [\#424](https://github.com/liyasthomas/postwoman/pull/424) ([liyasthomas](https://github.com/liyasthomas))
- I18n [\#423](https://github.com/liyasthomas/postwoman/pull/423) ([liyasthomas](https://github.com/liyasthomas))
- I18n German translation added [\#422](https://github.com/liyasthomas/postwoman/pull/422) ([NJannasch](https://github.com/NJannasch))
- Header key autocompletion [\#421](https://github.com/liyasthomas/postwoman/pull/421) ([AndrewBastin](https://github.com/AndrewBastin))
- Update id-ID.js [\#416](https://github.com/liyasthomas/postwoman/pull/416) ([williamsp](https://github.com/williamsp))
- Improving translation for id-ID [\#414](https://github.com/liyasthomas/postwoman/pull/414) ([williamsp](https://github.com/williamsp))
- Fixing bug on request saving [\#410](https://github.com/liyasthomas/postwoman/pull/410) ([adevr](https://github.com/adevr))
- ⬆️ Bump @nuxtjs/google-analytics from 2.2.1 to 2.2.2 [\#407](https://github.com/liyasthomas/postwoman/pull/407) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump vue-virtual-scroll-list from 1.4.3 to 1.4.4 [\#406](https://github.com/liyasthomas/postwoman/pull/406) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump nuxt-i18n from 6.4.0 to 6.4.1 [\#405](https://github.com/liyasthomas/postwoman/pull/405) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- I18n [\#404](https://github.com/liyasthomas/postwoman/pull/404) ([yubathom](https://github.com/yubathom))
- Custom methods support [\#400](https://github.com/liyasthomas/postwoman/pull/400) ([liyasthomas](https://github.com/liyasthomas))
- App UI [\#391](https://github.com/liyasthomas/postwoman/pull/391) ([liyasthomas](https://github.com/liyasthomas))
- i18n [\#383](https://github.com/liyasthomas/postwoman/pull/383) ([liyasthomas](https://github.com/liyasthomas))
- Added Turkish Language Support [\#382](https://github.com/liyasthomas/postwoman/pull/382) ([AliAnilKocak](https://github.com/AliAnilKocak))
- Translated new words to Farsi lang [\#380](https://github.com/liyasthomas/postwoman/pull/380) ([hosseinnedaee](https://github.com/hosseinnedaee))
- Two Way Data Binding \(v-model\) to Ace Editor component [\#379](https://github.com/liyasthomas/postwoman/pull/379) ([AndrewBastin](https://github.com/AndrewBastin))
- fix: twitter summary card image url [\#378](https://github.com/liyasthomas/postwoman/pull/378) ([peterpeterparker](https://github.com/peterpeterparker))
- Added nav shortcuts to GraphQL query and response, updated GraphQL shortcut icons [\#377](https://github.com/liyasthomas/postwoman/pull/377) ([AndrewBastin](https://github.com/AndrewBastin))
- Bump cypress from 3.6.1 to 3.7.0 [\#376](https://github.com/liyasthomas/postwoman/pull/376) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump vuex-persist from 2.1.1 to 2.2.0 [\#375](https://github.com/liyasthomas/postwoman/pull/375) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- I18n [\#372](https://github.com/liyasthomas/postwoman/pull/372) ([EdikWang](https://github.com/EdikWang))
- Use GraphQL logo for GraphQL tab [\#371](https://github.com/liyasthomas/postwoman/pull/371) ([NBTX](https://github.com/NBTX))
- Update i18n keywords for Bahasa Indonesia [\#369](https://github.com/liyasthomas/postwoman/pull/369) ([wahwahid](https://github.com/wahwahid))
- Intent to translate to Spanish on I18n [\#368](https://github.com/liyasthomas/postwoman/pull/368) ([adlpaf](https://github.com/adlpaf))
- I18n [\#366](https://github.com/liyasthomas/postwoman/pull/366) ([liyasthomas](https://github.com/liyasthomas))
- Add translations for FR/EN catalogues [\#364](https://github.com/liyasthomas/postwoman/pull/364) ([LaurentBrieu](https://github.com/LaurentBrieu))
- Added Bahasa Indonesia language support [\#362](https://github.com/liyasthomas/postwoman/pull/362) ([wahwahid](https://github.com/wahwahid))
- i18n [\#361](https://github.com/liyasthomas/postwoman/pull/361) ([liyasthomas](https://github.com/liyasthomas))
- Add Simplified Chinese language [\#360](https://github.com/liyasthomas/postwoman/pull/360) ([EdikWang](https://github.com/EdikWang))
- Added Brazilian Portuguese language support [\#359](https://github.com/liyasthomas/postwoman/pull/359) ([tetri](https://github.com/tetri))
- Added Farsi language support [\#357](https://github.com/liyasthomas/postwoman/pull/357) ([hosseinnedaee](https://github.com/hosseinnedaee))
- Adding french language basic [\#355](https://github.com/liyasthomas/postwoman/pull/355) ([thomasbnt](https://github.com/thomasbnt))
- Basic i18n support [\#351](https://github.com/liyasthomas/postwoman/pull/351) ([liyasthomas](https://github.com/liyasthomas))
- Undo header/param/body param deletion [\#350](https://github.com/liyasthomas/postwoman/pull/350) ([AndrewBastin](https://github.com/AndrewBastin))
- Added ability to run GraphQL queries [\#346](https://github.com/liyasthomas/postwoman/pull/346) ([AndrewBastin](https://github.com/AndrewBastin))
- Add Proxy URL option [\#345](https://github.com/liyasthomas/postwoman/pull/345) ([NBTX](https://github.com/NBTX))
- ♻️ Refactor Functions [\#344](https://github.com/liyasthomas/postwoman/pull/344) ([athul](https://github.com/athul))
- refactor: minor improvements [\#343](https://github.com/liyasthomas/postwoman/pull/343) ([jamesgeorge007](https://github.com/jamesgeorge007))
## [v1.0.0](https://github.com/liyasthomas/postwoman/tree/v1.0.0) (2019-11-04)
[Full Changelog](https://github.com/liyasthomas/postwoman/compare/v0.1.0...v1.0.0)
**Fixed bugs:**
- Bearer Token value still left even after being cleared [\#212](https://github.com/liyasthomas/postwoman/issues/212)
- All changes in input fields lost when you switch to another page [\#203](https://github.com/liyasthomas/postwoman/issues/203)
- POST request json bodies aren't sent [\#180](https://github.com/liyasthomas/postwoman/issues/180)
- Headers turn into 0 : \[Object object\] [\#166](https://github.com/liyasthomas/postwoman/issues/166)
- Send Again Button Constantly Flickering [\#157](https://github.com/liyasthomas/postwoman/issues/157)
- There are cross-domain problems [\#128](https://github.com/liyasthomas/postwoman/issues/128)
- Raw requests are not being sent [\#124](https://github.com/liyasthomas/postwoman/issues/124)
- Request Body Is Not Sent [\#113](https://github.com/liyasthomas/postwoman/issues/113)
- default menu option - 'Http' is not highlighted when launched from installed pwa app \(UI bug\) [\#100](https://github.com/liyasthomas/postwoman/issues/100)
- App is broken with old history in localStorage [\#74](https://github.com/liyasthomas/postwoman/issues/74)
- Last added history entry is removed automatically after refresh [\#66](https://github.com/liyasthomas/postwoman/issues/66)
- Cannot use localhost as base url [\#56](https://github.com/liyasthomas/postwoman/issues/56)
- \[CORS\] No 'Access-Control-Allow-Origin' header is present on the requested resource [\#2](https://github.com/liyasthomas/postwoman/issues/2)
**Closed issues:**
- Section labels don't display properly in Firefox [\#237](https://github.com/liyasthomas/postwoman/issues/237)
- Unsupported URLs \[BUG\]? [\#229](https://github.com/liyasthomas/postwoman/issues/229)
- Credentials are still being included in Permalink even when "Include in URL" is turned off [\#227](https://github.com/liyasthomas/postwoman/issues/227)
- Display sendRequest runtime errors in the console [\#206](https://github.com/liyasthomas/postwoman/issues/206)
- Chain requests. Execute a bunch of requests one by one and produce results [\#196](https://github.com/liyasthomas/postwoman/issues/196)
- Allow User to Choose Whether to Include Authentication in Permalink [\#178](https://github.com/liyasthomas/postwoman/issues/178)
- Allow HTTP \(not HTTPS\) on postwoman.io [\#175](https://github.com/liyasthomas/postwoman/issues/175)
- Docker-compose in development [\#168](https://github.com/liyasthomas/postwoman/issues/168)
- Add Docker [\#164](https://github.com/liyasthomas/postwoman/issues/164)
- Missing "Landing/start page" [\#162](https://github.com/liyasthomas/postwoman/issues/162)
- Response with content-type "application/hal+json" shows as \[Object object\] [\#158](https://github.com/liyasthomas/postwoman/issues/158)
- Clear Input [\#155](https://github.com/liyasthomas/postwoman/issues/155)
- A place to discuss [\#149](https://github.com/liyasthomas/postwoman/issues/149)
- Inconsistent version name [\#141](https://github.com/liyasthomas/postwoman/issues/141)
- introduce some script language to parse the response and pass environment variable as request parameter [\#139](https://github.com/liyasthomas/postwoman/issues/139)
- Add links to the footer version and commit sha [\#134](https://github.com/liyasthomas/postwoman/issues/134)
- Please add a label for each request. It will be helpful. [\#133](https://github.com/liyasthomas/postwoman/issues/133)
- Use 'icon buttons' instead of 'text buttons' [\#130](https://github.com/liyasthomas/postwoman/issues/130)
- Change .editorconfig [\#115](https://github.com/liyasthomas/postwoman/issues/115)
- \[UX\] Provide Focus State for Buttons, etc. [\#112](https://github.com/liyasthomas/postwoman/issues/112)
- Autoresize the textarea [\#102](https://github.com/liyasthomas/postwoman/issues/102)
- Content-Type revamping [\#99](https://github.com/liyasthomas/postwoman/issues/99)
- Add linter semistandard [\#98](https://github.com/liyasthomas/postwoman/issues/98)
- Add version number in footer [\#97](https://github.com/liyasthomas/postwoman/issues/97)
- Show "Send" button all over the page or enable hotkeys [\#94](https://github.com/liyasthomas/postwoman/issues/94)
- Import request from cURL [\#93](https://github.com/liyasthomas/postwoman/issues/93)
- Search on History [\#92](https://github.com/liyasthomas/postwoman/issues/92)
- Add support for "application/hal+json" Content-Type [\#88](https://github.com/liyasthomas/postwoman/issues/88)
- The query string is built incorrectly when the path contains a parameter [\#87](https://github.com/liyasthomas/postwoman/issues/87)
- The history doesn't show a date with the timestamp. [\#81](https://github.com/liyasthomas/postwoman/issues/81)
- Option to Copy request as Fetch or XHR Or CURL [\#76](https://github.com/liyasthomas/postwoman/issues/76)
- Not working on Brave Browser anymore [\#71](https://github.com/liyasthomas/postwoman/issues/71)
- Why da fuq is your name plastered all over the README? [\#70](https://github.com/liyasthomas/postwoman/issues/70)
- Comparison with Postman is missing [\#69](https://github.com/liyasthomas/postwoman/issues/69)
- Add Tests [\#65](https://github.com/liyasthomas/postwoman/issues/65)
- HTTP request with different library [\#61](https://github.com/liyasthomas/postwoman/issues/61)
- Editorconfig file [\#60](https://github.com/liyasthomas/postwoman/issues/60)
- 500 this.isValidURL is not a function [\#58](https://github.com/liyasthomas/postwoman/issues/58)
- Request Headers [\#57](https://github.com/liyasthomas/postwoman/issues/57)
- Colored response codes based on status code [\#46](https://github.com/liyasthomas/postwoman/issues/46)
- Improve SEO [\#45](https://github.com/liyasthomas/postwoman/issues/45)
- Add html preview to response section [\#41](https://github.com/liyasthomas/postwoman/issues/41)
- websocket support [\#40](https://github.com/liyasthomas/postwoman/issues/40)
- Styling with Tailwindcss [\#38](https://github.com/liyasthomas/postwoman/issues/38)
- Not Working in IE 11 [\#37](https://github.com/liyasthomas/postwoman/issues/37)
- Raw request body for POST requests and Authorization key/value in Header [\#36](https://github.com/liyasthomas/postwoman/issues/36)
- Code highlight on response body [\#33](https://github.com/liyasthomas/postwoman/issues/33)
- Template selector [\#32](https://github.com/liyasthomas/postwoman/issues/32)
- Vue template [\#31](https://github.com/liyasthomas/postwoman/issues/31)
- Add copy response to clipboard button [\#30](https://github.com/liyasthomas/postwoman/issues/30)
- Ability to store/share/create collections [\#29](https://github.com/liyasthomas/postwoman/issues/29)
- PWA not installable [\#19](https://github.com/liyasthomas/postwoman/issues/19)
- Send request on Enter Key press [\#17](https://github.com/liyasthomas/postwoman/issues/17)
- Simple Misspelling [\#8](https://github.com/liyasthomas/postwoman/issues/8)
- Readable [\#5](https://github.com/liyasthomas/postwoman/issues/5)
- Serialize a request into JSON? [\#4](https://github.com/liyasthomas/postwoman/issues/4)
**Merged pull requests:**
- docs: add liyasthomas as a contributor [\#264](https://github.com/liyasthomas/postwoman/pull/264) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add jamesgeorge007 as a contributor [\#263](https://github.com/liyasthomas/postwoman/pull/263) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add NBTX as a contributor [\#262](https://github.com/liyasthomas/postwoman/pull/262) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- Fix .all-contributorsrc badge template. [\#260](https://github.com/liyasthomas/postwoman/pull/260) ([NBTX](https://github.com/NBTX))
- docs: add hosseinnedaee as a contributor [\#259](https://github.com/liyasthomas/postwoman/pull/259) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add nityanandagohain as a contributor [\#257](https://github.com/liyasthomas/postwoman/pull/257) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add JacobAnavisca as a contributor [\#256](https://github.com/liyasthomas/postwoman/pull/256) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add izerozlu as a contributor [\#255](https://github.com/liyasthomas/postwoman/pull/255) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add vlad0337187 as a contributor [\#254](https://github.com/liyasthomas/postwoman/pull/254) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add AndrewBastin as a contributor [\#253](https://github.com/liyasthomas/postwoman/pull/253) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add terranblake as a contributor [\#252](https://github.com/liyasthomas/postwoman/pull/252) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add nickpalenchar as a contributor [\#251](https://github.com/liyasthomas/postwoman/pull/251) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add yubathom as a contributor [\#250](https://github.com/liyasthomas/postwoman/pull/250) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add larouxn as a contributor [\#249](https://github.com/liyasthomas/postwoman/pull/249) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add NBTX as a contributor [\#248](https://github.com/liyasthomas/postwoman/pull/248) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- docs: add liyasthomas as a contributor [\#247](https://github.com/liyasthomas/postwoman/pull/247) ([allcontributors[bot]](https://github.com/apps/allcontributors))
- Make page changes more fluid [\#246](https://github.com/liyasthomas/postwoman/pull/246) ([NBTX](https://github.com/NBTX))
- Minor tweaks [\#245](https://github.com/liyasthomas/postwoman/pull/245) ([liyasthomas](https://github.com/liyasthomas))
- Add brand new logo to the project [\#244](https://github.com/liyasthomas/postwoman/pull/244) ([caneco](https://github.com/caneco))
- ⬆️ Bump @nuxtjs/google-tag-manager from 2.3.0 to 2.3.1 [\#243](https://github.com/liyasthomas/postwoman/pull/243) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump yargs-parser from 15.0.0 to 16.1.0 [\#242](https://github.com/liyasthomas/postwoman/pull/242) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump @nuxtjs/toast from 3.2.1 to 3.3.0 [\#241](https://github.com/liyasthomas/postwoman/pull/241) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump highlight.js from 9.15.10 to 9.16.2 [\#240](https://github.com/liyasthomas/postwoman/pull/240) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump cypress from 3.5.0 to 3.6.0 [\#239](https://github.com/liyasthomas/postwoman/pull/239) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Fix legend labels in Firefox, fix colored labels slider [\#238](https://github.com/liyasthomas/postwoman/pull/238) ([NBTX](https://github.com/NBTX))
- Feature/pre request script [\#231](https://github.com/liyasthomas/postwoman/pull/231) ([nickpalenchar](https://github.com/nickpalenchar))
- Documentation Cleanup [\#230](https://github.com/liyasthomas/postwoman/pull/230) ([amitdash291](https://github.com/amitdash291))
- Fix \#227 Exclude credentials from permalink [\#228](https://github.com/liyasthomas/postwoman/pull/228) ([reefqi037](https://github.com/reefqi037))
- ⬆️ Bump cypress from 3.4.1 to 3.5.0 [\#224](https://github.com/liyasthomas/postwoman/pull/224) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump @nuxtjs/axios from 5.6.0 to 5.8.0 [\#223](https://github.com/liyasthomas/postwoman/pull/223) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump node-sass from 4.12.0 to 4.13.0 [\#222](https://github.com/liyasthomas/postwoman/pull/222) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump nuxt from 2.10.1 to 2.10.2 [\#221](https://github.com/liyasthomas/postwoman/pull/221) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump @nuxtjs/google-analytics from 2.2.0 to 2.2.1 [\#220](https://github.com/liyasthomas/postwoman/pull/220) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump vuex-persist from 2.1.0 to 2.1.1 [\#219](https://github.com/liyasthomas/postwoman/pull/219) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Add the ApolloTV proxy server [\#217](https://github.com/liyasthomas/postwoman/pull/217) ([NBTX](https://github.com/NBTX))
- Fixed frame colors toggle [\#216](https://github.com/liyasthomas/postwoman/pull/216) ([mateusppereira](https://github.com/mateusppereira))
- Re-order sections and add toggle for including authentication in URL [\#215](https://github.com/liyasthomas/postwoman/pull/215) ([NBTX](https://github.com/NBTX))
- chore: minor code refactor [\#214](https://github.com/liyasthomas/postwoman/pull/214) ([jamesgeorge007](https://github.com/jamesgeorge007))
- Fix \#212 Clear bearer token value [\#213](https://github.com/liyasthomas/postwoman/pull/213) ([reefqi037](https://github.com/reefqi037))
- bug: keeping information on page change [\#211](https://github.com/liyasthomas/postwoman/pull/211) ([breno-pereira](https://github.com/breno-pereira))
- Work in Progress: feature/allow-collections-importing [\#209](https://github.com/liyasthomas/postwoman/pull/209) ([vlad0337187](https://github.com/vlad0337187))
- fix: don't display 'Collection is empty' label if collection has any … [\#208](https://github.com/liyasthomas/postwoman/pull/208) ([vlad0337187](https://github.com/vlad0337187))
- Feature/log errors [\#207](https://github.com/liyasthomas/postwoman/pull/207) ([nickpalenchar](https://github.com/nickpalenchar))
- Use returned value from toggle component on change event [\#205](https://github.com/liyasthomas/postwoman/pull/205) ([hosseinnedaee](https://github.com/hosseinnedaee))
- Fix proxy URL [\#201](https://github.com/liyasthomas/postwoman/pull/201) ([NBTX](https://github.com/NBTX))
- Fix CORS and Mixed-Content issue & Bug Fixes [\#200](https://github.com/liyasthomas/postwoman/pull/200) ([NBTX](https://github.com/NBTX))
- Fix CORS and mixed content issue [\#199](https://github.com/liyasthomas/postwoman/pull/199) ([hosseinnedaee](https://github.com/hosseinnedaee))
- ⬆️ Bump start-server-and-test from 1.10.5 to 1.10.6 [\#198](https://github.com/liyasthomas/postwoman/pull/198) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Added Tooltips [\#197](https://github.com/liyasthomas/postwoman/pull/197) ([AndrewBastin](https://github.com/AndrewBastin))
- ⬆️ Bump start-server-and-test from 1.10.3 to 1.10.5 [\#194](https://github.com/liyasthomas/postwoman/pull/194) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump @nuxtjs/google-tag-manager from 2.2.1 to 2.3.0 [\#193](https://github.com/liyasthomas/postwoman/pull/193) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump nuxt from 2.10.0 to 2.10.1 [\#192](https://github.com/liyasthomas/postwoman/pull/192) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- ⬆️ Bump yargs-parser from 14.0.0 to 15.0.0 [\#191](https://github.com/liyasthomas/postwoman/pull/191) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Add quotation marks for generated code [\#187](https://github.com/liyasthomas/postwoman/pull/187) ([johnhenry](https://github.com/johnhenry))
- Added auto theme [\#185](https://github.com/liyasthomas/postwoman/pull/185) ([AndrewBastin](https://github.com/AndrewBastin))
- Add Request name label for every requests [\#184](https://github.com/liyasthomas/postwoman/pull/184) ([sharath2106](https://github.com/sharath2106))
- updated threshold and rootMargin for IntersectionObserver [\#182](https://github.com/liyasthomas/postwoman/pull/182) ([edisonaugusthy](https://github.com/edisonaugusthy))
- Add basic e2e tests [\#181](https://github.com/liyasthomas/postwoman/pull/181) ([yubathom](https://github.com/yubathom))
- ⬆️ Bump nuxt from 2.9.2 to 2.10.0 [\#179](https://github.com/liyasthomas/postwoman/pull/179) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- 🐛 Fixed sitemap configuration [\#177](https://github.com/liyasthomas/postwoman/pull/177) ([NicoPennec](https://github.com/NicoPennec))
- Collections [\#176](https://github.com/liyasthomas/postwoman/pull/176) ([TheHollidayInn](https://github.com/TheHollidayInn))
- Code Refactoring [\#173](https://github.com/liyasthomas/postwoman/pull/173) ([edisonaugusthy](https://github.com/edisonaugusthy))
- Added Black Theme [\#172](https://github.com/liyasthomas/postwoman/pull/172) ([AndrewBastin](https://github.com/AndrewBastin))
## [v0.1.0](https://github.com/liyasthomas/postwoman/tree/v0.1.0) (2019-08-22)
[Full Changelog](https://github.com/liyasthomas/postwoman/compare/91c08a5e6305cc95a0df46a33fdd0013bf7339b4...v0.1.0)
\* _This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)_
Visit [releases](https://github.com/hoppscotch/hoppscotch/releases) for full changelog.

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020
Copyright (c) 2022
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -89,13 +89,13 @@
- `TRACE` - Performs a message loop-back test along the path to the target resource
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
🌈 **Make it yours:** Customizable combinations for background, foreground and accent colors — [customize now](https://hoppscotch.io/settings).
🌈 **Make it yours:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
**Theming**
- Choose theme: System (default), Light, Dark and Black
- Choose accent color: Green (default), Teal, Blue, Indigo, Purple, Yellow, Orange, Red and Pink
- Distraction free Zen mode
- Choose a theme: System (default), Light, Dark, and Black
- Choose accent color: Green (default), Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
- Distraction-free Zen mode
_Customized themes are synced with cloud / local session_
@@ -120,11 +120,11 @@ _Customized themes are synced with cloud / local session_
🔌 **WebSocket:** Establish full-duplex communication channels over a single TCP connection.
📡 **Server Sent Events:** Receive a stream of updates from a server over a HTTP connection without resorting to polling.
📡 **Server-Sent Events:** Receive a stream of updates from a server over an HTTP connection without resorting to polling.
🌩 **Socket.IO:** Send and Receive data with SocketIO server.
🦟 **MQTT:** Subscribe and Publish to topics of a MQTT Broker.
🦟 **MQTT:** Subscribe and Publish to topics of an MQTT Broker.
🔮 **GraphQL:** GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.
@@ -134,7 +134,7 @@ _Customized themes are synced with cloud / local session_
- Query schema
- Get query response
🔐 **Authorization:** Allows to identify the end user.
🔐 **Authorization:** Allows to identify the end-user.
- None
- Basic
@@ -149,10 +149,10 @@ _Customized themes are synced with cloud / local session_
📃 **Request Body:** Used to send and receive data via the REST API.
- Set `Content Type`
- FormData, JSON and many more
- FormData, JSON, and many more
- Toggle between key-value and RAW input parameter list
👋 **Response:** Contains the status line, headers and the message/response body.
👋 **Response:** Contains the status line, headers, and the message/response body.
- Copy response to clipboard
- Download response as a file
@@ -163,22 +163,22 @@ _Customized themes are synced with cloud / local session_
📁 **Collections:** Keep your API requests organized with collections and folders. Reuse them with a single click.
- Unlimited collections, folders and requests
- Unlimited collections, folders, and requests
- Nested folders
- Export and import as file or GitHub gist
- Export and import as a file or GitHub gist
_Collections are synced with cloud / local session storage_
🌐 **Proxy:** Enable Proxy Mode from Settings to access blocked APIs.
- Hide your IP address
- Fixes [`CORS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) (Cross Origin Resource Sharing) issues
- Fixes [`CORS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) (Cross-Origin Resource Sharing) issues
- Access APIs served in non-HTTPS (`http://`) endpoints
- Use your own Proxy URL
- Use your Proxy URL
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/privacy)**_
📜 **Pre-Request Scripts β:** Snippets of code associated with a request that are executed before the request is sent.
📜 **Pre-Request Scripts β:** Snippets of code associated with a request that is executed before the request is sent.
- Set environment variables
- Include timestamp in the request headers
@@ -195,7 +195,7 @@ _Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/h
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/features/shortcuts)**
🌎 **i18n:** Experience the app in your own language.
🌎 **i18n:** Experience the app in your language.
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md), and the process for submitting pull requests to us.
@@ -228,7 +228,7 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
- Environments
- Settings
**Post-Request Tests β:** Write tests associated with a request that are executed after the request response.
**Post-Request Tests β:** Write tests associated with a request that is executed after the request's response.
- Check the status code as an integer
- Filter response headers
@@ -238,7 +238,7 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
🌱 **Environments** : Environment variables allow you to store and reuse values in your requests and scripts.
- Unlimited environments and variables
- Initialize through pre-request script
- Initialize through the pre-request script
- Export as / import from GitHub gist
<details>
@@ -277,10 +277,9 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
## **Usage**
1. Choose `method`
2. Enter `URL`
3. Send request
4. Get response
1. Provide your API endpoint in the URL field
2. CLick "Send" to simulate the request
3. View the response
## **Built with**
@@ -295,9 +294,9 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
0. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/.env.example) file found in `packages/hoppscotch-app` with your own keys and rename it to `.env`.
_Sample keys only works with the [production build](https://hoppscotch.io)._
_Sample keys only work with the [production build](https://hoppscotch.io)._
### Browser based development environment
### Browser-based development environment
- [GitHub codespace](https://docs.github.com/en/codespaces/developing-in-codespaces/creating-a-codespace)
- [Gitpod](https://gitpod.io/#https://github.com/hoppscotch/hoppscotch)
@@ -308,13 +307,13 @@ _Sample keys only works with the [production build](https://hoppscotch.io)._
2. Install pnpm using npm by running `npm install -g pnpm`.
3. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
4. Start the development server with `pnpm run dev`.
5. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
5. Open the development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
### Docker compose
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
2. Run `docker-compose up`
3. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
2. Run `docker-compose up` within the directory that you cloned (probably `hoppscotch`).
3. Open the development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
## **Docker**
@@ -348,7 +347,7 @@ See the [`CHANGELOG`](CHANGELOG.md) file for details.
## **Authors**
This project exists thanks to all the people who contribute — [make a contribution](CONTRIBUTING.md).
This project exists thanks to all the people who contribute — [contribute](CONTRIBUTING.md).
<div align="center">
<a href="https://github.com/hoppscotch/hoppscotch/graphs/contributors">

View File

@@ -1,14 +1,13 @@
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid != null;
}
// Make sure the uid of the requesting user matches the name of the user
// Make sure the uid of the requesting user matches name of the user
// document. The wildcard expression {userId} makes the userId variable
// available in rules.
match /users/{userId} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
allow read, write, create, update, delete: if request.auth.uid != null && request.auth.uid == userId;
}
match /users/{userId}/{document=**} {
allow read, write, create, update, delete: if request.auth.uid != null && request.auth.uid == userId;
}
}
}

View File

@@ -33,7 +33,7 @@
[[redirects]]
from = "/careers"
to = "https://hoppscotch.notion.site/3b9d5d5239a043bfb91701faabf5b8f0"
to = "https://company.hoppscotch.io/careers"
status = 301
force = true
@@ -54,3 +54,9 @@
to = "https://github.com/hoppscotch/hoppscotch"
status = 301
force = true
[[redirects]]
from = "/announcements"
to = "https://company.hoppscotch.io/announcements"
status = 301
force = true

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-app",
"version": "2.1.0",
"version": "2.2.1",
"description": "Open source API development ecosystem",
"author": "Hoppscotch (support@hoppscotch.io)",
"private": true,
@@ -21,10 +21,11 @@
],
"dependencies": {
"husky": "^7.0.4",
"lint-staged": "^12.1.2"
"lint-staged": "^12.3.1"
},
"devDependencies": {
"@commitlint/cli": "^15.0.0",
"@commitlint/config-conventional": "^15.0.0"
"@commitlint/cli": "^16.1.0",
"@commitlint/config-conventional": "^16.0.0",
"@types/node": "^17.0.10"
}
}

View File

@@ -3,6 +3,7 @@
"version": "0.1.0",
"description": "GraphQL language support for CodeMirror",
"author": "Hoppscotch (support@hoppscotch.io)",
"license": "MIT",
"scripts": {
"prepare": "rollup -c"
},
@@ -16,17 +17,16 @@
"types": "dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@codemirror/highlight": "^0.19.6",
"@codemirror/language": "^0.19.7",
"@lezer/lr": "^0.15.5"
"@codemirror/highlight": "^0.19.0",
"@codemirror/language": "^0.19.0",
"@lezer/lr": "^0.15.7"
},
"devDependencies": {
"@lezer/generator": "^0.15.0",
"mocha": "^9.0.1",
"rollup": "^2.60.2",
"rollup-plugin-dts": "^4.0.1",
"rollup-plugin-ts": "^2.0.4",
"typescript": "^4.5.2"
},
"license": "MIT"
"@lezer/generator": "^0.15.3",
"mocha": "^9.1.4",
"rollup": "^2.66.0",
"rollup-plugin-dts": "^4.1.0",
"rollup-plugin-ts": "^2.0.5",
"typescript": "^4.5.5"
}
}

View File

@@ -1,12 +1,12 @@
import typescript from "rollup-plugin-ts"
import {lezer} from "@lezer/generator/rollup"
import { lezer } from "@lezer/generator/rollup"
export default {
input: "src/index.js",
external: id => id != "tslib" && !/^(\.?\/|\w:)/.test(id),
external: (id) => id != "tslib" && !/^(\.?\/|\w:)/.test(id),
output: [
{file: "dist/index.cjs", format: "cjs"},
{dir: "./dist", format: "es"}
{ file: "dist/index.cjs", format: "cjs" },
{ dir: "./dist", format: "es" },
],
plugins: [lezer(), typescript()]
plugins: [lezer(), typescript()],
}

View File

@@ -1,22 +1,30 @@
import {parser} from "./syntax.grammar"
import {LRLanguage, LanguageSupport, indentNodeProp, foldNodeProp, foldInside, delimitedIndent} from "@codemirror/language"
import {styleTags, tags as t} from "@codemirror/highlight"
import { parser } from "./syntax.grammar"
import {
LRLanguage,
LanguageSupport,
indentNodeProp,
foldNodeProp,
foldInside,
delimitedIndent,
} from "@codemirror/language"
import { styleTags, tags as t } from "@codemirror/highlight"
export const GQLLanguage = LRLanguage.define({
parser: parser.configure({
props: [
indentNodeProp.add({
"SelectionSet FieldsDefinition ObjectValue SchemaDefinition RootTypeDef": delimitedIndent({ closing: "}", align: true }),
"SelectionSet FieldsDefinition ObjectValue SchemaDefinition RootTypeDef":
delimitedIndent({ closing: "}", align: true }),
}),
foldNodeProp.add({
Application: foldInside,
"SelectionSet FieldsDefinition ObjectValue RootOperationTypeDefinition RootTypeDef": (node) => {
return {
from: node.from,
to: node.to
}
}
"SelectionSet FieldsDefinition ObjectValue RootOperationTypeDefinition RootTypeDef":
(node) => {
return {
from: node.from,
to: node.to,
}
},
}),
styleTags({
Name: t.definition(t.variableName),
@@ -29,13 +37,13 @@ export const GQLLanguage = LRLanguage.define({
NullValue: t.null,
ObjectValue: t.brace,
Comment: t.lineComment,
})
]
}),
],
}),
languageData: {
commentTokens: { line: "#" },
closeBrackets: { brackets: ["(", "[", "{", '"', '"""'] }
}
closeBrackets: { brackets: ["(", "[", "{", '"', '"""'] },
},
})
export function GQL() {

View File

@@ -6,7 +6,7 @@
"newLine": "lf",
"declaration": true,
"moduleResolution": "node",
"allowJs": true,
"allowJs": true
},
"include": ["src/*"]
}
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M12 0C5.417 0 0 5.417 0 12s5.417 12 12 12 12-5.417 12-12S18.583 0 12 0zm0 2.478c5.256 0 9.522 4.266 9.522 9.522S17.256 21.522 12 21.522 2.478 17.256 2.478 12c0-.885.12-1.741.347-2.554a4.76 4.76 0 0 0 3.925 2.066 4.764 4.764 0 0 0 4.762-4.762 4.758 4.758 0 0 0-2.067-3.925A9.526 9.526 0 0 1 12 2.478Z"/></svg>

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M13.527.099C6.955-.744.942 3.9.099 10.473c-.843 6.572 3.8 12.584 10.373 13.428 6.573.843 12.587-3.801 13.428-10.374C24.744 6.955 20.101.943 13.527.099zm2.471 7.485a.855.855 0 0 0-.593.25l-4.453 4.453-.307-.307-.643-.643c4.389-4.376 5.18-4.418 5.996-3.753zm-4.863 4.861 4.44-4.44a.62.62 0 1 1 .847.903l-4.699 4.125-.588-.588zm.33.694-1.1.238a.06.06 0 0 1-.067-.032.06.06 0 0 1 .01-.073l.645-.645.512.512zm-2.803-.459 1.172-1.172.879.878-1.979.426a.074.074 0 0 1-.085-.039.072.072 0 0 1 .013-.093zm-3.646 6.058a.076.076 0 0 1-.069-.083.077.077 0 0 1 .022-.046h.002l.946-.946 1.222 1.222-2.123-.147zm2.425-1.256a.228.228 0 0 0-.117.256l.203.865a.125.125 0 0 1-.211.117h-.003l-.934-.934-.294-.295 3.762-3.758 1.82-.393.874.874c-1.255 1.102-2.971 2.201-5.1 3.268zm5.279-3.428h-.002l-.839-.839 4.699-4.125a.952.952 0 0 0 .119-.127c-.148 1.345-2.029 3.245-3.977 5.091zm3.657-6.46-.003-.002a1.822 1.822 0 0 1 2.459-2.684l-1.61 1.613a.119.119 0 0 0 0 .169l1.247 1.247a1.817 1.817 0 0 1-2.093-.343zm2.578 0a1.714 1.714 0 0 1-.271.218h-.001l-1.207-1.207 1.533-1.533c.661.72.637 1.832-.054 2.522zm-.1-1.544a.143.143 0 0 0-.053.157.416.416 0 0 1-.053.45.14.14 0 0 0 .023.197.141.141 0 0 0 .084.03.14.14 0 0 0 .106-.05.691.691 0 0 0 .087-.751.138.138 0 0 0-.194-.033z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
<path d="M9 12l2 2 4-4"></path>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>

After

Width:  |  Height:  |  Size: 342 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
<line x1="23" y1="11" x2="17" y2="11"></line>
</svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
<line x1="18" y1="8" x2="23" y2="13"></line>
<line x1="23" y1="8" x2="18" y2="13"></line>
</svg>

After

Width:  |  Height:  |  Size: 384 B

View File

@@ -1,7 +1,9 @@
*,
*::before,
*::after {
* {
@apply backface-hidden;
@apply before:backface-hidden;
@apply after:backface-hidden;
@apply selection:bg-accentDark;
@apply selection:text-accentContrast;
}
:root {
@@ -37,11 +39,6 @@
@apply hidden;
}
::selection {
@apply bg-accentDark;
@apply text-accentContrast;
}
input::placeholder,
textarea::placeholder {
@apply text-secondary;
@@ -64,10 +61,10 @@ body {
@apply font-medium;
@apply select-none;
@apply overflow-x-hidden;
@apply text-body;
@apply leading-body;
animation: fade 300ms forwards;
font-size: var(--body-font-size);
line-height: var(--body-line-height);
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
}
@@ -102,16 +99,18 @@ body {
.material-icons {
@apply flex-shrink-0;
@apply overflow-hidden;
font-size: var(--body-line-height) !important;
width: var(--body-line-height);
font-size: var(--line-height-body) !important;
width: var(--line-height-body);
}
.svg-icons {
@apply flex-shrink-0;
@apply overflow-hidden;
height: var(--body-line-height);
width: var(--body-line-height);
height: var(--line-height-body);
width: var(--line-height-body);
}
a {
@@ -120,9 +119,8 @@ a {
@apply no-underline;
@apply outline-none;
@apply transition;
font-size: var(--body-font-size);
line-height: var(--body-line-height);
@apply text-body;
@apply leading-body;
&.link {
@apply items-center;
@@ -136,44 +134,43 @@ a {
}
}
.tippy-popper {
.tooltip-theme {
@apply bg-tooltip;
@apply text-primary;
@apply font-semibold;
@apply py-1 px-2;
.tooltip-theme {
@apply bg-tooltip;
@apply text-primary;
@apply font-semibold;
@apply py-1 px-2;
@apply rounded;
@apply truncate;
@apply shadow;
@apply leading-body;
font-size: 86%;
kbd {
@apply hidden;
@apply sm:inline-flex;
@apply font-sans;
@apply bg-gray-500;
@apply bg-opacity-45;
@apply text-primaryLight;
@apply rounded-sm;
@apply px-1;
@apply ml-1;
@apply truncate;
@apply shadow;
font-size: 88%;
line-height: var(--body-line-height);
kbd {
@apply inline-flex;
@apply font-sans;
@apply bg-gray-500;
@apply bg-opacity-45;
@apply text-primaryLight;
@apply rounded-sm;
@apply px-1;
@apply ml-1;
@apply truncate;
}
}
}
.popover-theme {
@apply bg-popover;
@apply text-secondary;
@apply p-2;
@apply shadow-lg;
@apply focus:outline-none;
.popover-theme {
@apply bg-popover;
@apply text-secondary;
@apply p-2;
@apply shadow-lg;
@apply text-body;
@apply leading-body;
@apply focus:outline-none;
font-size: var(--body-font-size);
line-height: var(--body-line-height);
.tippy-roundarrow svg {
@apply fill-popover;
}
.tippy-roundarrow svg {
@apply fill-popover;
}
}
@@ -217,13 +214,12 @@ input,
select,
textarea,
button {
@apply focus:outline-none;
@apply truncate;
@apply transition;
@apply text-body;
@apply leading-body;
@apply focus:outline-none;
@apply disabled:cursor-not-allowed;
font-size: var(--body-font-size);
line-height: var(--body-line-height);
}
.input[type="file"],
@@ -233,7 +229,6 @@ button {
}
.floating-input ~ label {
@apply font-medium;
@apply py-0.5;
@apply px-2;
@apply m-2;
@@ -249,7 +244,7 @@ button {
@apply transform;
@apply origin-top-left;
@apply scale-75;
@apply -translate-y-5;
@apply -translate-y-4;
@apply translate-x-1;
}
@@ -324,9 +319,8 @@ pre.ace_editor {
@apply shadow;
@apply font-medium;
@apply transition;
font-size: var(--body-font-size);
line-height: var(--body-line-height);
@apply text-body;
@apply leading-body;
.action {
@apply bg-gray-500;
@@ -338,12 +332,11 @@ pre.ace_editor {
@apply rounded;
@apply text-current;
@apply normal-case;
@apply font-medium;
@apply text-body;
@apply leading-body;
@apply hover:bg-opacity-20;
@apply hover:no-underline;
@apply font-medium;
font-size: var(--body-font-size);
line-height: var(--body-line-height);
}
}
@@ -451,6 +444,25 @@ pre.ace_editor {
}
}
.shortcut-key {
@apply inline-flex;
@apply font-sans;
@apply text-tiny;
@apply bg-divider;
@apply rounded;
@apply ml-2;
@apply px-1;
@apply transition;
}
.capitalize-first {
@apply first-letter:capitalize;
}
details summary::-webkit-details-marker {
@apply hidden;
}
@media (max-width: 767px) {
main {
margin-bottom: env(safe-area-inset-bottom);

View File

@@ -2,6 +2,7 @@
--font-sans: "Inter", sans-serif;
--font-mono: "Roboto Mono", monospace;
--font-icon: "Material Icons";
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
}
@mixin dark-theme {
@@ -27,7 +28,7 @@
--secondary-color: theme("colors.true-gray.500");
--secondary-light-color: theme("colors.true-gray.400");
--secondary-dark-color: theme("colors.true-gray.900");
--divider-color: theme("colors.true-gray.200");
--divider-color: theme("colors.gray.100");
--divider-light-color: theme("colors.true-gray.100");
--divider-dark-color: theme("colors.true-gray.300");
--error-color: theme("colors.yellow.100");
@@ -44,11 +45,11 @@
--secondary-light-color: theme("colors.true-gray.500");
--secondary-dark-color: theme("colors.true-gray.100");
--divider-color: theme("colors.true-gray.800");
--divider-light-color: theme("colors.dark.700");
--divider-light-color: theme("colors.dark.800");
--divider-dark-color: theme("colors.dark.300");
--error-color: theme("colors.warm-gray.900");
--tooltip-color: theme("colors.true-gray.100");
--popover-color: theme("colors.dark.700");
--popover-color: theme("colors.dark.600");
--editor-theme: "twilight";
}
@@ -243,8 +244,8 @@
}
@mixin font-small {
--body-font-size: 0.75rem;
--body-line-height: 1rem;
--font-size-body: 0.75rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.125rem;
--upper-tertiary-sticky-fold: 8.188rem;
@@ -255,8 +256,8 @@
}
@mixin font-medium {
--body-font-size: 0.875rem;
--body-line-height: 1.25rem;
--font-size-body: 0.875rem;
--line-height-body: 1.25rem;
--upper-primary-sticky-fold: 4.375rem;
--upper-secondary-sticky-fold: 6.625rem;
--upper-tertiary-sticky-fold: 8.813rem;
@@ -267,8 +268,8 @@
}
@mixin font-large {
--body-font-size: 1rem;
--body-line-height: 1.5rem;
--font-size-body: 1rem;
--line-height-body: 1.5rem;
--upper-primary-sticky-fold: 4.625rem;
--upper-secondary-sticky-fold: 7.125rem;
--upper-tertiary-sticky-fold: 9.5rem;

View File

@@ -1,16 +1,14 @@
<template>
<div class="bg-error flex justify-between">
<span
class="flex py-2 px-4 transition justify-center group relative items-center"
>
<i class="mr-2 material-icons">info_outline</i>
<span class="text-secondaryDark">
<span class="md:hidden">
{{ t("helpers.offline_short") }}
</span>
<span class="hidden md:inline">
{{ t("helpers.offline") }}
</span>
<div
class="relative flex items-center px-4 py-2 transition bg-error text-tiny group"
>
<i class="mr-2 material-icons">info_outline</i>
<span class="text-secondaryDark">
<span class="md:hidden">
{{ t("helpers.offline_short") }}
</span>
<span class="<md:hidden">
{{ t("helpers.offline") }}
</span>
</span>
</div>

View File

@@ -22,97 +22,132 @@
}"
@click.native="ZEN_MODE = !ZEN_MODE"
/>
<tippy
ref="interceptorOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('settings.interceptor')"
svg="shield-check"
/>
</template>
<AppInterceptor />
</tippy>
</div>
<div class="flex">
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
arrow
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
svg="help-circle"
class="!rounded-none"
:label="`${t('app.help')}`"
/>
</template>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.d="documentation.$el.click()"
@keyup.s="shortcuts.$el.click()"
@keyup.c="chat.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<template #trigger>
<ButtonSecondary
svg="help-circle"
class="!rounded-none"
:label="`${t('app.help')}`"
/>
</template>
<div class="flex flex-col">
<SmartItem
svg="book"
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io"
blank
@click.native="$refs.options.tippy().hide()"
/>
<SmartItem
svg="zap"
:label="`${t('app.keyboard_shortcuts')}`"
@click.native="
() => {
showShortcuts = true
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="gift"
:label="`${t('app.whats_new')}`"
to="https://docs.hoppscotch.io/changelog"
blank
@click.native="$refs.options.tippy().hide()"
/>
<SmartItem
svg="message-circle"
:label="`${t('app.chat_with_us')}`"
@click.native="
() => {
chatWithUs()
$refs.options.tippy().hide()
}
"
/>
<hr />
<SmartItem
svg="github"
:label="`${t('app.github')}`"
to="https://github.com/hoppscotch/hoppscotch"
blank
@click.native="$refs.options.tippy().hide()"
/>
<SmartItem
svg="twitter"
:label="`${t('app.twitter')}`"
to="https://hoppscotch.io/twitter"
blank
@click.native="$refs.options.tippy().hide()"
/>
<SmartItem
svg="user-plus"
:label="`${t('app.invite')}`"
@click.native="
() => {
showShare = true
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="lock"
:label="`${t('app.terms_and_privacy')}`"
to="https://docs.hoppscotch.io/privacy"
blank
@click.native="$refs.options.tippy().hide()"
/>
<!-- <SmartItem :label="t('app.status')" /> -->
<div class="flex opacity-50 py-2 px-4">
{{ `${t("app.name")} ${t("app.version")}` }}
</div>
<SmartItem
ref="documentation"
svg="book"
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io"
blank
:shortcut="['D']"
@click.native="options.tippy().hide()"
/>
<SmartItem
ref="shortcuts"
svg="zap"
:label="`${t('app.keyboard_shortcuts')}`"
:shortcut="['S']"
@click.native="
() => {
showShortcuts = true
options.tippy().hide()
}
"
/>
<SmartItem
ref="chat"
svg="message-circle"
:label="`${t('app.chat_with_us')}`"
:shortcut="['C']"
@click.native="
() => {
chatWithUs()
options.tippy().hide()
}
"
/>
<SmartItem
svg="gift"
:label="`${t('app.whats_new')}`"
to="https://docs.hoppscotch.io/changelog"
blank
@click.native="options.tippy().hide()"
/>
<SmartItem
svg="activity"
:label="t('app.status')"
to="https://status.hoppscotch.io"
blank
@click.native="options.tippy().hide()"
/>
<hr />
<SmartItem
svg="github"
:label="`${t('app.github')}`"
to="https://github.com/hoppscotch/hoppscotch"
blank
@click.native="options.tippy().hide()"
/>
<SmartItem
svg="twitter"
:label="`${t('app.twitter')}`"
to="https://hoppscotch.io/twitter"
blank
@click.native="options.tippy().hide()"
/>
<SmartItem
svg="user-plus"
:label="`${t('app.invite')}`"
@click.native="
() => {
showShare = true
options.tippy().hide()
}
"
/>
<SmartItem
svg="lock"
:label="`${t('app.terms_and_privacy')}`"
to="https://docs.hoppscotch.io/privacy"
blank
@click.native="options.tippy().hide()"
/>
<div class="flex px-4 py-2 opacity-50">
{{ `${t("app.name")} v${$config.clientVersion}` }}
</div>
</tippy>
</span>
</div>
</tippy>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="zap"
@@ -135,7 +170,7 @@
@click.native="COLUMN_LAYOUT = !COLUMN_LAYOUT"
/>
<span
class="transform transition"
class="transition transform"
:class="{
'rotate-180': SIDEBAR_ON_LEFT,
}"
@@ -153,6 +188,7 @@
</div>
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
<AppShare :show="showShare" @hide-modal="showShare = false" />
<AppPowerSearch :show="showSearch" @hide-modal="showSearch = false" />
</div>
</template>
@@ -166,6 +202,7 @@ import { useI18n } from "~/helpers/utils/composables"
const t = useI18n()
const showShortcuts = ref(false)
const showShare = ref(false)
const showSearch = ref(false)
defineActionHandler("flyouts.keybinds.toggle", () => {
showShortcuts.value = !showShortcuts.value
@@ -175,6 +212,10 @@ defineActionHandler("modals.share.toggle", () => {
showShare.value = !showShare.value
})
defineActionHandler("modals.search.toggle", () => {
showSearch.value = !showSearch.value
})
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
const SIDEBAR = useSetting("SIDEBAR")
const ZEN_MODE = useSetting("ZEN_MODE")
@@ -208,4 +249,11 @@ const nativeShare = () => {
const chatWithUs = () => {
showChat()
}
// Template refs
const tippyActions = ref<any | null>(null)
const documentation = ref<any | null>(null)
const shortcuts = ref<any | null>(null)
const chat = ref<any | null>(null)
const options = ref<any | null>(null)
</script>

View File

@@ -11,10 +11,10 @@
/>
<div
v-if="searchResults.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<i class="opacity-75 pb-2 material-icons">manage_search</i>
<span class="text-center">
<i class="pb-2 opacity-75 material-icons">manage_search</i>
<span class="my-2 text-center">
{{ t("state.nothing_found") }} "{{ search }}"
</span>
</div>

View File

@@ -4,8 +4,8 @@
title="Star Hoppscotch"
href="https://github.com/hoppscotch/hoppscotch"
:data-color-scheme="
$colorMode.value != 'light'
? $colorMode.value == 'black'
colorMode.value != 'light'
? colorMode.value == 'black'
? 'dark'
: 'dark_dimmed'
: 'light'
@@ -20,6 +20,9 @@
<script setup lang="ts">
import GithubButton from "vue-github-button"
import { useColorMode } from "~/helpers/utils/composables"
const colorMode = useColorMode()
defineProps({
size: {

View File

@@ -1,17 +1,17 @@
<template>
<div>
<header
class="flex space-x-2 flex-1 py-2 px-2 items-center justify-between"
class="flex items-center justify-between flex-1 px-2 py-2 space-x-2"
>
<div class="space-x-2 inline-flex items-center">
<div class="inline-flex items-center space-x-2">
<ButtonSecondary
class="tracking-wide !font-bold !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
label="HOPPSCOTCH"
to="/"
/>
<AppGitHubStarButton class="mt-1.5 transition hidden sm:flex" />
<AppGitHubStarButton class="mt-1.5 transition <sm:hidden" />
</div>
<div class="space-x-2 inline-flex items-center">
<div class="inline-flex items-center space-x-2">
<ButtonSecondary
id="installPWA"
v-tippy="{ theme: 'tooltip' }"
@@ -25,7 +25,7 @@
:title="`${t('app.search')} <kbd>/</kbd>`"
svg="search"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click.native="showSearch = true"
@click.native="invokeAction('modals.search.toggle')"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@@ -47,7 +47,7 @@
:label="t('header.login')"
@click.native="showLogin = true"
/>
<div v-else class="space-x-2 inline-flex items-center">
<div v-else class="inline-flex items-center space-x-2">
<ButtonPrimary
v-tippy="{ theme: 'tooltip' }"
:title="t('team.invite_tooltip')"
@@ -57,7 +57,14 @@
@click.native="showTeamsModal = true"
/>
<span class="px-2">
<tippy ref="user" interactive trigger="click" theme="popover" arrow>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ProfilePicture
v-if="currentUser.photoURL"
@@ -78,19 +85,46 @@
svg="user"
/>
</template>
<SmartItem
to="/profile"
svg="user"
:label="t('navigation.profile')"
@click.native="$refs.user.tippy().hide()"
/>
<SmartItem
to="/settings"
svg="settings"
:label="t('navigation.settings')"
@click.native="$refs.user.tippy().hide()"
/>
<FirebaseLogout @confirm-logout="$refs.user.tippy().hide()" />
<div class="flex flex-col px-2 text-tiny">
<span class="inline-flex font-semibold truncate">
{{ currentUser.displayName }}
</span>
<span class="inline-flex truncate text-secondaryLight">
{{ currentUser.email }}
</span>
</div>
<hr />
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.enter="profile.$el.click()"
@keyup.s="settings.$el.click()"
@keyup.l="logout.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="profile"
to="/profile"
svg="user"
:label="t('navigation.profile')"
:shortcut="['↩']"
@click.native="options.tippy().hide()"
/>
<SmartItem
ref="settings"
to="/settings"
svg="settings"
:label="t('navigation.settings')"
:shortcut="['S']"
@click.native="options.tippy().hide()"
/>
<FirebaseLogout
ref="logout"
:shortcut="['L']"
@confirm-logout="options.tippy().hide()"
/>
</div>
</tippy>
</span>
</div>
@@ -99,7 +133,6 @@
<AppAnnouncement v-if="!isOnLine" />
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
<AppSupport :show="showSupport" @hide-modal="showSupport = false" />
<AppPowerSearch :show="showSearch" @hide-modal="showSearch = false" />
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
</div>
</template>
@@ -114,7 +147,7 @@ import {
useI18n,
useToast,
} from "~/helpers/utils/composables"
import { defineActionHandler } from "~/helpers/actions"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
const t = useI18n()
@@ -128,7 +161,6 @@ const toast = useToast()
const showInstallPrompt = ref(() => Promise.resolve()) // Async no-op till it is initialized
const showSupport = ref(false)
const showSearch = ref(false)
const showLogin = ref(false)
const showTeamsModal = ref(false)
@@ -139,9 +171,6 @@ const currentUser = useReadonlyStream(probableUser$, null)
defineActionHandler("modals.support.toggle", () => {
showSupport.value = !showSupport.value
})
defineActionHandler("modals.search.toggle", () => {
showSearch.value = !showSearch.value
})
onMounted(() => {
window.addEventListener("online", () => {
@@ -179,4 +208,11 @@ onMounted(() => {
})
}
})
// Template refs
const tippyActions = ref<any | null>(null)
const profile = ref<any | null>(null)
const settings = ref<any | null>(null)
const logout = ref<any | null>(null)
const options = ref<any | null>(null)
</script>

View File

@@ -1,58 +1,112 @@
<template>
<div class="flex flex-col space-y-4 p-4 items-start">
<SmartToggle
:on="PROXY_ENABLED"
@change="toggleSettingKey('PROXY_ENABLED')"
<div class="flex flex-col p-4 space-y-4">
<div class="flex flex-col">
<h2 class="inline-flex pb-1 font-semibold text-secondaryDark">
{{ t("settings.interceptor") }}
</h2>
<p class="inline-flex text-tiny">
{{ t("settings.interceptor_description") }}
</p>
</div>
<SmartRadioGroup
:radios="interceptors"
:selected="interceptorSelection"
@change="toggleSettingKey"
/>
<div
v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion"
class="flex space-x-2"
>
{{ t("settings.proxy") }}
</SmartToggle>
<SmartToggle
:on="EXTENSIONS_ENABLED"
@change="toggleSettingKey('EXTENSIONS_ENABLED')"
>
{{ t("settings.extensions") }}:
{{
extensionVersion != null
? `v${extensionVersion.major}.${extensionVersion.minor}`
: t("settings.extension_ver_not_reported")
}}
</SmartToggle>
<ButtonSecondary
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
blank
svg="brands/chrome"
label="Chrome"
outline
class="!flex-1"
/>
<ButtonSecondary
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
blank
svg="brands/firefox"
label="Firefox"
outline
class="!flex-1"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { computed, ref, watchEffect } from "@nuxtjs/composition-api"
import { KeysMatching } from "~/types/ts-utils"
import { SettingsType, toggleSetting, useSetting } from "~/newstore/settings"
import {
applySetting,
SettingsType,
toggleSetting,
useSetting,
} from "~/newstore/settings"
import { hasExtensionInstalled } from "~/helpers/strategies/ExtensionStrategy"
import { useI18n } from "~/helpers/utils/composables"
import { useI18n, usePolled } from "~/helpers/utils/composables"
const t = useI18n()
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
const toggleSettingKey = <K extends KeysMatching<SettingsType, boolean>>(
const toggleSettingKey = <
K extends KeysMatching<SettingsType | "BROWSER_ENABLED", boolean>
>(
key: K
) => {
if (key === "EXTENSIONS_ENABLED" && PROXY_ENABLED.value) {
toggleSetting("PROXY_ENABLED")
interceptorSelection.value = key
if (key === "EXTENSIONS_ENABLED") {
applySetting("EXTENSIONS_ENABLED", true)
if (PROXY_ENABLED.value) toggleSetting("PROXY_ENABLED")
}
if (key === "PROXY_ENABLED" && EXTENSIONS_ENABLED.value) {
toggleSetting("EXTENSIONS_ENABLED")
if (key === "PROXY_ENABLED") {
applySetting("PROXY_ENABLED", true)
if (EXTENSIONS_ENABLED.value) toggleSetting("EXTENSIONS_ENABLED")
}
if (key === "BROWSER_ENABLED") {
applySetting("PROXY_ENABLED", false)
applySetting("EXTENSIONS_ENABLED", false)
}
toggleSetting(key)
}
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
extensionVersion: hasExtensionInstalled()
? window.__POSTWOMAN_EXTENSION_HOOK__.getVersion()
: null,
}
const extensionVersion = usePolled(5000, (stopPolling) => {
const result = hasExtensionInstalled()
? window.__POSTWOMAN_EXTENSION_HOOK__.getVersion()
: null
// We don't need to poll anymore after we get value
if (result) stopPolling()
return result
})
const interceptors = computed(() => [
{ value: "BROWSER_ENABLED", label: t("state.none") },
{ value: "PROXY_ENABLED", label: t("settings.proxy") },
{
value: "EXTENSIONS_ENABLED",
label:
`${t("settings.extensions")}: ` +
(extensionVersion.value !== null
? `v${extensionVersion.value.major}.${extensionVersion.value.minor}`
: t("settings.extension_ver_not_reported")),
},
])
const interceptorSelection = ref("")
watchEffect(() => {
if (PROXY_ENABLED.value) {
interceptorSelection.value = "PROXY_ENABLED"
} else if (EXTENSIONS_ENABLED.value) {
interceptorSelection.value = "EXTENSIONS_ENABLED"
} else {
interceptorSelection.value = "BROWSER_ENABLED"
}
})
</script>

View File

@@ -6,16 +6,39 @@
@close="$emit('hide-modal')"
>
<template #body>
<input
id="command"
v-model="search"
v-focus
type="text"
autocomplete="off"
name="command"
:placeholder="`${t('app.type_a_command_search')}`"
class="bg-transparent border-b border-dividerLight flex flex-shrink-0 text-base text-secondaryDark p-6"
/>
<div class="flex transition flex-col border-b border-dividerLight">
<input
id="command"
v-model="search"
v-focus
type="text"
autocomplete="off"
name="command"
:placeholder="`${t('app.type_a_command_search')}`"
class="flex flex-shrink-0 p-6 text-base bg-transparent text-secondaryDark"
/>
<div
class="flex flex-shrink-0 text-tiny text-secondaryLight px-4 pb-4 justify-between whitespace-nowrap overflow-auto hide-scrollbar <sm:hidden"
>
<div class="flex items-center">
<kbd class="shortcut-key"></kbd>
<kbd class="shortcut-key"></kbd>
<span class="ml-2 truncate">
{{ t("action.to_navigate") }}
</span>
<kbd class="shortcut-key"></kbd>
<span class="ml-2 truncate">
{{ t("action.to_select") }}
</span>
</div>
<div class="flex items-center">
<kbd class="shortcut-key">ESC</kbd>
<span class="ml-2 truncate">
{{ t("action.to_close") }}
</span>
</div>
</div>
</div>
<AppFuse
v-if="search && show"
:input="fuse"
@@ -24,14 +47,14 @@
/>
<div
v-else
class="divide-dividerLight divide-y flex flex-col space-y-4 flex-1 overflow-auto hide-scrollbar"
class="flex flex-col flex-1 space-y-4 overflow-auto divide-y divide-dividerLight hide-scrollbar"
>
<div
v-for="(map, mapIndex) in mappings"
:key="`map-${mapIndex}`"
class="flex flex-col"
>
<h5 class="my-2 text-secondaryLight py-2 px-6">
<h5 class="px-6 py-2 my-2 text-secondaryLight">
{{ t(map.section) }}
</h5>
<AppPowerSearchEntry

View File

@@ -1,18 +1,18 @@
<template>
<button
class="cursor-pointer flex flex-1 py-2 px-6 transition items-center search-entry focus:outline-none"
class="flex items-center flex-1 px-6 py-3 font-medium transition cursor-pointer search-entry focus:outline-none"
:class="{ active: active }"
tabindex="-1"
@click="$emit('action', shortcut.action)"
@keydown.enter="$emit('action', shortcut.action)"
>
<SmartIcon
class="mr-4 opacity-50 transition svg-icons"
class="mr-4 transition opacity-50 svg-icons"
:class="{ 'opacity-100 text-secondaryDark': active }"
:name="shortcut.icon"
/>
<span
class="flex font-medium flex-1 mr-4 transition"
class="flex flex-1 mr-4 transition"
:class="{ 'text-secondaryDark': active }"
>
{{ t(shortcut.label) }}
@@ -33,7 +33,12 @@ import { useI18n } from "~/helpers/utils/composables"
const t = useI18n()
defineProps<{
shortcut: Object
shortcut: {
label: string
keys: string[]
action: string
icon: string
}
active: Boolean
}>()
</script>
@@ -64,13 +69,4 @@ defineProps<{
}
}
}
.shortcut-key {
@apply bg-dividerLight;
@apply rounded;
@apply ml-2;
@apply py-1;
@apply px-2;
@apply inline-flex;
}
</style>

View File

@@ -1,14 +0,0 @@
<template>
<section :id="label.toLowerCase()" class="flex flex-col flex-1 relative">
<slot></slot>
</section>
</template>
<script setup lang="ts">
defineProps({
label: {
type: String,
default: "Section",
},
})
</script>

View File

@@ -5,11 +5,11 @@
@close="hideModal"
>
<template #body>
<p class="text-secondaryLight mb-8 px-2">
<p class="px-2 mb-8 text-secondaryLight">
{{ t("app.invite_description") }}
</p>
<div class="flex flex-col space-y-2 px-2">
<div class="grid gap-4 grid-cols-3">
<div class="flex flex-col px-2 space-y-2">
<div class="grid grid-cols-3 gap-4">
<a
v-for="(platform, index) in platforms"
:key="`platform-${index}`"
@@ -17,13 +17,13 @@
target="_blank"
class="share-link"
>
<SmartIcon :name="platform.icon" class="h-6 w-6" />
<SmartIcon :name="platform.icon" class="w-6 h-6" />
<span class="mt-3">
{{ platform.name }}
</span>
</a>
<button class="share-link" @click="copyAppLink">
<SmartIcon class="h-6 text-xl w-6" :name="copyIcon" />
<SmartIcon class="w-6 h-6 text-xl" :name="copyIcon" />
<span class="mt-3">
{{ t("app.copy") }}
</span>
@@ -56,7 +56,7 @@ const text = "Hoppscotch - Open source API development ecosystem."
const description =
"Helps you create requests faster, saving precious time on development."
const subject = "Checkout Hoppscotch - an open source API development ecosystem"
const summary = `Hi there!%0D%0A%0D%0AI thought youll like this new platform that I joined called Hoppscotch - https://hoppscotch.io.%0D%0AIt is a simple and intuitive interface for creating and managing your APIs. You can build, test, document, and share your APIs.%0D%0A%0D%0AThe best part about Hoppscotch is that it is open source and free to get started.%0D%0A%0D%0A`
const summary = `Hi there!%0D%0A%0D%0AI thought you'll like this new platform that I joined called Hoppscotch - https://hoppscotch.io.%0D%0AIt is a simple and intuitive interface for creating and managing your APIs. You can build, test, document, and share your APIs.%0D%0A%0D%0AThe best part about Hoppscotch is that it is open source and free to get started.%0D%0A%0D%0A`
const twitter = "hoppscotch_io"
const copyIcon = ref("copy")

View File

@@ -1,33 +1,28 @@
<template>
<AppSlideOver :show="show" @close="close()">
<template #content>
<div
class="bg-primary border-b border-dividerLight flex p-2 top-0 z-10 sticky items-center justify-between"
>
<h3 class="ml-4 heading">{{ t("app.shortcuts") }}</h3>
<div class="flex">
<div class="sticky top-0 z-10 flex flex-col bg-primary">
<div
class="flex items-center justify-between p-2 border-b border-dividerLight"
>
<h3 class="ml-4 heading">{{ t("app.shortcuts") }}</h3>
<ButtonSecondary svg="x" @click.native="close()" />
</div>
</div>
<div class="bg-primary border-b border-dividerLight">
<div class="flex flex-col my-4 mx-6">
<div class="flex flex-col px-6 py-4 border-b border-dividerLight">
<input
v-model="filterText"
type="search"
autocomplete="off"
class="bg-primaryLight border border-dividerLight rounded flex w-full py-2 px-4 focus-visible:border-divider"
class="flex px-4 py-2 border rounded bg-primaryLight border-dividerLight focus-visible:border-divider"
:placeholder="`${t('action.search')}`"
/>
</div>
</div>
<div
v-if="filterText"
class="divide-dividerLight divide-y flex flex-col flex-1 overflow-auto hide-scrollbar"
>
<div v-if="filterText" class="flex flex-col divide-y divide-dividerLight">
<div
v-for="(map, mapIndex) in searchResults"
:key="`map-${mapIndex}`"
class="space-y-4 py-4 px-6"
class="px-6 py-4 space-y-4"
>
<h1 class="font-semibold text-secondaryDark">
{{ t(map.item.section) }}
@@ -40,22 +35,19 @@
</div>
<div
v-if="searchResults.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<i class="opacity-75 pb-2 material-icons">manage_search</i>
<span class="text-center">
<i class="pb-2 opacity-75 material-icons">manage_search</i>
<span class="my-2 text-center">
{{ t("state.nothing_found") }} "{{ filterText }}"
</span>
</div>
</div>
<div
v-else
class="divide-dividerLight divide-y flex flex-col flex-1 overflow-auto hide-scrollbar"
>
<div v-else class="flex flex-col divide-y divide-dividerLight">
<div
v-for="(map, mapIndex) in mappings"
:key="`map-${mapIndex}`"
class="space-y-4 py-4 px-6"
class="px-6 py-4 space-y-4"
>
<h1 class="font-semibold text-secondaryDark">
{{ t(map.section) }}
@@ -102,14 +94,3 @@ const close = () => {
emit("close")
}
</script>
<style lang="scss" scoped>
.shortcut-key {
@apply bg-dividerLight;
@apply rounded;
@apply ml-2;
@apply py-1;
@apply px-2;
@apply inline-flex;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex items-center">
<div class="flex items-center py-1">
<span class="flex flex-1 mr-4">
{{ t(shortcut.label) }}
</span>
@@ -19,17 +19,9 @@ import { useI18n } from "~/helpers/utils/composables"
const t = useI18n()
defineProps<{
shortcut: Object
shortcut: {
label: string
keys: string[]
}
}>()
</script>
<style lang="scss" scoped>
.shortcut-key {
@apply bg-dividerLight;
@apply rounded;
@apply ml-2;
@apply py-1;
@apply px-2;
@apply inline-flex;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<aside class="flex h-full justify-between md:flex-col">
<nav class="flex flex-nowrap flex-1 md:flex-col md:flex-none">
<aside class="flex justify-between h-full md:flex-col">
<nav class="flex flex-1 flex-nowrap md:flex-col md:flex-none">
<NuxtLink
v-for="(navigation, index) in primaryNavigation"
:key="`navigation-${index}`"
@@ -8,9 +8,6 @@
class="nav-link"
tabindex="0"
>
<i v-if="navigation.icon" class="material-icons">
{{ navigation.icon }}
</i>
<div v-if="navigation.svg">
<SmartIcon :name="navigation.svg" class="svg-icons" />
</div>
@@ -103,9 +100,7 @@ const primaryNavigation = [
span {
@apply mt-2;
@apply font-font-medium;
font-size: calc(var(--body-font-size) - 0.062rem);
@apply text-tiny;
}
&.exact-active-link {

View File

@@ -1,16 +1,16 @@
<template>
<div>
<transition v-if="show" name="fade" appear>
<div class="inset-0 transition-opacity z-20 fixed" @keydown.esc="close()">
<div class="fixed inset-0 z-20 transition-opacity" @keydown.esc="close()">
<div
class="bg-primaryLight opacity-90 inset-0 absolute"
class="absolute inset-0 bg-primaryLight opacity-90"
tabindex="0"
@click="close()"
></div>
</div>
</transition>
<aside
class="bg-primary flex flex-col h-full max-w-full transform transition top-0 ease-in-out right-0 w-96 z-30 duration-300 fixed overflow-auto"
class="fixed top-0 right-0 z-30 flex flex-col h-full max-w-full overflow-auto transition duration-300 ease-in-out transform bg-primary w-96 hide-scrollbar"
:class="show ? 'shadow-xl translate-x-0' : 'translate-x-full'"
>
<slot name="content"></slot>

View File

@@ -3,7 +3,7 @@
:to="`${/^\/(?!\/).*$/.test(to) ? localePath(to) : to}`"
:exact="exact"
:blank="blank"
class="font-bold py-2 transition inline-flex items-center justify-center focus:outline-none focus-visible:bg-accentDark"
class="inline-flex items-center justify-center py-2 font-bold transition focus:outline-none focus-visible:bg-accentDark"
:class="[
color
? `text-${color}-800 bg-${color}-200 hover:(text-${color}-900 bg-${color}-300) focus-visible:(text-${color}-900 bg-${color}-300)`
@@ -52,11 +52,11 @@
]"
/>
{{ label }}
<div v-if="shortcut.length" class="ml-2">
<div v-if="shortcut.length" class="ml-2 <sm:hidden">
<kbd
v-for="(key, index) in shortcut"
:key="`key-${index}`"
class="bg-accentLight rounded ml-1 px-1 inline-flex"
class="shortcut-key !bg-accentLight"
>
{{ key }}
</kbd>

View File

@@ -3,7 +3,7 @@
:to="`${/^\/(?!\/).*$/.test(to) ? localePath(to) : to}`"
:exact="exact"
:blank="blank"
class="font-semibold py-2 transition inline-flex items-center justify-center whitespace-nowrap focus:outline-none"
class="inline-flex items-center justify-center py-2 font-semibold transition whitespace-nowrap focus:outline-none"
:class="[
color
? `text-${color}-500 hover:text-${color}-600 focus-visible:text-${color}-600`
@@ -51,11 +51,11 @@
]"
/>
{{ label }}
<div v-if="shortcut.length" class="ml-2">
<div v-if="shortcut.length" class="ml-2 <sm:hidden">
<kbd
v-for="(key, index) in shortcut"
:key="`key-${index}`"
class="bg-dividerLight rounded text-secondaryLight ml-1 px-1 inline-flex"
class="shortcut-key"
>
{{ key }}
</kbd>

View File

@@ -1,60 +1,142 @@
<template>
<SmartModal
v-if="show"
:title="`${$t('modal.import_export')} ${$t('modal.collections')}`"
:title="`${t('modal.collections')}`"
max-width="sm:max-w-md"
@close="hideModal"
>
<template #actions>
<ButtonSecondary
v-if="mode == 'import_from_my_collections'"
v-if="importerType !== null"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.go_back')"
:title="t('action.go_back')"
svg="arrow-left"
@click.native="
() => {
mode = 'import_export'
mySelectedCollectionID = undefined
}
"
@click.native="resetImport"
/>
<span>
<tippy
v-if="
mode == 'import_export' && collectionsType.type == 'my-collections'
"
ref="options"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.more')"
svg="more-vertical"
</template>
<template #body>
<div v-if="importerType !== null" class="flex flex-col">
<div class="flex pb-6 flex-col px-2">
<div
v-for="(step, index) in importerSteps"
:key="`step-${index}`"
class="flex flex-col space-y-8"
>
<div v-if="step.name === 'FILE_IMPORT'" class="space-y-4">
<p class="flex items-center">
<span
class="inline-flex border-4 border-primary items-center justify-center flex-shrink-0 mr-4 rounded-full text-dividerDark"
:class="{
'!text-green-500': hasFile,
}"
>
<i class="material-icons">check_circle</i>
</span>
<span>
{{ t(`${step.metadata.caption}`) }}
</span>
</p>
<p class="flex flex-col ml-10">
<input
id="inputChooseFileToImportFrom"
ref="inputChooseFileToImportFrom"
name="inputChooseFileToImportFrom"
type="file"
class="transition cursor-pointer file:transition file:cursor-pointer text-secondary hover:text-secondaryDark file:mr-2 file:py-2 file:px-4 file:rounded file:border-0 file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
:accept="step.metadata.acceptedFileTypes"
@change="onFileChange"
/>
</p>
</div>
<div v-else-if="step.name === 'URL_IMPORT'" class="space-y-4">
<p class="flex items-center">
<span
class="inline-flex border-4 border-primary items-center justify-center flex-shrink-0 mr-4 rounded-full text-dividerDark"
:class="{
'!text-green-500': hasGist,
}"
>
<i class="material-icons">check_circle</i>
</span>
<span>
{{ t(`${step.metadata.caption}`) }}
</span>
</p>
<p class="flex flex-col ml-10">
<input
v-model="inputChooseGistToImportFrom"
type="url"
class="input"
:placeholder="`${$t('import.gist_url')}`"
/>
</p>
</div>
<div
v-else-if="step.name === 'TARGET_MY_COLLECTION'"
class="flex flex-col px-2"
>
<div class="select-wrapper">
<select
v-model="mySelectedCollectionID"
type="text"
autocomplete="off"
class="select"
autofocus
>
<option :key="undefined" :value="undefined" disabled selected>
{{ t("collection.select") }}
</option>
<option
v-for="(collection, collectionIndex) in myCollections"
:key="`collection-${collectionIndex}`"
:value="collectionIndex"
>
{{ collection.name }}
</option>
</select>
</div>
</div>
</div>
</div>
<ButtonPrimary
:label="t('import.title')"
:disabled="enableImportButton"
class="mx-2"
:loading="importingMyCollections"
@click.native="finishImport"
/>
</div>
<div v-else class="flex flex-col px-2">
<SmartExpand>
<template #body>
<SmartItem
v-for="(importer, index) in importerModules"
:key="`importer-${index}`"
:svg="importer.icon"
:label="t(`${importer.name}`)"
@click.native="importerType = index"
/>
</template>
</SmartExpand>
<hr />
<div class="flex flex-col space-y-2">
<SmartItem
icon="assignment_returned"
:label="$t('import.from_gist')"
@click.native="
() => {
readCollectionGist
$refs.options.tippy().hide()
}
"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
svg="download"
:label="t('export.as_json')"
@click.native="exportJSON"
/>
<span
v-tippy="{ theme: 'tooltip' }"
:title="
!currentUser
? $t('export.require_github')
? `${t('export.require_github')}`
: currentUser.provider !== 'github.com'
? $t('export.require_github')
: null
? `${t('export.require_github')}`
: undefined
"
class="flex"
>
<SmartItem
:disabled="
@@ -64,491 +146,276 @@
? true
: false
"
icon="assignment_turned_in"
:label="$t('export.create_secret_gist')"
svg="github"
:label="t('export.create_secret_gist')"
@click.native="
() => {
createCollectionGist()
$refs.options.tippy().hide()
}
"
/>
</span>
</tippy>
</span>
</template>
<template #body>
<div v-if="mode == 'import_export'" class="flex flex-col space-y-2">
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.replace_current')"
svg="file"
:label="$t('action.replace_json')"
@click.native="openDialogChooseFileToReplaceWith"
/>
<input
ref="inputChooseFileToReplaceWith"
class="input"
type="file"
style="display: none"
accept="application/json"
@change="replaceWithJSON"
/>
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.preserve_current')"
svg="folder-plus"
:label="$t('import.json')"
@click.native="openDialogChooseFileToImportFrom"
/>
<input
ref="inputChooseFileToImportFrom"
class="input"
type="file"
style="display: none"
accept="application/json"
@change="importFromJSON"
/>
<SmartItem
v-if="collectionsType.type == 'team-collections'"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.preserve_current')"
svg="user"
:label="$t('import.from_my_collections')"
@click.native="mode = 'import_from_my_collections'"
/>
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
svg="download"
:label="$t('export.as_json')"
@click.native="exportJSON"
/>
</div>
<div
v-if="mode == 'import_from_my_collections'"
class="flex flex-col px-2"
>
<div class="select-wrapper">
<select
type="text"
autocomplete="off"
class="select"
autofocus
@change="
($event) => {
mySelectedCollectionID = $event.target.value
}
"
>
<option
:key="undefined"
:value="undefined"
hidden
disabled
selected
>
Select Collection
</option>
<option
v-for="(collection, index) in myCollections"
:key="`collection-${index}`"
:value="index"
>
{{ collection.name }}
</option>
</select>
</div>
</div>
</template>
<template #footer>
<div v-if="mode == 'import_from_my_collections'">
<span>
<ButtonPrimary
:disabled="mySelectedCollectionID == undefined"
svg="folder-plus"
:label="$t('import.title')"
@click.native="importFromMyCollections"
/>
</span>
</div>
</template>
</SmartModal>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { computed, ref, watch } from "@nuxtjs/composition-api"
import * as E from "fp-ts/Either"
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
import { apolloClient } from "~/helpers/apollo"
import {
useAxios,
useI18n,
useReadonlyStream,
useToast,
} from "~/helpers/utils/composables"
import { currentUser$ } from "~/helpers/fb/auth"
import * as teamUtils from "~/helpers/teams/utils"
import { useReadonlyStream } from "~/helpers/utils/composables"
import {
restCollections$,
setRESTCollections,
appendRESTCollections,
} from "~/newstore/collections"
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
import { StepReturnValue } from "~/helpers/import-export/steps"
export default defineComponent({
props: {
show: Boolean,
collectionsType: { type: Object, default: () => {} },
},
setup() {
return {
myCollections: useReadonlyStream(restCollections$, []),
currentUser: useReadonlyStream(currentUser$, null),
}
},
data() {
return {
showJsonCode: false,
mode: "import_export",
mySelectedCollectionID: undefined,
collectionJson: "",
}
},
methods: {
async createCollectionGist() {
this.getJSONCollection()
await this.$axios
.$post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-collections.json": {
content: this.collectionJson,
},
},
const props = defineProps<{
show: boolean
collectionsType:
| {
type: "team-collections"
selectedTeam: {
id: string
}
}
| { type: "my-collections" }
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
(e: "update-team-collections"): void
}>()
const axios = useAxios()
const toast = useToast()
const t = useI18n()
const myCollections = useReadonlyStream(restCollections$, [])
const currentUser = useReadonlyStream(currentUser$, null)
// Template refs
const mode = ref("import_export")
const mySelectedCollectionID = ref<undefined | number>(undefined)
const collectionJson = ref("")
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
const inputChooseGistToImportFrom = ref<string>("")
const getJSONCollection = async () => {
if (props.collectionsType.type === "my-collections") {
collectionJson.value = JSON.stringify(myCollections.value, null, 2)
} else {
collectionJson.value = await teamUtils.exportAsJSON(
apolloClient,
props.collectionsType.selectedTeam.id
)
}
return collectionJson.value
}
const createCollectionGist = async () => {
if (!currentUser.value) {
toast.error(t("profile.no_permission").toString())
return
}
getJSONCollection()
try {
const res = await axios.$post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-collections.json": {
content: collectionJson.value,
},
{
headers: {
Authorization: `token ${this.currentUser.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
)
.then((res) => {
this.$toast.success(this.$t("export.gist_created"))
window.open(res.html_url)
})
.catch((e) => {
this.$toast.error(this.$t("error.something_went_wrong"))
console.error(e)
})
},
async readCollectionGist() {
const gist = prompt(this.$t("import.gist_url"))
if (!gist) return
await this.$axios
.$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
headers: {
Accept: "application/vnd.github.v3+json",
},
})
.then(({ files }) => {
const collections = JSON.parse(Object.values(files)[0].content)
setRESTCollections(collections)
this.fileImported()
})
.catch((e) => {
this.failedImport()
console.error(e)
})
},
hideModal() {
this.mode = "import_export"
this.mySelectedCollectionID = undefined
this.$emit("hide-modal")
},
openDialogChooseFileToReplaceWith() {
this.$refs.inputChooseFileToReplaceWith.click()
},
openDialogChooseFileToImportFrom() {
this.$refs.inputChooseFileToImportFrom.click()
},
replaceWithJSON() {
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target.result
let collections = JSON.parse(content)
if (collections[0]) {
const [name, folders, requests] = Object.keys(collections[0])
if (
name === "name" &&
folders === "folders" &&
requests === "requests"
) {
// Do nothing
}
} else if (
collections.info &&
collections.info.schema.includes("v2.1.0")
) {
collections = [this.parsePostmanCollection(collections)]
} else {
this.failedImport()
}
if (this.collectionsType.type === "team-collections") {
teamUtils
.replaceWithJSON(
this.$apollo,
collections,
this.collectionsType.selectedTeam.id
)
.then((status) => {
if (status) {
this.fileImported()
} else {
this.failedImport()
}
})
.catch((e) => {
console.error(e)
this.failedImport()
})
} else {
setRESTCollections(collections)
this.fileImported()
}
}
reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0])
this.$refs.inputChooseFileToReplaceWith.value = ""
},
importFromJSON() {
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target.result
let collections = JSON.parse(content)
if (collections[0]) {
const [name, folders, requests] = Object.keys(collections[0])
if (
name === "name" &&
folders === "folders" &&
requests === "requests"
) {
// Do nothing
}
} else if (
collections.info &&
collections.info.schema.includes("v2.1.0")
) {
// replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
collections = JSON.parse(
content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>")
)
collections = [this.parsePostmanCollection(collections)]
} else {
this.failedImport()
return
}
if (this.collectionsType.type === "team-collections") {
teamUtils
.importFromJSON(
this.$apollo,
collections,
this.collectionsType.selectedTeam.id
)
.then((status) => {
if (status) {
this.$emit("update-team-collections")
this.fileImported()
} else {
this.failedImport()
}
})
.catch((e) => {
console.error(e)
this.failedImport()
})
} else {
appendRESTCollections(collections)
this.fileImported()
}
}
reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0])
this.$refs.inputChooseFileToImportFrom.value = ""
},
importFromMyCollections() {
teamUtils
.importFromMyCollections(
this.$apollo,
this.mySelectedCollectionID,
this.collectionsType.selectedTeam.id
)
.then((success) => {
if (success) {
this.fileImported()
this.$emit("update-team-collections")
} else {
this.failedImport()
}
})
.catch((e) => {
console.error(e)
this.failedImport()
})
},
async getJSONCollection() {
if (this.collectionsType.type === "my-collections") {
this.collectionJson = JSON.stringify(this.myCollections, null, 2)
} else {
this.collectionJson = await teamUtils.exportAsJSON(
this.$apollo,
this.collectionsType.selectedTeam.id
)
}
return this.collectionJson
},
exportJSON() {
this.getJSONCollection()
const dataToWrite = this.collectionJson
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
this.$toast.success(this.$t("state.download_started"))
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
},
fileImported() {
this.$toast.success(this.$t("state.file_imported"))
},
failedImport() {
this.$toast.error(this.$t("import.failed"))
},
parsePostmanCollection({ info, name, item }) {
const hoppscotchCollection = {
name: "",
folders: [],
requests: [],
},
},
{
headers: {
Authorization: `token ${currentUser.value.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
)
hoppscotchCollection.name = info ? info.name : name
toast.success(t("export.gist_created").toString())
window.open(res.html_url)
} catch (e) {
toast.error(t("error.something_went_wrong").toString())
console.error(e)
}
}
if (item && item.length > 0) {
for (const collectionItem of item) {
if (collectionItem.request) {
if (
Object.prototype.hasOwnProperty.call(
hoppscotchCollection,
"folders"
)
) {
hoppscotchCollection.name = info ? info.name : name
hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
} else {
hoppscotchCollection.name = name || ""
hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
}
} else if (this.hasFolder(collectionItem)) {
hoppscotchCollection.folders.push(
this.parsePostmanCollection(collectionItem)
)
} else {
hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
}
}
}
return hoppscotchCollection
},
parsePostmanRequest({ name, request }) {
const pwRequest = {
url: "",
path: "",
method: "",
auth: "",
httpUser: "",
httpPassword: "",
passwordFieldType: "password",
bearerToken: "",
headers: [],
params: [],
bodyParams: [],
rawParams: "",
rawInput: false,
contentType: "",
requestType: "",
name: "",
}
const fileImported = () => {
toast.success(t("state.file_imported").toString())
hideModal()
}
pwRequest.name = name
if (request.url) {
const requestObjectUrl = request.url.raw.match(
/^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/
)
if (requestObjectUrl) {
pwRequest.url = requestObjectUrl[1]
pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
}
}
pwRequest.method = request.method
const itemAuth = request.auth ? request.auth : ""
const authType = itemAuth ? itemAuth.type : ""
if (authType === "basic") {
pwRequest.auth = "Basic Auth"
pwRequest.httpUser =
itemAuth.basic[0].key === "username"
? itemAuth.basic[0].value
: itemAuth.basic[1].value
pwRequest.httpPassword =
itemAuth.basic[0].key === "password"
? itemAuth.basic[0].value
: itemAuth.basic[1].value
} else if (authType === "oauth2") {
pwRequest.auth = "OAuth 2.0"
pwRequest.bearerToken =
itemAuth.oauth2[0].key === "accessToken"
? itemAuth.oauth2[0].value
: itemAuth.oauth2[1].value
} else if (authType === "bearer") {
pwRequest.auth = "Bearer Token"
pwRequest.bearerToken = itemAuth.bearer[0].value
}
const requestObjectHeaders = request.header
if (requestObjectHeaders) {
pwRequest.headers = requestObjectHeaders
for (const header of pwRequest.headers) {
delete header.name
delete header.type
}
}
if (request.url) {
const requestObjectParams = request.url.query
if (requestObjectParams) {
pwRequest.params = requestObjectParams
for (const param of pwRequest.params) {
delete param.disabled
}
}
}
if (request.body) {
if (request.body.mode === "urlencoded") {
const params = request.body.urlencoded
pwRequest.bodyParams = params || []
for (const param of pwRequest.bodyParams) {
delete param.type
}
} else if (request.body.mode === "raw") {
pwRequest.rawInput = true
pwRequest.rawParams = request.body.raw
}
}
return pwRequest
},
hasFolder(item) {
return Object.prototype.hasOwnProperty.call(item, "item")
},
},
const failedImport = () => {
toast.error(t("import.failed").toString())
}
const hideModal = () => {
mode.value = "import_export"
mySelectedCollectionID.value = undefined
resetImport()
emit("hide-modal")
}
const stepResults = ref<StepReturnValue[]>([])
watch(mySelectedCollectionID, (newValue) => {
if (newValue === undefined) return
stepResults.value = []
stepResults.value.push(newValue)
})
const importingMyCollections = ref(false)
const importToTeams = async (content: HoppCollection<HoppRESTRequest>) => {
importingMyCollections.value = true
if (props.collectionsType.type !== "team-collections") return
await teamUtils
.importFromJSON(
apolloClient,
content,
props.collectionsType.selectedTeam.id
)
.then((status) => {
if (status) {
emit("update-team-collections")
} else {
console.error(status)
}
})
.catch((e) => {
console.error(e)
})
.finally(() => {
importingMyCollections.value = false
})
}
const exportJSON = () => {
getJSONCollection()
const dataToWrite = collectionJson.value
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO: get uri from meta
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
}
const importerModules = computed(() =>
RESTCollectionImporters.filter(
(i) => i.applicableTo?.includes(props.collectionsType.type) ?? true
)
)
const importerType = ref<number | null>(null)
const importerModule = computed(() =>
importerType.value !== null ? importerModules.value[importerType.value] : null
)
const importerSteps = computed(() => importerModule.value?.steps ?? null)
const finishImport = async () => {
await importerAction(stepResults.value)
}
const importerAction = async (stepResults: any[]) => {
if (!importerModule.value) return
const result = await importerModule.value?.importer(stepResults as any)()
if (E.isLeft(result)) {
failedImport()
console.error("error", result.left)
} else if (E.isRight(result)) {
if (props.collectionsType.type === "team-collections") {
importToTeams(result.right)
fileImported()
} else {
appendRESTCollections(result.right)
fileImported()
}
}
}
const hasFile = ref(false)
const hasGist = ref(false)
watch(inputChooseGistToImportFrom, (v) => {
stepResults.value = []
if (v === "") {
hasGist.value = false
} else {
hasGist.value = true
stepResults.value.push(inputChooseGistToImportFrom.value)
}
})
const onFileChange = () => {
stepResults.value = []
if (!inputChooseFileToImportFrom.value[0]) {
hasFile.value = false
return
}
if (
!inputChooseFileToImportFrom.value[0].files ||
inputChooseFileToImportFrom.value[0].files.length === 0
) {
inputChooseFileToImportFrom.value[0].value = ""
hasFile.value = false
toast.show(t("action.choose_file").toString())
return
}
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target!.result as string | null
if (!content) {
hasFile.value = false
toast.show(t("action.choose_file").toString())
return
}
stepResults.value.push(content)
hasFile.value = !!content?.length
}
reader.readAsText(inputChooseFileToImportFrom.value[0].files[0])
}
const enableImportButton = computed(
() => !(stepResults.value.length === importerSteps.value?.length)
)
const resetImport = () => {
importerType.value = null
stepResults.value = []
inputChooseFileToImportFrom.value = ""
hasFile.value = false
inputChooseGistToImportFrom.value = ""
hasGist.value = false
mySelectedCollectionID.value = undefined
}
</script>

View File

@@ -34,8 +34,8 @@
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { HoppGQLRequest } from "@hoppscotch/data"
import { addGraphqlCollection, makeCollection } from "~/newstore/collections"
import { HoppGQLRequest, makeCollection } from "@hoppscotch/data"
import { addGraphqlCollection } from "~/newstore/collections"
export default defineComponent({
props: {

View File

@@ -8,7 +8,7 @@
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="$refs.options.tippy().show()"
@contextmenu.prevent="options.tippy().show()"
>
<span
class="cursor-pointer flex px-4 items-center justify-center"
@@ -16,7 +16,7 @@
>
<SmartIcon
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:class="{ 'text-accent': isSelected }"
:name="getCollectionIcon"
/>
</span>
@@ -24,7 +24,9 @@
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate"> {{ collection.name }} </span>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ collection.name }}
</span>
</span>
<div class="flex">
<ButtonSecondary
@@ -45,6 +47,7 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
@@ -53,39 +56,54 @@
svg="more-vertical"
/>
</template>
<SmartItem
svg="folder-plus"
:label="`${$t('folder.new')}`"
@click.native="
() => {
$emit('add-folder', {
path: `${collectionIndex}`,
})
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="edit"
:label="`${$t('action.edit')}`"
@click.native="
() => {
$emit('edit-collection')
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="trash-2"
color="red"
:label="`${$t('action.delete')}`"
@click.native="
() => {
confirmRemove = true
$refs.options.tippy().hide()
}
"
/>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="folderAction"
svg="folder-plus"
:label="`${$t('folder.new')}`"
:shortcut="['N']"
@click.native="
() => {
$emit('add-folder', {
path: `${collectionIndex}`,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="edit"
svg="edit"
:label="`${$t('action.edit')}`"
:shortcut="['E']"
@click.native="
() => {
$emit('edit-collection')
options.tippy().hide()
}
"
/>
<SmartItem
ref="deleteAction"
svg="trash-2"
:label="`${$t('action.delete')}`"
:shortcut="['⌫']"
@click.native="
() => {
confirmRemove = true
options.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
</div>
@@ -139,7 +157,7 @@
:src="`/images/states/${$colorMode.value}/pack.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 mb-4 w-16 inline-flex"
:alt="$t('empty.collection')"
:alt="`${$t('empty.collection')}`"
/>
<span class="text-center">
{{ $t("empty.collection") }}
@@ -157,7 +175,7 @@
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { defineComponent, ref } from "@nuxtjs/composition-api"
import {
removeGraphqlCollection,
moveGraphqlRequest,
@@ -173,6 +191,15 @@ export default defineComponent({
doc: Boolean,
isFiltered: Boolean,
},
setup() {
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
}
},
data() {
return {
showChildren: false,
@@ -226,10 +253,8 @@ export default defineComponent({
},
dropEvent({ dataTransfer }: any) {
this.dragging = !this.dragging
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
moveGraphqlRequest(folderPath, requestIndex, `${this.collectionIndex}`)
},
},

View File

@@ -8,7 +8,7 @@
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="$refs.options.tippy().show()"
@contextmenu.prevent="options.tippy().show()"
>
<span
class="cursor-pointer flex px-4 items-center justify-center"
@@ -16,7 +16,7 @@
>
<SmartIcon
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:class="{ 'text-accent': isSelected }"
:name="getCollectionIcon"
/>
</span>
@@ -24,7 +24,7 @@
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate">
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ folder.name ? folder.name : folder.title }}
</span>
</span>
@@ -43,6 +43,7 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
@@ -51,37 +52,52 @@
svg="more-vertical"
/>
</template>
<SmartItem
svg="folder-plus"
:label="`${$t('folder.new')}`"
@click.native="
() => {
$emit('add-folder', { folder, path: folderPath })
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="edit"
:label="`${$t('action.edit')}`"
@click.native="
() => {
$emit('edit-folder', { folder, folderPath })
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="trash-2"
color="red"
:label="`${$t('action.delete')}`"
@click.native="
() => {
confirmRemove = true
$refs.options.tippy().hide()
}
"
/>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="folderAction"
svg="folder-plus"
:label="`${$t('folder.new')}`"
:shortcut="['N']"
@click.native="
() => {
$emit('add-folder', { folder, path: folderPath })
options.tippy().hide()
}
"
/>
<SmartItem
ref="edit"
svg="edit"
:label="`${$t('action.edit')}`"
:shortcut="['E']"
@click.native="
() => {
$emit('edit-folder', { folder, folderPath })
options.tippy().hide()
}
"
/>
<SmartItem
ref="deleteAction"
svg="trash-2"
:label="`${$t('action.delete')}`"
:shortcut="['⌫']"
@click.native="
() => {
confirmRemove = true
options.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
</div>
@@ -138,7 +154,7 @@
:src="`/images/states/${$colorMode.value}/pack.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 mb-4 w-16 inline-flex"
:alt="$t('empty.folder')"
:alt="`${$t('empty.folder')}`"
/>
<span class="text-center">
{{ $t("empty.folder") }}
@@ -156,7 +172,7 @@
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { defineComponent, ref } from "@nuxtjs/composition-api"
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
export default defineComponent({
@@ -172,6 +188,15 @@ export default defineComponent({
doc: Boolean,
isFiltered: Boolean,
},
setup() {
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
}
},
data() {
return {
showChildren: false,
@@ -227,7 +252,6 @@ export default defineComponent({
this.dragging = !this.dragging
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
moveGraphqlRequest(folderPath, requestIndex, this.folderPath)
},
},

View File

@@ -1,7 +1,7 @@
<template>
<SmartModal
v-if="show"
:title="`${$t('modal.import_export')} ${$t('modal.collections')}`"
:title="`${t('modal.collections')}`"
max-width="sm:max-w-md"
@close="hideModal"
>
@@ -11,26 +11,28 @@
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.more')"
:title="t('action.more')"
svg="more-vertical"
/>
</template>
<SmartItem
icon="assignment_returned"
:label="$t('import.from_gist')"
:label="t('import.from_gist')"
@click.native="
readCollectionGist
$refs.options.tippy().hide()
() => {
readCollectionGist()
options.tippy().hide()
}
"
/>
<span
v-tippy="{ theme: 'tooltip' }"
:title="
!currentUser
? $t('export.require_github')
? `${t('export.require_github')}`
: currentUser.provider !== 'github.com'
? $t('export.require_github')
: null
? `${t('export.require_github')}`
: undefined
"
>
<SmartItem
@@ -42,10 +44,12 @@
: false
"
icon="assignment_turned_in"
:label="$t('export.create_secret_gist')"
:label="t('export.create_secret_gist')"
@click.native="
createCollectionGist()
$refs.options.tippy().hide()
() => {
createCollectionGist()
options.tippy().hide()
}
"
/>
</span>
@@ -53,26 +57,10 @@
</span>
</template>
<template #body>
<div class="flex flex-col space-y-2">
<div class="flex flex-col space-y-2 px-2">
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.replace_current')"
svg="file"
:label="$t('action.replace_json')"
@click.native="openDialogChooseFileToReplaceWith"
/>
<input
ref="inputChooseFileToReplaceWith"
class="input"
type="file"
accept="application/json"
@change="replaceWithJSON"
/>
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.preserve_current')"
svg="folder-plus"
:label="$t('import.json')"
:label="t('import.from_json')"
@click.native="openDialogChooseFileToImportFrom"
/>
<input
@@ -82,11 +70,12 @@
accept="application/json"
@change="importFromJSON"
/>
<hr />
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
:title="t('action.download_file')"
svg="download"
:label="$t('export.as_json')"
:label="t('export.as_json')"
@click.native="exportJSON"
/>
</div>
@@ -94,295 +83,175 @@
</SmartModal>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { computed, ref } from "@nuxtjs/composition-api"
import { currentUser$ } from "~/helpers/fb/auth"
import { useReadonlyStream } from "~/helpers/utils/composables"
import {
useAxios,
useI18n,
useReadonlyStream,
useToast,
} from "~/helpers/utils/composables"
import {
graphqlCollections$,
setGraphqlCollections,
appendGraphqlCollections,
} from "~/newstore/collections"
export default defineComponent({
props: {
show: Boolean,
},
setup() {
return {
collections: useReadonlyStream(graphqlCollections$, []),
currentUser: useReadonlyStream(currentUser$, null),
}
},
computed: {
collectionJson() {
return JSON.stringify(this.collections, null, 2)
},
},
methods: {
async createCollectionGist() {
await this.$axios
.$post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-collections.json": {
content: this.collectionJson,
},
},
},
{
headers: {
Authorization: `token ${this.currentUser.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
)
.then((res) => {
this.$toast.success(this.$t("export.gist_created"))
window.open(res.html_url)
})
.catch((e) => {
this.$toast.error(this.$t("error.something_went_wrong"))
console.error(e)
})
},
async readCollectionGist() {
const gist = prompt(this.$t("import.gist_url"))
if (!gist) return
await this.$axios
.$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
headers: {
Accept: "application/vnd.github.v3+json",
},
})
.then(({ files }) => {
const collections = JSON.parse(Object.values(files)[0].content)
setGraphqlCollections(collections)
this.fileImported()
})
.catch((e) => {
this.failedImport()
console.error(e)
})
},
hideModal() {
this.$emit("hide-modal")
},
openDialogChooseFileToReplaceWith() {
this.$refs.inputChooseFileToReplaceWith.click()
},
openDialogChooseFileToImportFrom() {
this.$refs.inputChooseFileToImportFrom.click()
},
replaceWithJSON() {
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target.result
let collections = JSON.parse(content)
if (collections[0]) {
const [name, folders, requests] = Object.keys(collections[0])
if (
name === "name" &&
folders === "folders" &&
requests === "requests"
) {
// Do nothing
}
} else if (
collections.info &&
collections.info.schema.includes("v2.1.0")
) {
collections = [this.parsePostmanCollection(collections)]
} else {
this.failedImport()
return
}
setGraphqlCollections(collections)
this.fileImported()
}
reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0])
this.$refs.inputChooseFileToReplaceWith.value = ""
},
importFromJSON() {
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target.result
let collections = JSON.parse(content)
if (collections[0]) {
const [name, folders, requests] = Object.keys(collections[0])
if (
name === "name" &&
folders === "folders" &&
requests === "requests"
) {
// Do nothing
}
} else if (
collections.info &&
collections.info.schema.includes("v2.1.0")
) {
// replace the variables, postman uses {{var}}, Hoppscotch uses <<var>>
collections = JSON.parse(
content.replaceAll(/{{([a-z]+)}}/gi, "<<$1>>")
)
collections = [this.parsePostmanCollection(collections)]
} else {
this.failedImport()
return
}
appendGraphqlCollections(collections)
this.fileImported()
}
reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0])
this.$refs.inputChooseFileToImportFrom.value = ""
},
exportJSON() {
const dataToWrite = this.collectionJson
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
this.$toast.success(this.$t("state.download_started"))
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
},
fileImported() {
this.$toast.success(this.$t("state.file_imported"))
},
failedImport() {
this.$toast.error(this.$t("import.failed"))
},
parsePostmanCollection({ info, name, item }) {
const hoppscotchCollection = {
name: "",
folders: [],
requests: [],
}
defineProps<{
show: boolean
}>()
hoppscotchCollection.name = info ? info.name : name
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
if (item && item.length > 0) {
for (const collectionItem of item) {
if (collectionItem.request) {
if (
Object.prototype.hasOwnProperty.call(
hoppscotchCollection,
"folders"
)
) {
hoppscotchCollection.name = info ? info.name : name
hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
} else {
hoppscotchCollection.name = name || ""
hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
}
} else if (this.hasFolder(collectionItem)) {
hoppscotchCollection.folders.push(
this.parsePostmanCollection(collectionItem)
)
} else {
hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem)
)
}
}
}
return hoppscotchCollection
},
parsePostmanRequest({ name, request }) {
const pwRequest = {
url: "",
path: "",
method: "",
auth: "",
httpUser: "",
httpPassword: "",
passwordFieldType: "password",
bearerToken: "",
headers: [],
params: [],
bodyParams: [],
rawParams: "",
rawInput: false,
contentType: "",
requestType: "",
name: "",
}
const axios = useAxios()
const toast = useToast()
const t = useI18n()
const collections = useReadonlyStream(graphqlCollections$, [])
const currentUser = useReadonlyStream(currentUser$, null)
pwRequest.name = name
const requestObjectUrl = request.url.raw.match(
/^(.+:\/\/[^/]+|{[^/]+})(\/[^?]+|).*$/
)
if (requestObjectUrl) {
pwRequest.url = requestObjectUrl[1]
pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""
}
pwRequest.method = request.method
const itemAuth = request.auth ? request.auth : ""
const authType = itemAuth ? itemAuth.type : ""
if (authType === "basic") {
pwRequest.auth = "Basic Auth"
pwRequest.httpUser =
itemAuth.basic[0].key === "username"
? itemAuth.basic[0].value
: itemAuth.basic[1].value
pwRequest.httpPassword =
itemAuth.basic[0].key === "password"
? itemAuth.basic[0].value
: itemAuth.basic[1].value
} else if (authType === "oauth2") {
pwRequest.auth = "OAuth 2.0"
pwRequest.bearerToken =
itemAuth.oauth2[0].key === "accessToken"
? itemAuth.oauth2[0].value
: itemAuth.oauth2[1].value
} else if (authType === "bearer") {
pwRequest.auth = "Bearer Token"
pwRequest.bearerToken = itemAuth.bearer[0].value
}
const requestObjectHeaders = request.header
if (requestObjectHeaders) {
pwRequest.headers = requestObjectHeaders
for (const header of pwRequest.headers) {
delete header.name
delete header.type
}
}
const requestObjectParams = request.url.query
if (requestObjectParams) {
pwRequest.params = requestObjectParams
for (const param of pwRequest.params) {
delete param.disabled
}
}
if (request.body) {
if (request.body.mode === "urlencoded") {
const params = request.body.urlencoded
pwRequest.bodyParams = params || []
for (const param of pwRequest.bodyParams) {
delete param.type
}
} else if (request.body.mode === "raw") {
pwRequest.rawInput = true
pwRequest.rawParams = request.body.raw
}
}
return pwRequest
},
hasFolder(item) {
return Object.prototype.hasOwnProperty.call(item, "item")
},
},
// Template refs
const options = ref<any>()
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
const collectionJson = computed(() => {
return JSON.stringify(collections.value, null, 2)
})
const createCollectionGist = async () => {
if (!currentUser.value) {
toast.error(t("profile.no_permission").toString())
return
}
try {
const res = await axios.$post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-collections.json": {
content: collectionJson.value,
},
},
},
{
headers: {
Authorization: `token ${currentUser.value.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
)
toast.success(t("export.gist_created").toString())
window.open(res.html_url)
} catch (e) {
toast.error(t("error.something_went_wrong").toString())
console.error(e)
}
}
const fileImported = () => {
toast.success(t("state.file_imported").toString())
}
const failedImport = () => {
toast.error(t("import.failed").toString())
}
const readCollectionGist = async () => {
const gist = prompt(t("import.gist_url").toString())
if (!gist) return
try {
const { files } = (await axios.$get(
`https://api.github.com/gists/${gist.split("/").pop()}`,
{
headers: {
Accept: "application/vnd.github.v3+json",
},
}
)) as {
files: {
[fileName: string]: {
content: any
}
}
}
const collections = JSON.parse(Object.values(files)[0].content)
setGraphqlCollections(collections)
fileImported()
} catch (e) {
failedImport()
console.error(e)
}
}
const hideModal = () => {
emit("hide-modal")
}
const openDialogChooseFileToImportFrom = () => {
if (inputChooseFileToImportFrom.value)
inputChooseFileToImportFrom.value.click()
}
const importFromJSON = () => {
if (!inputChooseFileToImportFrom.value) return
if (
!inputChooseFileToImportFrom.value.files ||
inputChooseFileToImportFrom.value.files.length === 0
) {
toast.show(t("action.choose_file").toString())
return
}
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target!.result as string | null
if (!content) {
toast.show(t("action.choose_file").toString())
return
}
const collections = JSON.parse(content)
if (collections[0]) {
const [name, folders, requests] = Object.keys(collections[0])
if (name === "name" && folders === "folders" && requests === "requests") {
// Do nothing
}
} else {
failedImport()
return
}
appendGraphqlCollections(collections)
fileImported()
}
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
inputChooseFileToImportFrom.value.value = ""
}
const exportJSON = () => {
const dataToWrite = collectionJson.value
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO: get uri from meta
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
}
</script>

View File

@@ -7,7 +7,7 @@
@dragover.stop
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="$refs.options.tippy().show()"
@contextmenu.prevent="options.tippy().show()"
>
<span
class="cursor-pointer flex px-2 w-16 items-center justify-center truncate"
@@ -15,7 +15,7 @@
>
<SmartIcon
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:class="{ 'text-accent': isSelected }"
:name="isSelected ? 'check-circle' : 'file'"
/>
</span>
@@ -23,7 +23,9 @@
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hover:text-secondaryDark"
@click="!doc ? selectRequest() : {}"
>
<span class="truncate"> {{ request.name }} </span>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }}
</span>
</span>
<div class="flex">
<ButtonSecondary
@@ -41,6 +43,7 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
@@ -49,45 +52,60 @@
svg="more-vertical"
/>
</template>
<SmartItem
svg="edit"
:label="`${$t('action.edit')}`"
@click.native="
() => {
$emit('edit-request', {
request,
requestIndex,
folderPath,
})
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="copy"
:label="`${$t('action.duplicate')}`"
@click.native="
() => {
$emit('duplicate-request', {
request,
requestIndex,
folderPath,
})
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="trash-2"
color="red"
:label="`${$t('action.delete')}`"
@click.native="
() => {
confirmRemove = true
$refs.options.tippy().hide()
}
"
/>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.e="edit.$el.click()"
@keyup.d="duplicate.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="edit"
svg="edit"
:label="`${$t('action.edit')}`"
:shortcut="['E']"
@click.native="
() => {
$emit('edit-request', {
request,
requestIndex,
folderPath,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="duplicate"
svg="copy"
:label="`${$t('action.duplicate')}`"
:shortcut="['D']"
@click.native="
() => {
$emit('duplicate-request', {
request,
requestIndex,
folderPath,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="deleteAction"
svg="trash-2"
:label="`${$t('action.delete')}`"
:shortcut="['⌫']"
@click.native="
() => {
confirmRemove = true
options.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
</div>
@@ -102,7 +120,7 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "@nuxtjs/composition-api"
import { defineComponent, PropType, ref } from "@nuxtjs/composition-api"
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
import { removeGraphqlRequest } from "~/newstore/collections"
import { setGQLSession } from "~/newstore/GQLSession"
@@ -118,6 +136,15 @@ export default defineComponent({
requestIndex: { type: Number, default: null },
doc: Boolean,
},
setup() {
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
edit: ref<any | null>(null),
duplicate: ref<any | null>(null),
deleteAction: ref<any | null>(null),
}
},
data() {
return {
dragging: false,

View File

@@ -1,8 +1,5 @@
<template>
<AppSection
label="collections"
:class="{ 'rounded border border-divider': savingMode }"
>
<div :class="{ 'rounded border border-divider': savingMode }">
<div
class="divide-dividerLight divide-y border-b border-dividerLight flex flex-col top-0 z-10 sticky"
:class="{ 'bg-primary': !savingMode }"
@@ -13,7 +10,7 @@
type="search"
autocomplete="off"
:placeholder="$t('action.search')"
class="bg-transparent flex w-full py-2 px-4"
class="bg-transparent flex py-2 px-4"
/>
<div class="flex flex-1 justify-between">
<ButtonSecondary
@@ -84,7 +81,7 @@
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
>
<i class="opacity-75 pb-2 material-icons">manage_search</i>
<span class="text-center">
<span class="my-2 text-center">
{{ $t("state.nothing_found") }} "{{ filterText }}"
</span>
</div>
@@ -126,11 +123,12 @@
:show="showModalImportExport"
@hide-modal="displayModalImportExport(false)"
/>
</AppSection>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import cloneDeep from "lodash/cloneDeep"
import clone from "lodash/clone"
import { useReadonlyStream } from "~/helpers/utils/composables"
import {

View File

@@ -1,19 +1,16 @@
<template>
<AppSection
label="collections"
:class="{ 'rounded border border-divider': saveRequest }"
>
<div :class="{ 'rounded border border-divider': saveRequest }">
<div
class="divide-dividerLight divide-y bg-primary border-b border-dividerLight rounded-t flex flex-col z-10 sticky"
:style="saveRequest ? 'top: calc(-1 * var(--body-font-size))' : 'top: 0'"
:style="saveRequest ? 'top: calc(-1 * var(--font-size-body))' : 'top: 0'"
>
<div v-if="!saveRequest" class="search-wrappe">
<div v-if="!saveRequest" class="flex flex-col">
<input
v-model="filterText"
type="search"
autocomplete="off"
:placeholder="$t('action.search')"
class="bg-transparent flex w-full py-2 pr-2 pl-4"
class="bg-transparent py-2 pr-2 pl-4"
/>
</div>
<CollectionsChooseType
@@ -135,7 +132,7 @@
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
>
<i class="opacity-75 pb-2 material-icons">manage_search</i>
<span class="text-center">
<span class="my-2 text-center">
{{ $t("state.nothing_found") }} "{{ filterText }}"
</span>
</div>
@@ -181,7 +178,7 @@
@hide-modal="displayModalImportExport(false)"
@update-team-collections="updateTeamCollections"
/>
</AppSection>
</div>
</template>
<script>
@@ -650,11 +647,26 @@ export default defineComponent({
})
}
},
duplicateRequest({ folderPath, request }) {
saveRESTRequestAs(folderPath, {
...cloneDeep(request),
name: `${request.name} - ${this.$t("action.duplicate")}`,
})
duplicateRequest({ folderPath, request, collectionID }) {
if (this.collectionsType.type === "team-collections") {
const newReq = {
...cloneDeep(request),
name: `${request.name} - ${this.$t("action.duplicate")}`,
}
teamUtils.saveRequestAsTeams(
this.$apollo,
JSON.stringify(newReq),
`${request.name} - ${this.$t("action.duplicate")}`,
this.collectionsType.selectedTeam.id,
collectionID
)
} else if (this.collectionsType.type === "my-collections") {
saveRESTRequestAs(folderPath, {
...cloneDeep(request),
name: `${request.name} - ${this.$t("action.duplicate")}`,
})
}
},
},
})

View File

@@ -8,7 +8,7 @@
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="$refs.options.tippy().show()"
@contextmenu.prevent="options.tippy().show()"
>
<span
class="cursor-pointer flex px-4 items-center justify-center"
@@ -16,7 +16,7 @@
>
<SmartIcon
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:class="{ 'text-accent': isSelected }"
:name="getCollectionIcon"
/>
</span>
@@ -24,7 +24,9 @@
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate"> {{ collection.name }} </span>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ collection.name }}
</span>
</span>
<div class="flex">
<ButtonSecondary
@@ -63,6 +65,7 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
@@ -71,40 +74,55 @@
svg="more-vertical"
/>
</template>
<SmartItem
svg="folder-plus"
:label="$t('folder.new')"
@click.native="
() => {
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="edit"
:label="$t('action.edit')"
@click.native="
() => {
$emit('edit-collection')
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="trash-2"
color="red"
:label="$t('action.delete')"
@click.native="
() => {
confirmRemove = true
$refs.options.tippy().hide()
}
"
/>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="folderAction"
svg="folder-plus"
:label="$t('folder.new')"
:shortcut="['N']"
@click.native="
() => {
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="edit"
svg="edit"
:label="$t('action.edit')"
:shortcut="['E']"
@click.native="
() => {
$emit('edit-collection')
options.tippy().hide()
}
"
/>
<SmartItem
ref="deleteAction"
svg="trash-2"
:label="$t('action.delete')"
:shortcut="['⌫']"
@click.native="
() => {
confirmRemove = true
options.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
</div>
@@ -165,7 +183,7 @@
:src="`/images/states/${$colorMode.value}/pack.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 mb-4 w-16 inline-flex"
:alt="$t('empty.collection')"
:alt="`${$t('empty.collection')}`"
/>
<span class="text-center">
{{ $t("empty.collection") }}
@@ -182,8 +200,8 @@
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api"
import { moveRESTRequest } from "~/newstore/collections"
export default defineComponent({
@@ -197,6 +215,15 @@ export default defineComponent({
collectionsType: { type: Object, default: () => {} },
picked: { type: Object, default: () => {} },
},
setup() {
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
}
},
data() {
return {
showChildren: false,
@@ -209,7 +236,7 @@ export default defineComponent({
}
},
computed: {
isSelected() {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "my-collection" &&

View File

@@ -8,7 +8,7 @@
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="$refs.options.tippy().show()"
@contextmenu.prevent="options.tippy().show()"
>
<span
class="cursor-pointer flex px-4 items-center justify-center"
@@ -16,7 +16,7 @@
>
<SmartIcon
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:class="{ 'text-accent': isSelected }"
:name="getCollectionIcon"
/>
</span>
@@ -24,7 +24,7 @@
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate">
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ folder.name ? folder.name : folder.title }}
</span>
</span>
@@ -32,7 +32,7 @@
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="folder-plus"
:title="$t('folder.new')"
:title="t('folder.new')"
class="hidden group-hover:inline-flex"
@click.native="$emit('add-folder', { folder, path: folderPath })"
/>
@@ -43,50 +43,66 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.more')"
:title="t('action.more')"
svg="more-vertical"
/>
</template>
<SmartItem
svg="folder-plus"
:label="$t('folder.new')"
@click.native="
() => {
$emit('add-folder', { folder, path: folderPath })
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="edit"
:label="$t('action.edit')"
@click.native="
() => {
$emit('edit-folder', {
folder,
folderIndex,
collectionIndex,
folderPath,
})
$refs.options.tippy().hide()
}
"
/>
<SmartItem
svg="trash-2"
color="red"
:label="$t('action.delete')"
@click.native="
() => {
confirmRemove = true
$refs.options.tippy().hide()
}
"
/>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="folderAction"
svg="folder-plus"
:label="t('folder.new')"
:shortcut="['N']"
@click.native="
() => {
$emit('add-folder', { folder, path: folderPath })
options.tippy().hide()
}
"
/>
<SmartItem
ref="edit"
svg="edit"
:label="t('action.edit')"
:shortcut="['E']"
@click.native="
() => {
$emit('edit-folder', {
folder,
folderIndex,
collectionIndex,
folderPath,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="deleteAction"
svg="trash-2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click.native="
() => {
confirmRemove = true
options.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
</div>
@@ -147,25 +163,26 @@
:src="`/images/states/${$colorMode.value}/pack.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 mb-4 w-16 inline-flex"
:alt="$t('empty.folder')"
:alt="`${t('empty.folder')}`"
/>
<span class="text-center">
{{ $t("empty.folder") }}
{{ t("empty.folder") }}
</span>
</div>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('confirm.remove_folder')"
:title="t('confirm.remove_folder')"
@hide-modal="confirmRemove = false"
@resolve="removeFolder"
/>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api"
import { useI18n } from "~/helpers/utils/composables"
import {
removeRESTFolder,
removeRESTRequest,
@@ -185,6 +202,18 @@ export default defineComponent({
collectionsType: { type: Object, default: () => {} },
picked: { type: Object, default: () => {} },
},
setup() {
const t = useI18n()
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
t,
}
},
data() {
return {
showChildren: false,
@@ -195,7 +224,7 @@ export default defineComponent({
}
},
computed: {
isSelected() {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "my-folder" &&
@@ -233,7 +262,7 @@ export default defineComponent({
this.$emit("select", { picked: null })
}
removeRESTFolder(this.folderPath)
this.$toast.success(this.$t("state.deleted"))
this.$toast.success(`${this.$t("state.deleted")}`)
},
dropEvent({ dataTransfer }) {
this.dragging = !this.dragging

View File

@@ -7,7 +7,7 @@
@dragover.stop
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="$refs.options.tippy().show()"
@contextmenu.prevent="options.tippy().show()"
>
<span
class="cursor-pointer flex px-2 w-16 items-center justify-center truncate"
@@ -17,7 +17,7 @@
<SmartIcon
v-if="isSelected"
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:class="{ 'text-accent': isSelected }"
name="check-circle"
/>
<span v-else class="truncate">
@@ -28,7 +28,9 @@
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition items-center group-hover:text-secondaryDark"
@click="!doc ? selectRequest() : {}"
>
<span class="truncate"> {{ request.name }} </span>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }}
</span>
<span
v-if="
active &&
@@ -36,8 +38,18 @@
active.folderPath === folderPath &&
active.requestIndex === requestIndex
"
class="rounded-full bg-green-500 flex-shrink-0 h-1.5 mx-3 w-1.5"
></span>
v-tippy="{ theme: 'tooltip' }"
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
:title="`${$t('collection.request_in_use')}`"
>
<span
class="absolute animate-ping inline-flex flex-shrink-0 h-full w-full rounded-full bg-green-500 opacity-75"
>
</span>
<span
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
></span>
</span>
</span>
<div class="flex">
<ButtonSecondary
@@ -55,6 +67,7 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
@@ -63,45 +76,66 @@
svg="more-vertical"
/>
</template>
<SmartItem
svg="edit"
:label="$t('action.edit')"
@click.native="
$emit('edit-request', {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
})
$refs.options.tippy().hide()
"
/>
<SmartItem
svg="copy"
:label="$t('action.duplicate')"
@click.native="
$emit('duplicate-request', {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
})
$refs.options.tippy().hide()
"
/>
<SmartItem
svg="trash-2"
color="red"
:label="$t('action.delete')"
@click.native="
confirmRemove = true
$refs.options.tippy().hide()
"
/>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.e="edit.$el.click()"
@keyup.d="duplicate.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="edit"
svg="edit"
:label="$t('action.edit')"
:shortcut="['E']"
@click.native="
() => {
$emit('edit-request', {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="duplicate"
svg="copy"
:label="$t('action.duplicate')"
:shortcut="['D']"
@click.native="
() => {
$emit('duplicate-request', {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="deleteAction"
svg="trash-2"
:label="$t('action.delete')"
:shortcut="['⌫']"
@click.native="
() => {
confirmRemove = true
options.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
</div>
@@ -115,11 +149,15 @@
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import { translateToNewRequest } from "@hoppscotch/data"
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api"
import {
safelyExtractRESTRequest,
translateToNewRequest,
} from "@hoppscotch/data"
import { useReadonlyStream } from "~/helpers/utils/composables"
import {
getDefaultRESTRequest,
restSaveContext$,
setRESTRequest,
setRESTSaveContext,
@@ -143,6 +181,11 @@ export default defineComponent({
const active = useReadonlyStream(restSaveContext$, null)
return {
active,
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
edit: ref<any | null>(null),
duplicate: ref<any | null>(null),
deleteAction: ref<any | null>(null),
}
},
data() {
@@ -159,7 +202,7 @@ export default defineComponent({
}
},
computed: {
isSelected() {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "my-request" &&
@@ -190,11 +233,17 @@ export default defineComponent({
},
})
else {
setRESTRequest(translateToNewRequest(this.request), {
originLocation: "user-collection",
folderPath: this.folderPath,
requestIndex: this.requestIndex,
})
setRESTRequest(
safelyExtractRESTRequest(
translateToNewRequest(this.request),
getDefaultRESTRequest()
),
{
originLocation: "user-collection",
folderPath: this.folderPath,
requestIndex: this.requestIndex,
}
)
}
},
dragStart({ dataTransfer }) {
@@ -210,7 +259,7 @@ export default defineComponent({
requestIndex: this.$props.requestIndex,
})
},
getRequestLabelColor(method) {
getRequestLabelColor(method: string): string {
return (
this.requestMethodLabels[method.toLowerCase()] ||
this.requestMethodLabels.default

View File

@@ -2,7 +2,13 @@
<div class="flex flex-col">
<div
class="flex items-stretch group"
@contextmenu.prevent="$refs.options.tippy().show()"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="options.tippy().show()"
>
<span
class="cursor-pointer flex px-4 items-center justify-center"
@@ -10,7 +16,7 @@
>
<SmartIcon
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:class="{ 'text-accent': isSelected }"
:name="getCollectionIcon"
/>
</span>
@@ -18,13 +24,15 @@
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate"> {{ collection.title }} </span>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ collection.title }}
</span>
</span>
<div class="flex">
<ButtonSecondary
v-if="doc && !selected"
v-tippy="{ theme: 'tooltip' }"
:title="$t('import.title')"
:title="t('import.title')"
svg="circle"
color="green"
@click.native="$emit('select-collection')"
@@ -32,7 +40,7 @@
<ButtonSecondary
v-if="doc && selected"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.remove')"
:title="t('action.remove')"
svg="check-circle"
color="green"
@click.native="$emit('unselect-collection')"
@@ -41,7 +49,7 @@
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
svg="folder-plus"
:title="$t('folder.new')"
:title="t('folder.new')"
class="hidden group-hover:inline-flex"
@click.native="
$emit('add-folder', {
@@ -58,51 +66,64 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.more')"
:title="t('action.more')"
svg="more-vertical"
/>
</template>
<SmartItem
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
svg="folder-plus"
:label="$t('folder.new')"
@click.native="
() => {
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
$refs.options.tippy().hide()
}
"
/>
<SmartItem
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
svg="edit"
:label="$t('action.edit')"
@click.native="
() => {
$emit('edit-collection')
$refs.options.tippy().hide()
}
"
/>
<SmartItem
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
svg="trash-2"
color="red"
:label="$t('action.delete')"
@click.native="
() => {
confirmRemove = true
$refs.options.tippy().hide()
}
"
/>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="folderAction"
svg="folder-plus"
:label="t('folder.new')"
:shortcut="['N']"
@click.native="
() => {
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="edit"
svg="edit"
:label="t('action.edit')"
:shortcut="['E']"
@click.native="
() => {
$emit('edit-collection')
options.tippy().hide()
}
"
/>
<SmartItem
ref="deleteAction"
svg="trash-2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click.native="
() => {
confirmRemove = true
options.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
</div>
@@ -131,6 +152,7 @@
@select="$emit('select', $event)"
@expand-collection="expandCollection"
@remove-request="removeRequest"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<CollectionsTeamsRequest
v-for="(request, index) in collection.requests"
@@ -142,11 +164,13 @@
:request-index="request.id"
:doc="doc"
:save-request="saveRequest"
:collection-i-d="collection.id"
:collections-type="collectionsType"
:picked="picked"
@edit-request="editRequest($event)"
@select="$emit('select', $event)"
@remove-request="removeRequest"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<div
v-if="
@@ -161,25 +185,28 @@
:src="`/images/states/${$colorMode.value}/pack.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 mb-4 w-16 inline-flex"
:alt="$t('empty.collection')"
:alt="`${t('empty.collection')}`"
/>
<span class="text-center">
{{ $t("empty.collection") }}
{{ t("empty.collection") }}
</span>
</div>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('confirm.remove_collection')"
:title="t('confirm.remove_collection')"
@hide-modal="confirmRemove = false"
@resolve="removeCollection"
/>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api"
import * as E from "fp-ts/Either"
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
import { useI18n } from "~/helpers/utils/composables"
export default defineComponent({
props: {
@@ -192,9 +219,22 @@ export default defineComponent({
collectionsType: { type: Object, default: () => {} },
picked: { type: Object, default: () => {} },
},
setup() {
const t = useI18n()
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
t,
}
},
data() {
return {
showChildren: false,
dragging: false,
selectedFolder: {},
confirmRemove: false,
prevCursor: "",
@@ -203,7 +243,7 @@ export default defineComponent({
}
},
computed: {
isSelected() {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "teams-collection" &&
@@ -250,6 +290,16 @@ export default defineComponent({
expandCollection(collectionID) {
this.$emit("expand-collection", collectionID)
},
async dropEvent({ dataTransfer }) {
this.dragging = !this.dragging
const requestIndex = dataTransfer.getData("requestIndex")
const moveRequestResult = await moveRESTTeamRequest(
requestIndex,
this.collection.id
)()
if (E.isLeft(moveRequestResult))
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
},
removeRequest({ collectionIndex, folderName, requestIndex }) {
this.$emit("remove-request", {
collectionIndex,

View File

@@ -1,8 +1,14 @@
<template>
<div class="flex flex-col">
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div
class="flex items-stretch group"
@contextmenu.prevent="$refs.options.tippy().show()"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="options.tippy().show()"
>
<span
class="cursor-pointer flex px-4 items-center justify-center"
@@ -10,7 +16,7 @@
>
<SmartIcon
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:class="{ 'text-accent': isSelected }"
:name="getCollectionIcon"
/>
</span>
@@ -18,7 +24,7 @@
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate">
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ folder.name ? folder.name : folder.title }}
</span>
</span>
@@ -39,6 +45,7 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
@@ -47,45 +54,57 @@
svg="more-vertical"
/>
</template>
<SmartItem
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
svg="folder-plus"
:label="$t('folder.new')"
@click.native="
() => {
$emit('add-folder', { folder, path: folderPath })
$refs.options.tippy().hide()
}
"
/>
<SmartItem
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
svg="edit"
:label="$t('action.edit')"
@click.native="
() => {
$emit('edit-folder', {
folder,
folderIndex,
collectionIndex,
folderPath: '',
})
$refs.options.tippy().hide()
}
"
/>
<SmartItem
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
svg="trash-2"
color="red"
:label="$t('action.delete')"
@click.native="
() => {
confirmRemove = true
$refs.options.tippy().hide()
}
"
/>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="folderAction"
svg="folder-plus"
:label="$t('folder.new')"
:shortcut="['N']"
@click.native="
() => {
$emit('add-folder', { folder, path: folderPath })
options.tippy().hide()
}
"
/>
<SmartItem
ref="edit"
svg="edit"
:label="$t('action.edit')"
:shortcut="['E']"
@click.native="
() => {
$emit('edit-folder', {
folder,
folderIndex,
collectionIndex,
folderPath: '',
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="deleteAction"
svg="trash-2"
:label="$t('action.delete')"
:shortcut="['⌫']"
@click.native="
() => {
confirmRemove = true
options.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
</div>
@@ -114,6 +133,7 @@
@select="$emit('select', $event)"
@expand-collection="expandCollection"
@remove-request="removeRequest"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<CollectionsTeamsRequest
v-for="(request, index) in folder.requests"
@@ -127,9 +147,11 @@
:save-request="saveRequest"
:collections-type="collectionsType"
:picked="picked"
:collection-i-d="folder.id"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
@remove-request="removeRequest"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<div
v-if="
@@ -142,7 +164,7 @@
:src="`/images/states/${$colorMode.value}/pack.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 mb-4 w-16 inline-flex"
:alt="$t('empty.folder')"
:alt="`${$t('empty.folder')}`"
/>
<span class="text-center">
{{ $t("empty.folder") }}
@@ -159,8 +181,10 @@
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api"
import * as E from "fp-ts/Either"
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
import * as teamUtils from "~/helpers/teams/utils"
export default defineComponent({
@@ -176,16 +200,26 @@ export default defineComponent({
collectionsType: { type: Object, default: () => {} },
picked: { type: Object, default: () => {} },
},
setup() {
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
}
},
data() {
return {
showChildren: false,
dragging: false,
confirmRemove: false,
prevCursor: "",
cursor: "",
}
},
computed: {
isSelected() {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "teams-folder" &&
@@ -226,11 +260,11 @@ export default defineComponent({
teamUtils
.deleteCollection(this.$apollo, this.folder.id)
.then(() => {
this.$toast.success(this.$t("state.deleted"))
this.$toast.success(`${this.$t("state.deleted")}`)
this.$emit("update-team-collections")
})
.catch((e) => {
this.$toast.error(this.$t("error.something_went_wrong"))
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
console.error(e)
})
this.$emit("update-team-collections")
@@ -239,6 +273,16 @@ export default defineComponent({
expandCollection(collectionID) {
this.$emit("expand-collection", collectionID)
},
async dropEvent({ dataTransfer }) {
this.dragging = !this.dragging
const requestIndex = dataTransfer.getData("requestIndex")
const moveRequestResult = await moveRESTTeamRequest(
requestIndex,
this.folder.id
)()
if (E.isLeft(moveRequestResult))
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
},
removeRequest({ collectionIndex, folderName, requestIndex }) {
this.$emit("remove-request", {
collectionIndex,

View File

@@ -1,8 +1,13 @@
<template>
<div class="flex flex-col">
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div
class="flex items-stretch group"
@contextmenu.prevent="$refs.options.tippy().show()"
draggable="true"
@dragstart="dragStart"
@dragover.stop
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="options.tippy().show()"
>
<span
class="cursor-pointer flex px-2 w-16 items-center justify-center truncate"
@@ -12,10 +17,10 @@
<SmartIcon
v-if="isSelected"
class="svg-icons"
:class="{ 'text-green-500': isSelected }"
:class="{ 'text-accent': isSelected }"
name="check-circle"
/>
<span v-else>
<span v-else class="truncate">
{{ request.method }}
</span>
</span>
@@ -23,15 +28,27 @@
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition items-center group-hover:text-secondaryDark"
@click="!doc ? selectRequest() : {}"
>
<span class="truncate"> {{ request.name }} </span>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }}
</span>
<span
v-if="
active &&
active.originLocation === 'team-collection' &&
active.requestID === requestIndex
"
class="rounded-full bg-green-500 flex-shrink-0 h-1.5 mx-3 w-1.5"
></span>
v-tippy="{ theme: 'tooltip' }"
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
:title="`${$t('collection.request_in_use')}`"
>
<span
class="absolute animate-ping inline-flex flex-shrink-0 h-full w-full rounded-full bg-green-500 opacity-75"
>
</span>
<span
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
></span>
</span>
</span>
<div class="flex">
<ButtonSecondary
@@ -50,6 +67,7 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
@@ -58,29 +76,62 @@
svg="more-vertical"
/>
</template>
<SmartItem
svg="edit"
:label="$t('action.edit')"
@click.native="
$emit('edit-request', {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
})
$refs.options.tippy().hide()
"
/>
<SmartItem
svg="trash-2"
color="red"
:label="$t('action.delete')"
@click.native="
confirmRemove = true
$refs.options.tippy().hide()
"
/>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.e="edit.$el.click()"
@keyup.d="duplicate.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="edit"
svg="edit"
:label="$t('action.edit')"
:shortcut="['E']"
@click.native="
() => {
$emit('edit-request', {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="duplicate"
svg="copy"
:label="$t('action.duplicate')"
:shortcut="['D']"
@click.native="
() => {
$emit('duplicate-request', {
request,
requestIndex,
collectionID,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="deleteAction"
svg="trash-2"
:label="$t('action.delete')"
:shortcut="['⌫']"
@click.native="
() => {
confirmRemove = true
options.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
</div>
@@ -95,10 +146,14 @@
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { translateToNewRequest } from "@hoppscotch/data"
import { defineComponent, ref } from "@nuxtjs/composition-api"
import {
safelyExtractRESTRequest,
translateToNewRequest,
} from "@hoppscotch/data"
import { useReadonlyStream } from "~/helpers/utils/composables"
import {
getDefaultRESTRequest,
restSaveContext$,
setRESTRequest,
setRESTSaveContext,
@@ -116,15 +171,22 @@ export default defineComponent({
saveRequest: Boolean,
collectionsType: { type: Object, default: () => {} },
picked: { type: Object, default: () => {} },
collectionID: { type: String, default: null },
},
setup() {
const active = useReadonlyStream(restSaveContext$, null)
return {
active,
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
duplicate: ref<any | null>(null),
}
},
data() {
return {
dragging: false,
requestMethodLabels: {
get: "text-green-500",
post: "text-yellow-500",
@@ -162,10 +224,20 @@ export default defineComponent({
},
})
else
setRESTRequest(translateToNewRequest(this.request), {
originLocation: "team-collection",
requestID: this.requestIndex as string,
})
setRESTRequest(
safelyExtractRESTRequest(
translateToNewRequest(this.request),
getDefaultRESTRequest()
),
{
originLocation: "team-collection",
requestID: this.requestIndex as string,
}
)
},
dragStart({ dataTransfer }) {
this.dragging = !this.dragging
dataTransfer.setData("requestIndex", this.requestIndex)
},
removeRequest() {
this.$emit("remove-request", {

View File

@@ -6,7 +6,7 @@
>
<template #body>
<div class="flex flex-col px-2">
<div class="flex relative">
<div class="relative flex">
<input
id="selectLabelEnvEdit"
v-model="name"
@@ -22,7 +22,7 @@
{{ $t("action.label") }}
</label>
</div>
<div class="flex flex-1 items-center justify-between">
<div class="flex items-center justify-between flex-1">
<label for="variableList" class="p-4">
{{ $t("environment.variable_list") }}
</label>
@@ -41,21 +41,21 @@
/>
</div>
</div>
<div class="divide-dividerLight divide-y border border-divider rounded">
<div class="border divide-y rounded divide-dividerLight border-divider">
<div
v-for="(variable, index) in vars"
:key="`variable-${index}`"
class="divide-dividerLight divide-x flex"
class="flex divide-x divide-dividerLight"
>
<input
v-model="variable.key"
class="bg-transparent flex flex-1 py-2 px-4"
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${$t('count.variable', { count: index + 1 })}`"
:name="'param' + index"
/>
<input
v-model="variable.value"
class="bg-transparent flex flex-1 py-2 px-4"
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${$t('count.value', { count: index + 1 })}`"
:name="'value' + index"
/>
@@ -72,15 +72,15 @@
</div>
<div
v-if="vars.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="$t('empty.environments')"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${$t('empty.environments')}`"
/>
<span class="text-center pb-4">
<span class="pb-4 text-center">
{{ $t("empty.environments") }}
</span>
<ButtonSecondary

View File

@@ -1,16 +1,16 @@
<template>
<div
class="flex items-stretch group"
@contextmenu.prevent="$refs.options.tippy().show()"
@contextmenu.prevent="options.tippy().show()"
>
<span
class="cursor-pointer flex px-4 items-center justify-center"
class="flex items-center justify-center px-4 cursor-pointer"
@click="$emit('edit-environment')"
>
<SmartIcon class="svg-icons" name="layers" />
</span>
<span
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hover:text-secondaryDark"
class="flex flex-1 min-w-0 py-2 pr-2 transition cursor-pointer group-hover:text-secondaryDark"
@click="$emit('edit-environment')"
>
<span class="truncate">
@@ -18,7 +18,14 @@
</span>
</span>
<span>
<tippy ref="options" interactive trigger="click" theme="popover" arrow>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@@ -26,38 +33,55 @@
svg="more-vertical"
/>
</template>
<SmartItem
svg="edit"
:label="`${$t('action.edit')}`"
@click.native="
() => {
$emit('edit-environment')
$refs.options.tippy().hide()
}
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.e="edit.$el.click()"
@keyup.d="duplicate.$el.click()"
@keyup.delete="
!(environmentIndex === 'Global') ? deleteAction.$el.click() : null
"
/>
<SmartItem
svg="copy"
:label="`${$t('action.duplicate')}`"
@click.native="
() => {
duplicateEnvironment()
$refs.options.tippy().hide()
}
"
/>
<SmartItem
v-if="!(environmentIndex === 'Global')"
svg="trash-2"
color="red"
:label="`${$t('action.delete')}`"
@click.native="
() => {
confirmRemove = true
$refs.options.tippy().hide()
}
"
/>
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="edit"
svg="edit"
:label="`${$t('action.edit')}`"
:shortcut="['E']"
@click.native="
() => {
$emit('edit-environment')
options.tippy().hide()
}
"
/>
<SmartItem
ref="duplicate"
svg="copy"
:label="`${$t('action.duplicate')}`"
:shortcut="['D']"
@click.native="
() => {
duplicateEnvironment()
options.tippy().hide()
}
"
/>
<SmartItem
v-if="!(environmentIndex === 'Global')"
ref="deleteAction"
svg="trash-2"
:label="`${$t('action.delete')}`"
:shortcut="['⌫']"
@click.native="
() => {
confirmRemove = true
options.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
<SmartConfirmModal
@@ -70,7 +94,7 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "@nuxtjs/composition-api"
import { defineComponent, PropType, ref } from "@nuxtjs/composition-api"
import {
deleteEnvironment,
duplicateEnvironment,
@@ -88,6 +112,15 @@ export default defineComponent({
default: null,
},
},
setup() {
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
edit: ref<any | null>(null),
duplicate: ref<any | null>(null),
deleteAction: ref<any | null>(null),
}
},
data() {
return {
confirmRemove: false,

View File

@@ -1,7 +1,7 @@
<template>
<SmartModal
v-if="show"
:title="`${$t('modal.import_export')} ${$t('environment.title')}`"
:title="`${t('environment.title')}`"
max-width="sm:max-w-md"
@close="hideModal"
>
@@ -11,26 +11,28 @@
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.more')"
:title="t('action.more')"
svg="more-vertical"
/>
</template>
<SmartItem
icon="assignment_returned"
:label="$t('import.from_gist')"
:label="t('import.from_gist')"
@click.native="
readEnvironmentGist
$refs.options.tippy().hide()
() => {
readEnvironmentGist()
options.tippy().hide()
}
"
/>
<span
v-tippy="{ theme: 'tooltip' }"
:title="
!currentUser
? $t('export.require_github')
? `${t('export.require_github')}`
: currentUser.provider !== 'github.com'
? $t('export.require_github')
: null
? `${t('export.require_github')}`
: undefined
"
>
<SmartItem
@@ -42,10 +44,12 @@
: false
"
icon="assignment_turned_in"
:label="$t('export.create_secret_gist')"
:label="t('export.create_secret_gist')"
@click.native="
createEnvironmentGist
$refs.options.tippy().hide()
() => {
createEnvironmentGist()
options.tippy().hide()
}
"
/>
</span>
@@ -53,26 +57,10 @@
</span>
</template>
<template #body>
<div class="flex flex-col space-y-2">
<div class="flex flex-col px-2 space-y-2">
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.replace_current')"
svg="file"
:label="$t('action.replace_json')"
@click.native="openDialogChooseFileToReplaceWith"
/>
<input
ref="inputChooseFileToReplaceWith"
class="input"
type="file"
accept="application/json"
@change="replaceWithJSON"
/>
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.preserve_current')"
svg="folder-plus"
:label="$t('import.json')"
:label="t('import.from_json')"
@click.native="openDialogChooseFileToImportFrom"
/>
<input
@@ -82,11 +70,12 @@
accept="application/json"
@change="importFromJSON"
/>
<hr />
<SmartItem
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
:title="t('action.download_file')"
svg="download"
:label="$t('export.as_json')"
:label="t('export.as_json')"
@click.native="exportJSON"
/>
</div>
@@ -94,146 +83,197 @@
</SmartModal>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { computed, ref } from "@nuxtjs/composition-api"
import { currentUser$ } from "~/helpers/fb/auth"
import { useReadonlyStream } from "~/helpers/utils/composables"
import {
useAxios,
useI18n,
useReadonlyStream,
useToast,
} from "~/helpers/utils/composables"
import {
environments$,
replaceEnvironments,
appendEnvironments,
Environment,
} from "~/newstore/environments"
export default defineComponent({
props: {
show: Boolean,
},
setup() {
return {
environments: useReadonlyStream(environments$, []),
currentUser: useReadonlyStream(currentUser$, null),
}
},
computed: {
environmentJson() {
return JSON.stringify(this.environments, null, 2)
},
},
methods: {
async createEnvironmentGist() {
await this.$axios
.$post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-environments.json": {
content: this.environmentJson,
},
},
defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const axios = useAxios()
const toast = useToast()
const t = useI18n()
const environments = useReadonlyStream(environments$, [])
const currentUser = useReadonlyStream(currentUser$, null)
// Template refs
const options = ref<any>()
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
const environmentJson = computed(() => {
return JSON.stringify(environments.value, null, 2)
})
const createEnvironmentGist = async () => {
if (!currentUser.value) {
toast.error(t("profile.no_permission").toString())
return
}
try {
const res = await axios.$post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-environments.json": {
content: environmentJson.value,
},
{
headers: {
Authorization: `token ${this.currentUser.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
)
.then((res) => {
this.$toast.success(this.$t("export.gist_created"))
window.open(res.html_url)
})
.catch((e) => {
this.$toast.error(this.$t("error.something_went_wrong"))
console.error(e)
})
},
async readEnvironmentGist() {
const gist = prompt(this.$t("import.gist_url"))
if (!gist) return
await this.$axios
.$get(`https://api.github.com/gists/${gist.split("/").pop()}`, {
headers: {
Accept: "application/vnd.github.v3+json",
},
})
.then(({ files }) => {
const environments = JSON.parse(Object.values(files)[0].content)
replaceEnvironments(environments)
this.fileImported()
})
.catch((e) => {
this.failedImport()
console.error(e)
})
},
hideModal() {
this.$emit("hide-modal")
},
openDialogChooseFileToReplaceWith() {
this.$refs.inputChooseFileToReplaceWith.click()
},
openDialogChooseFileToImportFrom() {
this.$refs.inputChooseFileToImportFrom.click()
},
replaceWithJSON() {
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target.result
const environments = JSON.parse(content)
replaceEnvironments(environments)
},
},
{
headers: {
Authorization: `token ${currentUser.value.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0])
this.fileImported()
this.$refs.inputChooseFileToReplaceWith.value = ""
},
importFromJSON() {
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target.result
const importFileObj = JSON.parse(content)
if (
importFileObj._postman_variable_scope === "environment" ||
importFileObj._postman_variable_scope === "globals"
) {
this.importFromPostman(importFileObj)
} else {
this.importFromHoppscotch(importFileObj)
)
toast.success(t("export.gist_created").toString())
window.open(res.html_url)
} catch (e) {
toast.error(t("error.something_went_wrong").toString())
console.error(e)
}
}
const fileImported = () => {
toast.success(t("state.file_imported").toString())
}
const failedImport = () => {
toast.error(t("import.failed").toString())
}
const readEnvironmentGist = async () => {
const gist = prompt(t("import.gist_url").toString())
if (!gist) return
try {
const { files } = (await axios.$get(
`https://api.github.com/gists/${gist.split("/").pop()}`,
{
headers: {
Accept: "application/vnd.github.v3+json",
},
}
)) as {
files: {
[fileName: string]: {
content: any
}
}
reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0])
this.$refs.inputChooseFileToImportFrom.value = ""
},
importFromHoppscotch(environments) {
appendEnvironments(environments)
this.fileImported()
},
importFromPostman({ name, values }) {
const environment = { name, variables: [] }
values.forEach(({ key, value }) =>
environment.variables.push({ key, value })
)
const environments = [environment]
this.importFromHoppscotch(environments)
},
exportJSON() {
const dataToWrite = this.environmentJson
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
this.$toast.success(this.$t("state.download_started"))
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
},
fileImported() {
this.$toast.success(this.$t("state.file_imported"))
},
},
})
}
const environments = JSON.parse(Object.values(files)[0].content)
replaceEnvironments(environments)
fileImported()
} catch (e) {
failedImport()
console.error(e)
}
}
const hideModal = () => {
emit("hide-modal")
}
const openDialogChooseFileToImportFrom = () => {
if (inputChooseFileToImportFrom.value)
inputChooseFileToImportFrom.value.click()
}
const importFromJSON = () => {
if (!inputChooseFileToImportFrom.value) return
if (
!inputChooseFileToImportFrom.value.files ||
inputChooseFileToImportFrom.value.files.length === 0
) {
toast.show(t("action.choose_file").toString())
return
}
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target!.result as string | null
if (!content) {
toast.show(t("action.choose_file").toString())
return
}
const environments = JSON.parse(content)
if (
environments._postman_variable_scope === "environment" ||
environments._postman_variable_scope === "globals"
) {
importFromPostman(environments)
} else if (environments[0]) {
const [name, variables] = Object.keys(environments[0])
if (name === "name" && variables === "variables") {
// Do nothing
}
importFromHoppscotch(environments)
} else {
failedImport()
}
}
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
inputChooseFileToImportFrom.value.value = ""
}
const importFromHoppscotch = (environments: Environment[]) => {
appendEnvironments(environments)
fileImported()
}
const importFromPostman = ({
name,
values,
}: {
name: string
values: { key: string; value: string }[]
}) => {
const environment: Environment = { name, variables: [] }
values.forEach(({ key, value }) => environment.variables.push({ key, value }))
const environments = [environment]
importFromHoppscotch(environments)
}
const exportJSON = () => {
const dataToWrite = environmentJson.value
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO: get uri from meta
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
}
</script>

View File

@@ -1,22 +1,22 @@
<template>
<AppSection :label="`${$t('environment.title')}`">
<div class="bg-primary rounded-t flex flex-col top-0 z-10 sticky">
<div>
<div class="sticky top-0 z-10 flex flex-col rounded-t bg-primary">
<tippy ref="options" interactive trigger="click" theme="popover" arrow>
<template #trigger>
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${$t('environment.select')}`"
class="bg-transparent border-b border-dividerLight flex-1 select-wrapper"
class="flex-1 bg-transparent border-b border-dividerLight select-wrapper"
>
<ButtonSecondary
v-if="selectedEnvironmentIndex !== -1"
:label="environments[selectedEnvironmentIndex].name"
class="rounded-none flex-1 pr-8"
class="flex-1 pr-8 rounded-none"
/>
<ButtonSecondary
v-else
:label="`${$t('environment.no_environment')}`"
class="rounded-none flex-1 pr-8"
class="flex-1 pr-8 rounded-none"
/>
</span>
</template>
@@ -45,7 +45,7 @@
"
/>
</tippy>
<div class="border-b border-dividerLight flex flex-1 justify-between">
<div class="flex justify-between flex-1 border-b border-dividerLight">
<ButtonSecondary
svg="plus"
:label="`${$t('action.new')}`"
@@ -69,19 +69,6 @@
</div>
</div>
</div>
<EnvironmentsAdd
:show="showModalAdd"
@hide-modal="displayModalAdd(false)"
/>
<EnvironmentsEdit
:show="showModalEdit"
:editing-environment-index="editingEnvironmentIndex"
@hide-modal="displayModalEdit(false)"
/>
<EnvironmentsImportExport
:show="showModalImportExport"
@hide-modal="displayModalImportExport(false)"
/>
<div class="flex flex-col">
<EnvironmentsEnvironment
environment-index="Global"
@@ -99,15 +86,15 @@
</div>
<div
v-if="environments.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="$t('empty.environments')"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${$t('empty.environments')}`"
/>
<span class="text-center pb-4">
<span class="pb-4 text-center">
{{ $t("empty.environments") }}
</span>
<ButtonSecondary
@@ -117,7 +104,20 @@
@click.native="displayModalAdd(true)"
/>
</div>
</AppSection>
<EnvironmentsAdd
:show="showModalAdd"
@hide-modal="displayModalAdd(false)"
/>
<EnvironmentsEdit
:show="showModalEdit"
:editing-environment-index="editingEnvironmentIndex"
@hide-modal="displayModalEdit(false)"
/>
<EnvironmentsImportExport
:show="showModalImportExport"
@hide-modal="displayModalImportExport(false)"
/>
</div>
</template>
<script lang="ts">

View File

@@ -7,7 +7,7 @@
@close="hideModal"
>
<template #body>
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2 px-2">
<div v-if="mode === 'sign-in'" class="flex flex-col px-2 space-y-2">
<SmartItem
:loading="signingInWithGitHub"
svg="auth/github"
@@ -56,8 +56,8 @@
/>
</form>
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
<div class="flex flex-col max-w-md items-center justify-center">
<SmartIcon class="h-6 text-accent w-6" name="inbox" />
<div class="flex flex-col items-center justify-center max-w-md">
<SmartIcon class="w-6 h-6 text-accent" name="inbox" />
<h3 class="my-2 text-lg text-center">
{{ $t("auth.we_sent_magic_link") }}
</h3>
@@ -95,7 +95,7 @@
</p>
<p
v-if="mode === 'email-sent'"
class="flex flex-1 text-secondaryLight justify-between"
class="flex justify-between flex-1 text-secondaryLight"
>
<SmartAnchor
class="link"

View File

@@ -1,9 +1,11 @@
<template>
<div class="flex">
<div class="flex" @click="$refs.logout.$el.click()">
<SmartItem
ref="logout"
svg="log-out"
:label="`${$t('auth.logout')}`"
:outline="outline"
:shortcut="shortcut"
@click.native="
() => {
$emit('confirm-logout')
@@ -30,6 +32,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
shortcut: {
type: Array,
default: () => [],
},
},
data() {
return {

View File

@@ -21,19 +21,19 @@
</div>
<div
v-if="gqlField.description"
class="text-secondaryLight py-2 field-desc"
class="py-2 text-secondaryLight field-desc"
>
{{ gqlField.description }}
</div>
<div
v-if="gqlField.isDeprecated"
class="rounded bg-yellow-200 my-1 text-black py-1 px-2 inline-block field-deprecated"
class="inline-block px-2 py-1 my-1 text-black bg-yellow-200 rounded field-deprecated"
>
{{ $t("state.deprecated") }}
</div>
<div v-if="fieldArgs.length > 0">
<h5 class="my-2">Arguments:</h5>
<div class="border-divider border-l-2 pl-4">
<div class="pl-4 border-l-2 border-divider">
<div v-for="(field, index) in fieldArgs" :key="`field-${index}`">
<span>
{{ field.name }}:
@@ -44,7 +44,7 @@
</span>
<div
v-if="field.description"
class="text-secondaryLight py-2 field-desc"
class="py-2 text-secondaryLight field-desc"
>
{{ field.description }}
</div>

View File

@@ -1,13 +1,13 @@
<template>
<div class="bg-primary flex p-4 top-0 z-10 sticky">
<div class="space-x-2 flex-1 inline-flex">
<div class="sticky top-0 z-10 flex p-4 bg-primary">
<div class="inline-flex flex-1 space-x-2">
<input
id="url"
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
class="bg-primaryLight border border-divider rounded text-secondaryDark w-full py-2 px-4 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
:placeholder="`${t('request.url')}`"
:disabled="connected"
@keyup.enter="onConnectClick"

View File

@@ -1,133 +1,168 @@
<template>
<div>
<SmartTabs styles="sticky bg-primary top-upperPrimaryStickyFold z-10">
<SmartTab :id="'query'" :label="`${t('tab.query')}`" :selected="true">
<AppSection label="query">
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperSecondaryStickyFold pl-4 z-10 sticky items-center justify-between gqlRunQuery"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.query") }}
</label>
<div class="flex">
<ButtonSecondary
:label="`${t('request.run')}`"
svg="play"
class="rounded-none !text-accent !hover:text-accentDark"
@click.native="runQuery()"
/>
<ButtonSecondary
ref="saveRequest"
:label="`${t('request.save')}`"
svg="save"
class="rounded-none"
@click.native="saveRequest"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/graphql"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:svg="`${prettifyQueryIcon}`"
@click.native="prettifyQuery"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:svg="`${copyQueryIcon}`"
@click.native="copyQuery"
/>
</div>
<SmartTab
:id="'query'"
:label="`${t('tab.query')}`"
:selected="true"
:indicator="gqlQueryString && gqlQueryString.length > 0 ? true : false"
>
<div
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold gqlRunQuery"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.query") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
:title="`${t(
'request.run'
)} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
:label="`${t('request.run')}`"
svg="play"
class="rounded-none !text-accent !hover:text-accentDark"
@click.native="runQuery()"
/>
<ButtonSecondary
ref="saveRequest"
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
:title="`${t(
'request.save'
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
:label="`${t('request.save')}`"
svg="save"
class="rounded-none"
@click.native="saveRequest"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/graphql"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearGQLQuery()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:svg="`${prettifyQueryIcon}`"
@click.native="prettifyQuery"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:svg="`${copyQueryIcon}`"
@click.native="copyQuery"
/>
</div>
<div ref="queryEditor"></div>
</AppSection>
</div>
<div ref="queryEditor"></div>
</SmartTab>
<SmartTab :id="'variables'" :label="`${t('tab.variables')}`">
<AppSection label="variables">
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.variables") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/graphql"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:svg="`${copyVariablesIcon}`"
@click.native="copyVariables"
/>
</div>
<SmartTab
:id="'variables'"
:label="`${t('tab.variables')}`"
:indicator="variableString && variableString.length > 0 ? true : false"
>
<div
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.variables") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/graphql"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearGQLVariables()"
/>
<ButtonSecondary
ref="prettifyRequest"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:svg="prettifyVariablesIcon"
@click.native="prettifyVariableString"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:svg="`${copyVariablesIcon}`"
@click.native="copyVariables"
/>
</div>
<div ref="variableEditor"></div>
</AppSection>
</div>
<div ref="variableEditor"></div>
</SmartTab>
<SmartTab :id="'headers'" :label="`${t('tab.headers')}`">
<AppSection label="headers">
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
>
<label class="font-semibold text-secondaryLight">
{{ t("tab.headers") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/graphql"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
svg="edit"
:class="{ '!text-accent': bulkMode }"
@click.native="bulkMode = !bulkMode"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
svg="plus"
:disabled="bulkMode"
@click.native="addRequestHeader"
/>
</div>
<SmartTab
:id="'headers'"
:label="`${t('tab.headers')}`"
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
>
<div
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("tab.headers") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/graphql"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
svg="edit"
:class="{ '!text-accent': bulkMode }"
@click.native="bulkMode = !bulkMode"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
svg="plus"
:disabled="bulkMode"
@click.native="addHeader"
/>
</div>
<div v-if="bulkMode" ref="bulkEditor"></div>
<div v-else>
<div
v-for="(header, index) in headers"
:key="`header-${String(index)}`"
class="divide-dividerLight divide-x border-b border-dividerLight flex"
>
<SmartAutoComplete
:placeholder="`${t('count.header', { count: index + 1 })}`"
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
styles="
</div>
<div v-if="bulkMode" ref="bulkEditor"></div>
<div v-else>
<div
v-for="(header, index) in workingHeaders"
:key="`header-${String(index)}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight"
>
<SmartAutoComplete
:placeholder="`${t('count.header', { count: index + 1 })}`"
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
styles="
bg-transparent
flex
flex-1
@@ -135,92 +170,90 @@
px-4
truncate
"
class="flex-1 !flex"
@input="
updateRequestHeader(index, {
key: $event,
value: header.value,
active: header.active,
})
"
/>
<input
class="bg-transparent flex flex-1 py-2 px-4"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"
autofocus
@change="
updateRequestHeader(index, {
key: header.key,
value: $event.target.value,
active: header.active,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
header.hasOwnProperty('active')
? header.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:svg="
header.hasOwnProperty('active')
? header.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateRequestHeader(index, {
key: header.key,
value: header.value,
active: !header.active,
})
"
/>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
svg="trash"
color="red"
@click.native="removeRequestHeader(index)"
/>
</span>
</div>
<div
v-if="headers.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
>
<img
:src="`/images/states/${$colorMode.value}/add_category.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="`${t('empty.headers')}`"
/>
<span class="text-center pb-4">
{{ t("empty.headers") }}
</span>
class="flex-1 !flex"
@input="
updateHeader(index, {
key: $event,
value: header.value,
active: header.active,
})
"
/>
<input
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"
autofocus
@change="
updateHeader(index, {
key: header.key,
value: $event.target.value,
active: header.active,
})
"
/>
<span>
<ButtonSecondary
:label="`${t('add.new')}`"
filled
svg="plus"
class="mb-4"
@click.native="addRequestHeader"
v-tippy="{ theme: 'tooltip' }"
:title="
header.hasOwnProperty('active')
? header.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:svg="
header.hasOwnProperty('active')
? header.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateHeader(index, {
key: header.key,
value: header.value,
active: !header.active,
})
"
/>
</div>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
svg="trash"
color="red"
@click.native="deleteHeader(index)"
/>
</span>
</div>
</AppSection>
<div
v-if="workingHeaders.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_category.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.headers')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.headers") }}
</span>
<ButtonSecondary
:label="`${t('add.new')}`"
filled
svg="plus"
class="mb-4"
@click.native="addHeader"
/>
</div>
</div>
</SmartTab>
</SmartTabs>
<CollectionsSaveRequest
mode="graphql"
:show="showSaveRequestModal"
@@ -230,10 +263,11 @@
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from "@nuxtjs/composition-api"
import { Ref, computed, reactive, ref, watch } from "@nuxtjs/composition-api"
import clone from "lodash/clone"
import * as gql from "graphql"
import { GQLHeader, makeGQLRequest } from "@hoppscotch/data"
import isEqual from "lodash/isEqual"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import {
useNuxt,
@@ -243,18 +277,15 @@ import {
useToast,
} from "~/helpers/utils/composables"
import {
addGQLHeader,
gqlHeaders$,
gqlQuery$,
gqlResponse$,
gqlURL$,
gqlVariables$,
removeGQLHeader,
setGQLHeaders,
setGQLQuery,
setGQLResponse,
setGQLVariables,
updateGQLHeader,
} from "~/newstore/GQLSession"
import { commonHeaders } from "~/helpers/headers"
import { GQLConnection } from "~/helpers/GQLConnection"
@@ -265,6 +296,8 @@ import { useCodemirror } from "~/helpers/editor/codemirror"
import jsonLinter from "~/helpers/editor/linting/json"
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
const t = useI18n()
@@ -273,33 +306,18 @@ const props = defineProps<{
}>()
const toast = useToast()
const nuxt = useNuxt()
const bulkMode = ref(false)
const bulkHeaders = ref("")
watch(bulkHeaders, () => {
try {
const transformation = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
value: item.substring(item.indexOf(":") + 1).trim(),
active: !item.trim().startsWith("//"),
}))
setGQLHeaders(transformation as GQLHeader[])
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const url = useReadonlyStream(gqlURL$, "")
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
const variableString = useStream(gqlVariables$, "", setGQLVariables)
const headers = useStream(gqlHeaders$, [], setGQLHeaders)
const bulkMode = ref(false)
const bulkHeaders = ref("")
const bulkEditor = ref<any | null>(null)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
useCodemirror(bulkEditor, bulkHeaders, {
extendedEditorConfig: {
mode: "text/x-yaml",
@@ -307,18 +325,192 @@ useCodemirror(bulkEditor, bulkHeaders, {
},
linter: null,
completer: null,
environmentHighlights: false,
})
// The functional headers list (the headers actually in the system)
const headers = useStream(gqlHeaders$, [], setGQLHeaders) as Ref<GQLHeader[]>
// The UI representation of the headers list (has the empty end header)
const workingHeaders = ref<GQLHeader[]>([
{
key: "",
value: "",
active: true,
},
])
// Rule: Working Headers always have one empty header or the last element is always an empty header
watch(workingHeaders, (headersList) => {
if (
headersList.length > 0 &&
headersList[headersList.length - 1].key !== ""
) {
workingHeaders.value.push({
key: "",
value: "",
active: true,
})
}
})
// Sync logic between headers and working headers
watch(
headers,
(newHeadersList) => {
// Sync should overwrite working headers
const filteredWorkingHeaders = workingHeaders.value.filter(
(e) => e.key !== ""
)
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
workingHeaders.value = newHeadersList
}
},
{ immediate: true }
)
watch(workingHeaders, (newWorkingHeaders) => {
const fixedHeaders = newWorkingHeaders.filter((e) => e.key !== "")
if (!isEqual(headers.value, fixedHeaders)) {
headers.value = fixedHeaders
}
})
// Bulk Editor Syncing with Working Headers
watch(bulkHeaders, () => {
try {
const transformation = bulkHeaders.value
.split("\n")
.filter((x) => x.trim().length > 0 && x.includes(":"))
.map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredHeaders = workingHeaders.value.filter((x) => x.key !== "")
if (!isEqual(filteredHeaders, transformation)) {
workingHeaders.value = transformation
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
watch(workingHeaders, (newHeadersList) => {
// If we are in bulk mode, don't apply direct changes
if (bulkMode.value) return
try {
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
bulkHeaders.value = filteredHeaders
.map((header) => {
return `${header.active ? "" : "#"}${header.key}: ${header.value}`
})
.join("\n")
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const addHeader = () => {
workingHeaders.value.push({
key: "",
value: "",
active: true,
})
}
const updateHeader = (index: number, header: GQLHeader) => {
workingHeaders.value = workingHeaders.value.map((h, i) =>
i === index ? header : h
)
}
const deleteHeader = (index: number) => {
const headersBeforeDeletion = clone(workingHeaders.value)
if (
!(
headersBeforeDeletion.length > 0 &&
index === headersBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingHeaders.value = headersBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingHeaders.value.splice(index, 1)
}
const clearContent = () => {
// set headers list to the initial state
workingHeaders.value = [
{
key: "",
value: "",
active: true,
},
]
bulkHeaders.value = ""
}
const activeGQLHeadersCount = computed(
() =>
headers.value.filter((x) => x.active && (x.key !== "" || x.value !== ""))
.length
)
const variableEditor = ref<any | null>(null)
useCodemirror(variableEditor, variableString, {
extendedEditorConfig: {
mode: "application/ld+json",
placeholder: `${t("request.variables")}`,
},
linter: jsonLinter,
completer: null,
})
useCodemirror(
variableEditor,
variableString,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
placeholder: `${t("request.variables")}`,
},
linter: computed(() =>
variableString.value.length > 0 ? jsonLinter : null
),
completer: null,
environmentHighlights: false,
})
)
const queryEditor = ref<any | null>(null)
const schemaString = useReadonlyStream(props.conn.schema$, null)
@@ -330,50 +522,16 @@ useCodemirror(queryEditor, gqlQueryString, {
},
linter: createGQLQueryLinter(schemaString),
completer: queryCompleter(schemaString),
environmentHighlights: false,
})
const copyQueryIcon = ref("copy")
const prettifyQueryIcon = ref("wand")
const copyVariablesIcon = ref("copy")
const prettifyQueryIcon = ref("wand")
const prettifyVariablesIcon = ref("wand")
const showSaveRequestModal = ref(false)
watch(
headers,
() => {
if (!bulkMode.value)
if (
(headers.value[headers.value.length - 1]?.key !== "" ||
headers.value[headers.value.length - 1]?.value !== "") &&
headers.value.length
)
addRequestHeader()
},
{ deep: true }
)
const editBulkHeadersLine = (index: number, item?: GQLHeader | null) => {
bulkHeaders.value = headers.value
.reduce((all, header, pIndex) => {
const current =
index === pIndex && item != null
? `${item.active ? "" : "//"}${item.key}: ${item.value}`
: `${header.active ? "" : "//"}${header.key}: ${header.value}`
return [...all, current]
}, [])
.join("\n")
}
const clearBulkEditor = () => {
bulkHeaders.value = ""
}
onMounted(() => {
if (!headers.value?.length) {
addRequestHeader()
}
})
const copyQuery = () => {
copyToClipboard(gqlQueryString.value)
copyQueryIcon.value = "check"
@@ -465,47 +623,28 @@ const copyVariables = () => {
setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
}
const addRequestHeader = () => {
const empty = { key: "", value: "", active: true }
const index = headers.value.length
addGQLHeader(empty)
editBulkHeadersLine(index, empty)
}
const updateRequestHeader = (
index: number,
item: { key: string; value: string; active: boolean }
) => {
updateGQLHeader(index, item)
editBulkHeadersLine(index, item)
}
const removeRequestHeader = (index: number) => {
const headersBeforeDeletion = headers.value
removeGQLHeader(index)
editBulkHeadersLine(index, null)
const deletedItem = headersBeforeDeletion[index]
if (deletedItem.key || deletedItem.value) {
toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
setGQLHeaders(headersBeforeDeletion as GQLHeader[])
editBulkHeadersLine(index, deletedItem)
toastObject.goAway(0)
},
},
],
})
const prettifyVariableString = () => {
try {
const jsonObj = JSON.parse(variableString.value)
variableString.value = JSON.stringify(jsonObj, null, 2)
prettifyVariablesIcon.value = "check"
} catch (e) {
console.error(e)
prettifyVariablesIcon.value = "info"
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
setTimeout(() => (prettifyVariablesIcon.value = "wand"), 1000)
}
const clearContent = () => {
headers.value = []
clearBulkEditor()
const clearGQLQuery = () => {
gqlQueryString.value = ""
}
const clearGQLVariables = () => {
variableString.value = ""
}
defineActionHandler("request.send-cancel", runQuery)
defineActionHandler("request.save", saveRequest)
defineActionHandler("request.reset", clearGQLQuery)
</script>

View File

@@ -1,15 +1,15 @@
<template>
<AppSection ref="response" label="response">
<div>
<div
v-if="responseString === 'loading'"
class="flex flex-col p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4"
>
<SmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div v-else-if="responseString">
<div
class="bg-primary border-b border-dividerLight flex flex-1 pl-4 top-0 z-10 sticky items-center justify-between"
class="sticky top-0 z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight"
>
<label class="font-semibold text-secondaryLight">
{{ t("response.title") }}
@@ -42,14 +42,14 @@
</div>
<div
v-else
class="flex flex-col flex-1 text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center flex-1 p-4 text-secondaryLight"
>
<div class="flex space-x-2 my-4 pb-4">
<div class="flex flex-col space-y-4 text-right items-end">
<span class="flex flex-1 items-center">
<div class="flex pb-4 my-4 space-x-2">
<div class="flex flex-col items-end space-y-4 text-right">
<span class="flex items-center flex-1">
{{ t("shortcut.general.command_menu") }}
</span>
<span class="flex flex-1 items-center">
<span class="flex items-center flex-1">
{{ t("shortcut.general.help_menu") }}
</span>
</div>
@@ -71,7 +71,7 @@
reverse
/>
</div>
</AppSection>
</div>
</template>
<script setup lang="ts">
@@ -105,6 +105,7 @@ useCodemirror(
},
linter: null,
completer: null,
environmentHighlights: false,
})
)
@@ -136,14 +137,3 @@ const downloadResponse = () => {
}, 1000)
}
</script>
<style lang="scss" scoped>
.shortcut-key {
@apply bg-dividerLight;
@apply rounded;
@apply ml-2;
@apply py-1;
@apply px-2;
@apply inline-flex;
}
</style>

View File

@@ -26,125 +26,34 @@
icon="book-open"
:label="`${t('tab.documentation')}`"
>
<AppSection label="docs">
<div
v-if="
queryFields.length === 0 &&
mutationFields.length === 0 &&
subscriptionFields.length === 0 &&
graphqlTypes.length === 0
"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
>
<img
:src="`/images/states/${$colorMode.value}/add_comment.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="`${t('empty.documentation')}`"
<div
v-if="
queryFields.length === 0 &&
mutationFields.length === 0 &&
subscriptionFields.length === 0 &&
graphqlTypes.length === 0
"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_comment.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.documentation')}`"
/>
<span class="mb-4 text-center">
{{ t("empty.documentation") }}
</span>
</div>
<div v-else>
<div class="sticky top-0 z-10 flex bg-primary">
<input
v-model="graphqlFieldsFilterText"
type="search"
autocomplete="off"
:placeholder="`${t('action.search')}`"
class="flex w-full p-4 py-2 bg-transparent"
/>
<span class="text-center mb-4">
{{ t("empty.documentation") }}
</span>
</div>
<div v-else>
<div class="bg-primary flex top-0 z-10 sticky">
<input
v-model="graphqlFieldsFilterText"
type="search"
autocomplete="off"
:placeholder="`${t('action.search')}`"
class="bg-transparent flex w-full p-4 py-2"
/>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/quickstart/graphql"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
</div>
</div>
<SmartTabs
ref="gqlTabs"
styles="border-t border-b border-dividerLight bg-primary sticky z-10 top-sidebarPrimaryStickyFold"
>
<div class="gqlTabs">
<SmartTab
v-if="queryFields.length > 0"
:id="'queries'"
:label="`${t('tab.queries')}`"
:selected="true"
class="divide-dividerLight divide-y"
>
<GraphqlField
v-for="(field, index) in filteredQueryFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
/>
</SmartTab>
<SmartTab
v-if="mutationFields.length > 0"
:id="'mutations'"
:label="`${t('graphql.mutations')}`"
class="divide-dividerLight divide-y"
>
<GraphqlField
v-for="(field, index) in filteredMutationFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
/>
</SmartTab>
<SmartTab
v-if="subscriptionFields.length > 0"
:id="'subscriptions'"
:label="`${t('graphql.subscriptions')}`"
class="divide-dividerLight divide-y"
>
<GraphqlField
v-for="(field, index) in filteredSubscriptionFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
/>
</SmartTab>
<SmartTab
v-if="graphqlTypes.length > 0"
:id="'types'"
ref="typesTab"
:label="`${t('tab.types')}`"
class="divide-dividerLight divide-y"
>
<GraphqlType
v-for="(type, index) in filteredGraphqlTypes"
:key="`type-${index}`"
:gql-type="type"
:gql-types="graphqlTypes"
:is-highlighted="isGqlTypeHighlighted(type)"
:highlighted-fields="getGqlTypeHighlightedFields(type)"
:jump-type-callback="handleJumpToType"
/>
</SmartTab>
</div>
</SmartTabs>
</div>
</AppSection>
</SmartTab>
<SmartTab :id="'schema'" icon="box" :label="`${t('tab.schema')}`">
<AppSection ref="schema" label="schema">
<div
v-if="schemaString"
class="bg-primary border-b border-dividerLight flex flex-1 pl-4 top-0 z-10 sticky items-center justify-between"
>
<label class="font-semibold text-secondaryLight">
{{ t("graphql.schema") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@@ -153,45 +62,132 @@
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="wrap-text"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
ref="downloadSchema"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
:svg="downloadSchemaIcon"
@click.native="downloadSchema"
/>
<ButtonSecondary
ref="copySchemaCode"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:svg="copySchemaIcon"
@click.native="copySchema"
/>
</div>
</div>
<div v-if="schemaString" ref="schemaEditor"></div>
<div
v-else
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
<SmartTabs
ref="gqlTabs"
styles="border-t border-b border-dividerLight bg-primary sticky z-10 top-sidebarPrimaryStickyFold"
>
<img
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="`${t('empty.schema')}`"
<div class="gqlTabs">
<SmartTab
v-if="queryFields.length > 0"
:id="'queries'"
:label="`${t('tab.queries')}`"
:selected="true"
class="divide-y divide-dividerLight"
>
<GraphqlField
v-for="(field, index) in filteredQueryFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
/>
</SmartTab>
<SmartTab
v-if="mutationFields.length > 0"
:id="'mutations'"
:label="`${t('graphql.mutations')}`"
class="divide-y divide-dividerLight"
>
<GraphqlField
v-for="(field, index) in filteredMutationFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
/>
</SmartTab>
<SmartTab
v-if="subscriptionFields.length > 0"
:id="'subscriptions'"
:label="`${t('graphql.subscriptions')}`"
class="divide-y divide-dividerLight"
>
<GraphqlField
v-for="(field, index) in filteredSubscriptionFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
/>
</SmartTab>
<SmartTab
v-if="graphqlTypes.length > 0"
:id="'types'"
ref="typesTab"
:label="`${t('tab.types')}`"
class="divide-y divide-dividerLight"
>
<GraphqlType
v-for="(type, index) in filteredGraphqlTypes"
:key="`type-${index}`"
:gql-type="type"
:gql-types="graphqlTypes"
:is-highlighted="isGqlTypeHighlighted(type)"
:highlighted-fields="getGqlTypeHighlightedFields(type)"
:jump-type-callback="handleJumpToType"
/>
</SmartTab>
</div>
</SmartTabs>
</div>
</SmartTab>
<SmartTab :id="'schema'" icon="box" :label="`${t('tab.schema')}`">
<div
v-if="schemaString"
class="sticky top-0 z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight"
>
<label class="font-semibold text-secondaryLight">
{{ t("graphql.schema") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/quickstart/graphql"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="wrap-text"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
ref="downloadSchema"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
:svg="downloadSchemaIcon"
@click.native="downloadSchema"
/>
<ButtonSecondary
ref="copySchemaCode"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:svg="copySchemaIcon"
@click.native="copySchema"
/>
<span class="text-center mb-4">
{{ t("empty.schema") }}
</span>
</div>
</AppSection>
</div>
<div v-if="schemaString" ref="schemaEditor"></div>
<div
v-else
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.schema')}`"
/>
<span class="mb-4 text-center">
{{ t("empty.schema") }}
</span>
</div>
</SmartTab>
</SmartTabs>
</template>
@@ -409,6 +405,7 @@ useCodemirror(
},
linter: null,
completer: null,
environmentHighlights: false,
})
)

View File

@@ -6,7 +6,7 @@
<span v-else-if="isEnum" class="text-accent">enum </span>
{{ gqlType.name }}
</div>
<div v-if="gqlType.description" class="text-secondaryLight py-2 type-desc">
<div v-if="gqlType.description" class="py-2 text-secondaryLight type-desc">
{{ gqlType.description }}
</div>
<div v-if="interfaces.length > 0">
@@ -18,7 +18,7 @@
<GraphqlTypeLink
:gql-type="gqlInterface"
:jump-type-callback="jumpTypeCallback"
class="border-divider border-l-2 pl-4"
class="pl-4 border-l-2 border-divider"
/>
</div>
</div>
@@ -29,7 +29,7 @@
:key="`child-${index}`"
:gql-type="child"
:jump-type-callback="jumpTypeCallback"
class="border-divider border-l-2 pl-4"
class="pl-4 border-l-2 border-divider"
/>
</div>
<div v-if="gqlType.getFields">
@@ -37,7 +37,7 @@
<GraphqlField
v-for="(field, index) in gqlType.getFields()"
:key="`field-${index}`"
class="border-divider border-l-2 pl-4"
class="pl-4 border-l-2 border-divider"
:gql-field="field"
:is-highlighted="isFieldHighlighted({ field })"
:jump-type-callback="jumpTypeCallback"
@@ -48,7 +48,7 @@
<div
v-for="(value, index) in gqlType.getValues()"
:key="`value-${index}`"
class="border-divider border-l-2 pl-4"
class="pl-4 border-l-2 border-divider"
v-text="value.name"
></div>
</div>

View File

@@ -2,13 +2,19 @@
<div class="flex flex-col group">
<div class="flex items-center">
<span
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 pl-4 transition group-hover:text-secondaryDark"
class="flex flex-1 min-w-0 py-2 pl-4 pr-2 transition cursor-pointer group-hover:text-secondaryDark"
data-testid="restore_history_entry"
@click="useEntry"
>
<span class="truncate">
{{ entry.request.url }}
</span>
<tippy
v-if="entry.updatedOn"
theme="tooltip"
:delay="[500, 20]"
:content="`${new Date(entry.updatedOn).toLocaleString()}`"
/>
</span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@@ -36,11 +42,11 @@
@click.native="$emit('toggle-star')"
/>
</div>
<div class="flex flex-col">
<div class="flex flex-col text-tiny">
<span
v-for="(line, index) in query"
:key="`line-${index}`"
class="cursor-pointer font-mono text-secondaryLight px-4 truncate whitespace-pre"
class="px-4 font-mono truncate whitespace-pre cursor-pointer text-secondaryLight"
data-testid="restore_history_entry"
@click="useEntry"
>{{ line }}</span
@@ -49,53 +55,39 @@
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
PropType,
ref,
} from "@nuxtjs/composition-api"
<script setup lang="ts">
import { computed, ref } from "@nuxtjs/composition-api"
import { makeGQLRequest } from "@hoppscotch/data"
import { setGQLSession } from "~/newstore/GQLSession"
import { GQLHistoryEntry } from "~/newstore/history"
export default defineComponent({
props: {
entry: { type: Object as PropType<GQLHistoryEntry>, default: () => {} },
showMore: Boolean,
},
setup(props) {
const expand = ref(false)
const props = defineProps<{
entry: GQLHistoryEntry
showMore: Boolean
}>()
const query = computed(() =>
expand.value
? (props.entry.request.query.split("\n") as string[])
: (props.entry.request.query
.split("\n")
.slice(0, 2)
.concat(["..."]) as string[])
)
const expand = ref(false)
const useEntry = () => {
setGQLSession({
request: makeGQLRequest({
name: props.entry.request.name,
url: props.entry.request.url,
headers: props.entry.request.headers,
query: props.entry.request.query,
variables: props.entry.request.variables,
}),
schema: "",
response: props.entry.response,
})
}
const query = computed(() =>
expand.value
? (props.entry.request.query.split("\n") as string[])
: (props.entry.request.query
.split("\n")
.slice(0, 2)
.concat(["..."]) as string[])
)
return {
expand,
query,
useEntry,
}
},
})
const useEntry = () => {
setGQLSession({
request: makeGQLRequest({
name: props.entry.request.name,
url: props.entry.request.url,
headers: props.entry.request.headers,
query: props.entry.request.query,
variables: props.entry.request.variables,
}),
schema: "",
response: props.entry.response,
})
}
</script>

View File

@@ -1,19 +1,19 @@
<template>
<AppSection label="history">
<div class="bg-primary border-b border-dividerLight flex top-0 z-10 sticky">
<div>
<div class="sticky top-0 z-10 flex border-b bg-primary border-dividerLight">
<input
v-model="filterText"
type="search"
autocomplete="off"
class="bg-transparent flex w-full p-4 py-2"
:placeholder="`${$t('action.search')}`"
class="flex w-full p-4 py-2 bg-transparent"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/history"
blank
:title="$t('app.wiki')"
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
@@ -21,67 +21,94 @@
data-testid="clear_history"
:disabled="history.length === 0"
svg="trash-2"
:title="$t('action.clear_all')"
:title="t('action.clear_all')"
@click.native="confirmRemove = true"
/>
</div>
</div>
<div class="flex flex-col">
<div v-for="(entry, index) in filteredHistory" :key="`entry-${index}`">
<HistoryRestCard
v-if="page == 'rest'"
:id="index"
:entry="entry"
:show-more="showMore"
@toggle-star="toggleStar(entry)"
@delete-entry="deleteHistory(entry)"
@use-entry="useHistory(entry)"
/>
<HistoryGraphqlCard
v-if="page == 'graphql'"
:entry="entry"
:show-more="showMore"
@toggle-star="toggleStar(entry)"
@delete-entry="deleteHistory(entry)"
@use-entry="useHistory(entry)"
/>
</div>
<details
v-for="(
filteredHistoryGroup, filteredHistoryGroupIndex
) in filteredHistoryGroups"
:key="`filteredHistoryGroup-${filteredHistoryGroupIndex}`"
class="flex flex-col"
open
>
<summary
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
>
<span
class="px-4 py-2 truncate transition group-hover:text-secondary capitalize-first"
>
{{ filteredHistoryGroupIndex }}
</span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="trash"
color="red"
:title="$t('action.remove')"
class="hidden group-hover:inline-flex"
@click.native="deleteBattleHistoryEntry(filteredHistoryGroup)"
/>
</summary>
<div
v-for="(entry, index) in filteredHistoryGroup"
:key="`entry-${index}`"
>
<component
:is="page == 'rest' ? 'HistoryRestCard' : 'HistoryGraphqlCard'"
:id="index"
:entry="entry"
:show-more="showMore"
@toggle-star="toggleStar(entry)"
@delete-entry="deleteHistory(entry)"
@use-entry="useHistory(entry)"
/>
</div>
</details>
</div>
<div
v-if="!(filteredHistory.length !== 0 || history.length === 0)"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<i class="opacity-75 pb-2 material-icons">manage_search</i>
<span class="text-center">
{{ $t("state.nothing_found") }} "{{ filterText }}"
<i class="pb-2 opacity-75 material-icons">manage_search</i>
<span class="my-2 text-center">
{{ t("state.nothing_found") }} "{{ filterText }}"
</span>
</div>
<div
v-if="history.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/history.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="$t('empty.history')"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.history')}`"
/>
<span class="text-center mb-4">
{{ $t("empty.history") }}
<span class="mb-4 text-center">
{{ t("empty.history") }}
</span>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="`${$t('confirm.remove_history')}`"
:title="`${t('confirm.remove_history')}`"
@hide-modal="confirmRemove = false"
@resolve="clearHistory"
/>
</AppSection>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "@nuxtjs/composition-api"
import { useReadonlyStream } from "~/helpers/utils/composables"
<script setup lang="ts">
import { computed, ref } from "@nuxtjs/composition-api"
import * as timeago from "timeago.js"
import { safelyExtractRESTRequest } from "@hoppscotch/data"
import {
useI18n,
useReadonlyStream,
useToast,
} from "~/helpers/utils/composables"
import {
restHistory$,
graphqlHistory$,
@@ -94,66 +121,87 @@ import {
RESTHistoryEntry,
GQLHistoryEntry,
} from "~/newstore/history"
import { setRESTRequest } from "~/newstore/RESTSession"
import { getDefaultRESTRequest, setRESTRequest } from "~/newstore/RESTSession"
export default defineComponent({
props: {
page: { type: String as PropType<"rest" | "graphql">, default: null },
},
setup(props) {
return {
history: useReadonlyStream<RESTHistoryEntry[] | GQLHistoryEntry[]>(
props.page === "rest" ? restHistory$ : graphqlHistory$,
[]
),
}
},
data() {
return {
filterText: "",
showMore: false,
confirmRemove: false,
}
},
computed: {
filteredHistory(): any[] {
const filteringHistory = this.history as Array<
RESTHistoryEntry | GQLHistoryEntry
>
const props = defineProps<{
page: "rest" | "graphql"
}>()
return filteringHistory.filter(
(entry: RESTHistoryEntry | GQLHistoryEntry) => {
const filterText = this.filterText.toLowerCase()
return Object.keys(entry).some((key) => {
let value = entry[key as keyof typeof entry]
if (value) {
value = `${value}`
return value.toLowerCase().includes(filterText)
}
return false
})
}
)
},
},
methods: {
clearHistory() {
if (this.page === "rest") clearRESTHistory()
else clearGraphqlHistory()
this.$toast.success(`${this.$t("state.history_deleted")}`)
},
useHistory(entry: any) {
if (this.page === "rest") setRESTRequest(entry.request)
},
deleteHistory(entry: any) {
if (this.page === "rest") deleteRESTHistoryEntry(entry)
else deleteGraphqlHistoryEntry(entry)
this.$toast.success(`${this.$t("state.deleted")}`)
},
toggleStar(entry: any) {
if (this.page === "rest") toggleRESTHistoryEntryStar(entry)
else toggleGraphqlHistoryEntryStar(entry)
},
},
const filterText = ref("")
const showMore = ref(false)
const confirmRemove = ref(false)
const toast = useToast()
const t = useI18n()
const groupByDate = (array: any[], key: string) => {
return array.reduce((rv: any, x: any) => {
;(rv[timeago.format(x[key])] = rv[timeago.format(x[key])] || []).push(x)
return rv
}, {})
}
const history = useReadonlyStream<RESTHistoryEntry[] | GQLHistoryEntry[]>(
props.page === "rest" ? restHistory$ : graphqlHistory$,
[]
)
const filteredHistory = computed(() => {
const regExp = new RegExp(filterText.value, "gi")
const check = (obj: any) => {
if (obj !== null && typeof obj === "object") {
return Object.values(obj).some(check)
}
if (Array.isArray(obj)) {
return obj.some(check)
}
return (
(typeof obj === "string" || typeof obj === "number") &&
regExp.test(obj as string)
)
}
return (history.value as Array<RESTHistoryEntry | GQLHistoryEntry>).filter(
check
)
})
const filteredHistoryGroups = computed(() =>
groupByDate(filteredHistory.value, "updatedOn")
)
const clearHistory = () => {
if (props.page === "rest") clearRESTHistory()
else clearGraphqlHistory()
toast.success(`${t("state.history_deleted")}`)
}
const useHistory = (entry: any) => {
if (props.page === "rest")
setRESTRequest(
safelyExtractRESTRequest(entry.request, getDefaultRESTRequest())
)
}
const deleteBattleHistoryEntry = (entries: number[]) => {
if (props.page === "rest") {
entries.forEach((entry: any) => {
deleteRESTHistoryEntry(entry)
})
} else {
entries.forEach((entry: any) => {
deleteGraphqlHistoryEntry(entry)
})
}
toast.success(`${t("state.deleted")}`)
}
const deleteHistory = (entry: any) => {
if (props.page === "rest") deleteRESTHistoryEntry(entry)
else deleteGraphqlHistoryEntry(entry)
toast.success(`${t("state.deleted")}`)
}
const toggleStar = (entry: any) => {
if (props.page === "rest") toggleRESTHistoryEntryStar(entry)
else toggleGraphqlHistoryEntryStar(entry)
}
</script>

View File

@@ -1,7 +1,8 @@
<template>
<div class="flex items-stretch group">
<span
class="cursor-pointer flex px-2 w-16 items-center justify-center truncate"
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
:class="entryStatus.className"
data-testid="restore_history_entry"
:title="`${duration}`"
@@ -10,14 +11,19 @@
{{ entry.request.method }}
</span>
<span
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hover:text-secondaryDark"
class="flex flex-1 min-w-0 py-2 pr-2 transition cursor-pointer group-hover:text-secondaryDark"
data-testid="restore_history_entry"
:title="`${duration}`"
@click="$emit('use-entry')"
>
<span class="truncate">
{{ entry.request.endpoint }}
</span>
<tippy
v-if="entry.updatedOn"
theme="tooltip"
:delay="[500, 20]"
:content="`${new Date(entry.updatedOn).toLocaleString()}`"
/>
</span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@@ -40,46 +46,36 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { computed } from "@nuxtjs/composition-api"
import findStatusGroup from "~/helpers/findStatusGroup"
import { useI18n } from "~/helpers/utils/composables"
import { RESTHistoryEntry } from "~/newstore/history"
export default defineComponent({
props: {
entry: { type: Object as PropType<RESTHistoryEntry>, default: () => {} },
showMore: Boolean,
},
setup(props) {
const t = useI18n()
const props = defineProps<{
entry: RESTHistoryEntry
showMore: Boolean
}>()
const duration = computed(() => {
if (props.entry.responseMeta.duration) {
const responseDuration = props.entry.responseMeta.duration
if (!responseDuration) return ""
const t = useI18n()
return responseDuration > 0
? `${t("request.duration")}: ${responseDuration}ms`
: t("error.no_duration")
} else return t("error.no_duration")
})
const duration = computed(() => {
if (props.entry.responseMeta.duration) {
const responseDuration = props.entry.responseMeta.duration
if (!responseDuration) return ""
const entryStatus = computed(() => {
const foundStatusGroup = findStatusGroup(
props.entry.responseMeta.statusCode
)
return (
foundStatusGroup || {
className: "",
}
)
})
return responseDuration > 0
? `${t("request.duration")}: ${responseDuration}ms`
: t("error.no_duration")
} else return t("error.no_duration")
})
return {
duration,
entryStatus,
const entryStatus = computed(() => {
const foundStatusGroup = findStatusGroup(props.entry.responseMeta.statusCode)
return (
foundStatusGroup || {
className: "",
}
},
)
})
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold text-secondaryLight">
@@ -17,7 +17,7 @@
<template #trigger>
<span class="select-wrapper">
<ButtonSecondary
class="rounded-none ml-2 pr-8"
class="pr-8 ml-2 rounded-none"
:label="authName"
/>
</span>
@@ -29,9 +29,12 @@
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:active="authName === 'None'"
@click.native="
authType = 'none'
$refs.authTypeOptions.tippy().hide()
() => {
authType = 'none'
authTypeOptions.tippy().hide()
}
"
/>
<SmartItem
@@ -41,9 +44,12 @@
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:active="authName === 'Basic Auth'"
@click.native="
authType = 'basic'
$refs.authTypeOptions.tippy().hide()
() => {
authType = 'basic'
authTypeOptions.tippy().hide()
}
"
/>
<SmartItem
@@ -53,9 +59,12 @@
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:active="authName === 'Bearer'"
@click.native="
authType = 'bearer'
$refs.authTypeOptions.tippy().hide()
() => {
authType = 'bearer'
authTypeOptions.tippy().hide()
}
"
/>
<SmartItem
@@ -65,9 +74,27 @@
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:active="authName === 'OAuth 2.0'"
@click.native="
authType = 'oauth-2'
$refs.authTypeOptions.tippy().hide()
() => {
authType = 'oauth-2'
authTypeOptions.tippy().hide()
}
"
/>
<SmartItem
label="API key"
:icon="
authName === 'API key'
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:active="authName === 'API key'"
@click.native="
() => {
authType = 'api-key'
authTypeOptions.tippy().hide()
}
"
/>
</tippy>
@@ -103,15 +130,15 @@
</div>
<div
v-if="authType === 'none'"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/login.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="$t('empty.authorization')"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${$t('empty.authorization')}`"
/>
<span class="text-center pb-4">
<span class="pb-4 text-center">
{{ $t("empty.authorization") }}
</span>
<ButtonSecondary
@@ -124,104 +151,135 @@
class="mb-4"
/>
</div>
<div v-if="authType === 'basic'" class="border-b border-dividerLight flex">
<div class="border-r border-dividerLight w-2/3">
<div class="border-b border-dividerLight flex">
<SmartEnvInput
v-model="basicUsername"
:placeholder="$t('authorization.username')"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
<div v-else class="flex border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">
<div v-if="authType === 'basic'">
<div class="flex border-b border-dividerLight">
<SmartEnvInput
v-model="basicUsername"
:placeholder="$t('authorization.username')"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
</div>
<div class="flex border-b border-dividerLight">
<SmartEnvInput
v-model="basicPassword"
:placeholder="$t('authorization.password')"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
</div>
</div>
<div class="border-b border-dividerLight flex">
<SmartEnvInput
v-model="basicPassword"
:placeholder="$t('authorization.password')"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
<div v-if="authType === 'bearer'">
<div class="flex border-b border-dividerLight">
<SmartEnvInput
v-model="bearerToken"
placeholder="Token"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
</div>
</div>
<div v-if="authType === 'oauth-2'">
<div class="flex border-b border-dividerLight">
<SmartEnvInput
v-model="oauth2Token"
placeholder="Token"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
</div>
<HttpOAuth2Authorization />
</div>
<div v-if="authType === 'api-key'">
<div class="flex border-b border-dividerLight">
<SmartEnvInput
v-model="apiKey"
placeholder="Key"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
</div>
<div class="flex border-b border-dividerLight">
<SmartEnvInput
v-model="apiValue"
placeholder="Value"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
</div>
<div class="flex items-center border-b border-dividerLight">
<label class="ml-4 text-secondaryLight">
{{ $t("authorization.pass_key_by") }}
</label>
<tippy
ref="addToOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<span class="select-wrapper">
<ButtonSecondary
:label="addTo || $t('state.none')"
class="pr-8 ml-2 rounded-none"
/>
</span>
</template>
<SmartItem
:icon="
addTo === 'Headers'
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:active="addTo === 'Headers'"
:label="'Headers'"
@click.native="
() => {
addTo = 'Headers'
addToOptions.tippy().hide()
}
"
/>
<SmartItem
:icon="
addTo === 'Query params'
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:active="addTo === 'Query params'"
:label="'Query params'"
@click.native="
() => {
addTo = 'Query params'
addToOptions.tippy().hide()
}
"
/>
</tippy>
</div>
</div>
</div>
<div
class="bg-primary h-full top-upperTertiaryStickyFold min-w-46 max-w-1/3 p-4 z-9 sticky overflow-auto"
class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
>
<div class="p-2">
<div class="text-secondaryLight pb-2">
{{ $t("helpers.authorization") }}
</div>
<SmartAnchor
class="link"
:label="`${$t('authorization.learn')} \xA0 →`"
to="https://docs.hoppscotch.io/features/authorization"
blank
/>
</div>
</div>
</div>
<div v-if="authType === 'bearer'" class="border-b border-dividerLight flex">
<div class="border-r border-dividerLight w-2/3">
<div class="border-b border-dividerLight flex">
<SmartEnvInput
v-model="bearerToken"
placeholder="Token"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
</div>
</div>
<div
class="bg-primary h-full top-upperTertiaryStickyFold min-w-46 max-w-1/3 p-4 z-9 sticky overflow-auto"
>
<div class="p-2">
<div class="text-secondaryLight pb-2">
{{ $t("helpers.authorization") }}
</div>
<SmartAnchor
class="link"
:label="`${$t('authorization.learn')} \xA0 →`"
to="https://docs.hoppscotch.io/features/authorization"
blank
/>
</div>
</div>
</div>
<div
v-if="authType === 'oauth-2'"
class="border-b border-dividerLight flex"
>
<div class="border-r border-dividerLight w-2/3">
<div class="border-b border-dividerLight flex">
<SmartEnvInput
v-model="oauth2Token"
placeholder="Token"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
</div>
<HttpOAuth2Authorization />
</div>
<div
class="bg-primary h-full top-upperTertiaryStickyFold min-w-46 max-w-1/3 p-4 z-9 sticky overflow-auto"
>
<div class="p-2">
<div class="text-secondaryLight pb-2">
{{ $t("helpers.authorization") }}
</div>
<SmartAnchor
class="link"
:label="`${$t('authorization.learn')} \xA0 →`"
to="https://docs.hoppscotch.io/features/authorization"
blank
/>
<div class="pb-2 text-secondaryLight">
{{ $t("helpers.authorization") }}
</div>
<SmartAnchor
class="link"
:label="`${$t('authorization.learn')} \xA0 →`"
to="https://docs.hoppscotch.io/features/authorization"
blank
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, Ref } from "@nuxtjs/composition-api"
import { computed, defineComponent, ref, Ref } from "@nuxtjs/composition-api"
import {
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey,
} from "@hoppscotch/data"
import { pluckRef, useStream } from "~/helpers/utils/composables"
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
@@ -239,6 +297,7 @@ export default defineComponent({
if (authType.value === "basic") return "Basic Auth"
else if (authType.value === "bearer") return "Bearer"
else if (authType.value === "oauth-2") return "OAuth 2.0"
else if (authType.value === "api-key") return "API key"
else return "None"
})
const authActive = pluckRef(auth, "authActive")
@@ -246,6 +305,15 @@ export default defineComponent({
const basicPassword = pluckRef(auth as Ref<HoppRESTAuthBasic>, "password")
const bearerToken = pluckRef(auth as Ref<HoppRESTAuthBearer>, "token")
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
const apiKey = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "key")
const apiValue = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "value")
const addTo = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "addTo")
if (typeof addTo.value === "undefined") {
addTo.value = "Headers"
apiKey.value = ""
apiValue.value = ""
}
const URLExcludes = useSetting("URL_EXCLUDES")
const clearContent = () => {
auth.value = {
@@ -264,6 +332,11 @@ export default defineComponent({
oauth2Token,
URLExcludes,
clearContent,
apiKey,
apiValue,
addTo,
authTypeOptions: ref<any | null>(null),
addToOptions: ref<any | null>(null),
}
},
})

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold text-secondaryLight">
@@ -18,7 +18,7 @@
<span class="select-wrapper">
<ButtonSecondary
:label="contentType || $t('state.none').toLowerCase()"
class="rounded-none ml-2 pr-8"
class="pr-8 ml-2 rounded-none"
/>
</span>
</template>
@@ -50,18 +50,21 @@
</span>
</div>
<HttpBodyParameters v-if="contentType === 'multipart/form-data'" />
<HttpURLEncodedParams
v-else-if="contentType === 'application/x-www-form-urlencoded'"
/>
<HttpRawBody v-else-if="contentType !== null" :content-type="contentType" />
<div
v-if="contentType == null"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/upload_single_file.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="$t('empty.body')"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${$t('empty.body')}`"
/>
<span class="text-center pb-4">
<span class="pb-4 text-center">
{{ $t("empty.body") }}
</span>
<ButtonSecondary

View File

@@ -1,7 +1,7 @@
<template>
<AppSection label="bodyParameters">
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperTertiaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperTertiaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ $t("request.body") }}
@@ -29,9 +29,9 @@
</div>
</div>
<div
v-for="(param, index) in bodyParams"
v-for="(param, index) in workingParams"
:key="`param-${index}`"
class="divide-dividerLight divide-x border-b border-dividerLight flex"
class="flex border-b divide-x divide-dividerLight border-dividerLight"
>
<SmartEnvInput
v-model="param.key"
@@ -54,13 +54,12 @@
/>
<div v-if="param.isFile" class="file-chips-container hide-scrollbar">
<div class="space-x-2 file-chips-wrapper">
<SmartDeletableChip
<SmartFileChip
v-for="(file, fileIndex) in param.value"
:key="`param-${index}-file-${fileIndex}`"
@chip-delete="chipDelete(index, fileIndex)"
>
{{ file.name }}
</SmartDeletableChip>
</SmartFileChip>
</div>
</div>
<span v-else class="flex flex-1">
@@ -85,21 +84,17 @@
/>
</span>
<span>
<label for="attachment" class="p-0">
<ButtonSecondary
class="w-full"
svg="paperclip"
@click.native="$refs.attachment[index].click()"
<label :for="`attachment${index}`" class="p-0">
<input
:id="`attachment${index}`"
:ref="`attachment${index}`"
:name="`attachment${index}`"
type="file"
multiple
class="p-1 transition cursor-pointer file:transition file:cursor-pointer text-secondaryLight hover:text-secondaryDark file:mr-2 file:py-1 file:px-4 file:rounded file:border-0 file:text-tiny text-tiny file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
@change="setRequestAttachment(index, param, $event)"
/>
</label>
<input
ref="attachment"
class="input"
name="attachment"
type="file"
multiple
@change="setRequestAttachment(index, param, $event)"
/>
</span>
<span>
<ButtonSecondary
@@ -141,15 +136,15 @@
</div>
<div
v-if="bodyParams.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/upload_single_file.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="$t('empty.body')"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${$t('empty.body')}`"
/>
<span class="text-center pb-4">
<span class="pb-4 text-center">
{{ $t("empty.body") }}
</span>
<ButtonSecondary
@@ -160,103 +155,156 @@
@click.native="addBodyParam"
/>
</div>
</AppSection>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, Ref, watch } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { ref, Ref, watch } from "@nuxtjs/composition-api"
import { FormDataKeyValue } from "@hoppscotch/data"
import { pluckRef } from "~/helpers/utils/composables"
import {
addFormDataEntry,
deleteAllFormDataEntries,
deleteFormDataEntry,
updateFormDataEntry,
useRESTRequestBody,
} from "~/newstore/RESTSession"
import isEqual from "lodash/isEqual"
import { clone } from "lodash"
import { pluckRef, useI18n, useToast } from "~/helpers/utils/composables"
import { useRESTRequestBody } from "~/newstore/RESTSession"
export default defineComponent({
setup() {
const bodyParams = pluckRef<any, any>(useRESTRequestBody(), "body") as Ref<
FormDataKeyValue[]
>
const t = useI18n()
const addBodyParam = () => {
addFormDataEntry({ key: "", value: "", active: true, isFile: false })
}
const toast = useToast()
const updateBodyParam = (index: number, entry: FormDataKeyValue) => {
updateFormDataEntry(index, entry)
}
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const deleteBodyParam = (index: number) => {
deleteFormDataEntry(index)
}
const bodyParams = pluckRef<any, any>(useRESTRequestBody(), "body") as Ref<
FormDataKeyValue[]
>
const clearContent = () => {
deleteAllFormDataEntries()
}
// The UI representation of the parameters list (has the empty end param)
const workingParams = ref<FormDataKeyValue[]>([
{
key: "",
value: "",
active: true,
isFile: false,
},
])
const chipDelete = (paramIndex: number, fileIndex: number) => {
const entry = bodyParams.value[paramIndex]
if (entry.isFile) {
entry.value.splice(fileIndex, 1)
if (entry.value.length === 0) {
updateFormDataEntry(paramIndex, {
...entry,
isFile: false,
value: "",
})
return
}
}
// Rule: Working Params always have last element is always an empty param
watch(workingParams, (paramsList) => {
if (paramsList.length > 0 && paramsList[paramsList.length - 1].key !== "") {
workingParams.value.push({
key: "",
value: "",
active: true,
isFile: false,
})
}
})
updateFormDataEntry(paramIndex, entry)
}
const setRequestAttachment = (
index: number,
entry: FormDataKeyValue,
event: InputEvent
) => {
const fileEntry: FormDataKeyValue = {
...entry,
isFile: true,
value: Array.from((event.target as HTMLInputElement).files!),
}
updateFormDataEntry(index, fileEntry)
}
watch(
bodyParams,
() => {
if (
bodyParams.value.length > 0 &&
(bodyParams.value[bodyParams.value.length - 1].key !== "" ||
bodyParams.value[bodyParams.value.length - 1].value !== "")
)
addBodyParam()
},
{ deep: true }
// Sync logic between params and working params
watch(
bodyParams,
(newParamsList) => {
// Sync should overwrite working params
const filteredWorkingParams = workingParams.value.filter(
(e) => e.key !== ""
)
onMounted(() => {
if (!bodyParams.value?.length) {
addBodyParam()
}
})
return {
bodyParams,
addBodyParam,
updateBodyParam,
deleteBodyParam,
clearContent,
setRequestAttachment,
chipDelete,
if (!isEqual(newParamsList, filteredWorkingParams)) {
workingParams.value = newParamsList
}
},
{ immediate: true }
)
watch(workingParams, (newWorkingParams) => {
const fixedParams = newWorkingParams.filter((e) => e.key !== "")
if (!isEqual(bodyParams.value, fixedParams)) {
bodyParams.value = fixedParams
}
})
const addBodyParam = () => {
workingParams.value.push({
key: "",
value: "",
active: true,
isFile: false,
})
}
const updateBodyParam = (index: number, param: FormDataKeyValue) => {
workingParams.value = workingParams.value.map((h, i) =>
i === index ? param : h
)
}
const deleteBodyParam = (index: number) => {
const paramsBeforeDeletion = clone(workingParams.value)
if (
!(
paramsBeforeDeletion.length > 0 &&
index === paramsBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingParams.value = paramsBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingParams.value.splice(index, 1)
}
const clearContent = () => {
// set params list to the initial state
workingParams.value = [
{
key: "",
value: "",
active: true,
isFile: false,
},
]
}
const setRequestAttachment = (
index: number,
entry: FormDataKeyValue,
event: InputEvent
) => {
// check if file exists or not
if ((event.target as HTMLInputElement).files?.length === 0) {
updateBodyParam(index, {
...entry,
isFile: false,
value: "",
})
return
}
const fileEntry: FormDataKeyValue = {
...entry,
isFile: true,
value: Array.from((event.target as HTMLInputElement).files!),
}
updateBodyParam(index, fileEntry)
}
</script>
<style scoped lang="scss">
@@ -268,8 +316,7 @@ export default defineComponent({
.file-chips-wrapper {
@apply flex;
@apply px-4;
@apply py-1;
@apply p-1;
@apply w-0;
}
}

View File

@@ -13,35 +13,56 @@
<template #trigger>
<span class="select-wrapper">
<ButtonSecondary
:label="codegens.find((x) => x.id === codegenType).name"
:label="
CodegenDefinitions.find((x) => x.name === codegenType).caption
"
outline
class="flex-1 pr-8"
/>
</span>
</template>
<SmartItem
v-for="(gen, index) in codegens"
:key="`gen-${index}`"
:label="gen.name"
:info-icon="gen.id === codegenType ? 'done' : ''"
:active-info-icon="gen.id === codegenType"
@click.native="
() => {
codegenType = gen.id
options.tippy().hide()
}
"
/>
<div class="flex flex-col space-y-2">
<div class="sticky top-0">
<input
v-model="searchQuery"
type="search"
autocomplete="off"
class="flex w-full p-4 py-2 !bg-popover input"
:placeholder="`${t('action.search')}`"
/>
</div>
<div class="flex flex-col">
<SmartItem
v-for="codegen in filteredCodegenDefinitions"
:key="codegen.name"
:label="codegen.caption"
:info-icon="codegen.name === codegenType ? 'done' : ''"
:active-info-icon="codegen.name === codegenType"
@click.native="
() => {
codegenType = codegen.name
options.tippy().hide()
}
"
/>
</div>
</div>
</tippy>
<div class="flex flex-1 justify-between">
<div class="flex justify-between flex-1">
<label for="generatedCode" class="p-4">
{{ t("request.generated_code") }}
</label>
</div>
<div
v-if="codegenType"
v-if="errorState"
class="bg-primaryLight rounded font-mono w-full py-2 px-4 text-red-400 overflow-auto whitespace-normal"
>
{{ t("error.something_went_wrong") }}
</div>
<div
v-else-if="codegenType"
ref="generatedCode"
class="border border-dividerLight rounded"
class="border rounded border-dividerLight"
></div>
</div>
</template>
@@ -63,13 +84,22 @@
<script setup lang="ts">
import { computed, ref, watch } from "@nuxtjs/composition-api"
import { codegens, generateCodegenContext } from "~/helpers/codegen/codegen"
import * as O from "fp-ts/Option"
import { makeRESTRequest } from "@hoppscotch/data"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { getEffectiveRESTRequest } from "~/helpers/utils/EffectiveURL"
import { getCurrentEnvironment } from "~/newstore/environments"
import {
getEffectiveRESTRequest,
resolvesEnvsInBody,
} from "~/helpers/utils/EffectiveURL"
import { Environment, getAggregateEnvs } from "~/newstore/environments"
import { getRESTRequest } from "~/newstore/RESTSession"
import { useI18n, useToast } from "~/helpers/utils/composables"
import {
CodegenDefinitions,
CodegenName,
generateCode,
} from "~/helpers/new-codegen"
const t = useI18n()
@@ -86,18 +116,44 @@ const toast = useToast()
const options = ref<any | null>(null)
const request = ref(getRESTRequest())
const codegenType = ref("curl")
const codegenType = ref<CodegenName>("shell-curl")
const copyIcon = ref("copy")
const errorState = ref(false)
const requestCode = computed(() => {
const effectiveRequest = getEffectiveRESTRequest(
request.value as any,
getCurrentEnvironment()
const aggregateEnvs = getAggregateEnvs()
const env: Environment = {
name: "Env",
variables: aggregateEnvs,
}
const effectiveRequest = getEffectiveRESTRequest(request.value, env)
if (!props.show) return ""
const result = generateCode(
codegenType.value,
makeRESTRequest({
...effectiveRequest,
body: resolvesEnvsInBody(effectiveRequest.body, env),
headers: effectiveRequest.effectiveFinalHeaders.map((header) => ({
...header,
active: true,
})),
params: effectiveRequest.effectiveFinalParams.map((param) => ({
...param,
active: true,
})),
endpoint: effectiveRequest.effectiveFinalURL,
})
)
return codegens
.find((x) => x.id === codegenType.value)!
.generator(generateCodegenContext(effectiveRequest))
if (O.isSome(result)) {
errorState.value = false
return result.value
} else {
errorState.value = true
return ""
}
})
const generatedCode = ref<any | null>(null)
@@ -109,6 +165,7 @@ useCodemirror(generatedCode, requestCode, {
},
linter: null,
completer: null,
environmentHighlights: false,
})
watch(
@@ -128,4 +185,12 @@ const copyRequestCode = () => {
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
const searchQuery = ref("")
const filteredCodegenDefinitions = computed(() => {
return CodegenDefinitions.filter((obj) =>
Object.values(obj).some((val) => val.includes(searchQuery.value))
)
})
</script>

View File

@@ -1,7 +1,7 @@
<template>
<AppSection label="headers">
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.header_list") }}
@@ -39,9 +39,9 @@
<div v-if="bulkMode" ref="bulkEditor"></div>
<div v-else>
<div
v-for="(header, index) in headers$"
v-for="(header, index) in workingHeaders"
:key="`header-${index}`"
class="divide-dividerLight divide-x border-b border-dividerLight flex"
class="flex border-b divide-x divide-dividerLight border-dividerLight"
>
<SmartAutoComplete
:placeholder="`${t('count.header', { count: index + 1 })}`"
@@ -106,9 +106,7 @@
updateHeader(index, {
key: header.key,
value: header.value,
active: header.hasOwnProperty('active')
? !header.active
: false,
active: !header.active,
})
"
/>
@@ -124,16 +122,16 @@
</span>
</div>
<div
v-if="headers$.length === 0"
v-if="workingHeaders.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
>
<img
:src="`/images/states/${$colorMode.value}/add_category.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.headers')}`"
/>
<span class="text-center pb-4">
<span class="pb-4 text-center">
{{ t("empty.headers") }}
</span>
<ButtonSecondary
@@ -145,36 +143,28 @@
/>
</div>
</div>
</AppSection>
</div>
</template>
<script setup lang="ts">
import { onBeforeUpdate, ref, watch } from "@nuxtjs/composition-api"
import { Ref, ref, watch } from "@nuxtjs/composition-api"
import isEqual from "lodash/isEqual"
import clone from "lodash/clone"
import { HoppRESTHeader } from "@hoppscotch/data"
import { useCodemirror } from "~/helpers/editor/codemirror"
import {
addRESTHeader,
deleteAllRESTHeaders,
deleteRESTHeader,
restHeaders$,
setRESTHeaders,
updateRESTHeader,
} from "~/newstore/RESTSession"
import { restHeaders$, setRESTHeaders } from "~/newstore/RESTSession"
import { commonHeaders } from "~/helpers/headers"
import {
useReadonlyStream,
useI18n,
useToast,
} from "~/helpers/utils/composables"
import { useI18n, useStream, useToast } from "~/helpers/utils/composables"
const t = useI18n()
const toast = useToast()
const bulkMode = ref(false)
const bulkHeaders = ref("")
const bulkEditor = ref<any | null>(null)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
useCodemirror(bulkEditor, bulkHeaders, {
extendedEditorConfig: {
mode: "text/x-yaml",
@@ -182,96 +172,168 @@ useCodemirror(bulkEditor, bulkHeaders, {
},
linter: null,
completer: null,
environmentHighlights: true,
})
// The functional headers list (the headers actually in the system)
const headers = useStream(restHeaders$, [], setRESTHeaders) as Ref<
HoppRESTHeader[]
>
// The UI representation of the headers list (has the empty end header)
const workingHeaders = ref<HoppRESTHeader[]>([
{
key: "",
value: "",
active: true,
},
])
// Rule: Working Headers always have one empty header or the last element is always an empty header
watch(workingHeaders, (headersList) => {
if (
headersList.length > 0 &&
headersList[headersList.length - 1].key !== ""
) {
workingHeaders.value.push({
key: "",
value: "",
active: true,
})
}
})
// Sync logic between headers and working headers
watch(
headers,
(newHeadersList) => {
// Sync should overwrite working headers
const filteredWorkingHeaders = workingHeaders.value.filter(
(e) => e.key !== ""
)
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
workingHeaders.value = newHeadersList
}
},
{ immediate: true }
)
watch(workingHeaders, (newWorkingHeaders) => {
const fixedHeaders = newWorkingHeaders.filter((e) => e.key !== "")
if (!isEqual(headers.value, fixedHeaders)) {
headers.value = fixedHeaders
}
})
// Bulk Editor Syncing with Working Headers
watch(bulkHeaders, () => {
try {
const transformation = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
value: item.substring(item.indexOf(":") + 1).trim(),
active: !item.trim().startsWith("//"),
}))
setRESTHeaders(transformation as HoppRESTHeader[])
const transformation = bulkHeaders.value
.split("\n")
.filter((x) => x.trim().length > 0 && x.includes(":"))
.map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredHeaders = workingHeaders.value.filter((x) => x.key !== "")
if (!isEqual(filteredHeaders, transformation)) {
workingHeaders.value = transformation
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const headers$ = useReadonlyStream(restHeaders$, [])
watch(workingHeaders, (newHeadersList) => {
// If we are in bulk mode, don't apply direct changes
if (bulkMode.value) return
watch(
headers$,
(newValue) => {
if (!bulkMode.value)
if (
(newValue[newValue.length - 1]?.key !== "" ||
newValue[newValue.length - 1]?.value !== "") &&
newValue.length
)
addHeader()
},
{ deep: true }
)
try {
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
onBeforeUpdate(() => editBulkHeadersLine(-1, null))
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
const editBulkHeadersLine = (index: number, item?: HoppRESTHeader | null) => {
const headers = headers$.value
bulkHeaders.value = headers
.reduce((all, header, pIndex) => {
const current =
index === pIndex && item != null
? `${item.active ? "" : "//"}${item.key}: ${item.value}`
: `${header.active ? "" : "//"}${header.key}: ${header.value}`
return [...all, current]
}, [])
.join("\n")
}
const clearBulkEditor = () => {
bulkHeaders.value = ""
}
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
bulkHeaders.value = filteredHeaders
.map((header) => {
return `${header.active ? "" : "#"}${header.key}: ${header.value}`
})
.join("\n")
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const addHeader = () => {
const empty = { key: "", value: "", active: true }
const index = headers$.value.length
addRESTHeader(empty)
editBulkHeadersLine(index, empty)
workingHeaders.value.push({
key: "",
value: "",
active: true,
})
}
const updateHeader = (index: number, item: HoppRESTHeader) => {
updateRESTHeader(index, item)
editBulkHeadersLine(index, item)
const updateHeader = (index: number, header: HoppRESTHeader) => {
workingHeaders.value = workingHeaders.value.map((h, i) =>
i === index ? header : h
)
}
const deleteHeader = (index: number) => {
const headersBeforeDeletion = headers$.value
const headersBeforeDeletion = clone(workingHeaders.value)
deleteRESTHeader(index)
editBulkHeadersLine(index, null)
if (
!(
headersBeforeDeletion.length > 0 &&
index === headersBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
const deletedItem = headersBeforeDeletion[index]
if (deletedItem.key || deletedItem.value) {
toast.success(`${t("state.deleted")}`, {
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
setRESTHeaders(headersBeforeDeletion as HoppRESTHeader[])
editBulkHeadersLine(index, deletedItem)
workingHeaders.value = headersBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingHeaders.value.splice(index, 1)
}
const clearContent = () => {
deleteAllRESTHeaders()
clearBulkEditor()
// set headers list to the initial state
workingHeaders.value = [
{
key: "",
value: "",
active: true,
},
]
bulkHeaders.value = ""
}
</script>

View File

@@ -1,13 +1,17 @@
<template>
<SmartModal v-if="show" :title="`${t('import.curl')}`" @close="hideModal">
<template #body>
<div class="flex flex-col px-2">
<div ref="curlEditor" class="border border-dividerLight rounded"></div>
<div class="px-2 h-46">
<div
ref="curlEditor"
class="h-full border rounded border-dividerLight"
></div>
</div>
</template>
<template #footer>
<span class="flex">
<ButtonPrimary
ref="importButton"
:label="`${t('import.title')}`"
@click.native="handleImport"
/>
@@ -21,7 +25,7 @@
</template>
<script setup lang="ts">
import { ref } from "@nuxtjs/composition-api"
import { ref, watch } from "@nuxtjs/composition-api"
import {
HoppRESTHeader,
HoppRESTParam,
@@ -40,6 +44,8 @@ const curl = ref("")
const curlEditor = ref<any | null>(null)
const props = defineProps<{ show: boolean; text: string }>()
useCodemirror(curlEditor, curl, {
extendedEditorConfig: {
mode: "application/x-sh",
@@ -47,9 +53,18 @@ useCodemirror(curlEditor, curl, {
},
linter: null,
completer: null,
environmentHighlights: false,
})
defineProps<{ show: boolean }>()
watch(
() => props.show,
() => {
if (props.show) {
curl.value = props.text.toString()
}
},
{ immediate: false }
)
const emit = defineEmits<{
(e: "hide-modal"): void
@@ -69,6 +84,7 @@ const handleImport = () => {
const endpoint = origin + pathname
const headers: HoppRESTHeader[] = []
const params: HoppRESTParam[] = []
const body = parsedCurl.body
if (parsedCurl.query) {
for (const key of Object.keys(parsedCurl.query)) {
const val = parsedCurl.query[key]!
@@ -99,6 +115,7 @@ const handleImport = () => {
})
}
}
const method = parsedCurl.method.toUpperCase()
setRESTRequest(
@@ -116,7 +133,7 @@ const handleImport = () => {
},
body: {
contentType: "application/json",
body: "",
body,
},
})
)

View File

@@ -1,46 +1,46 @@
<template>
<div class="flex flex-col">
<div class="border-b border-dividerLight flex">
<div class="flex border-b border-dividerLight">
<input
id="oidcDiscoveryURL"
v-model="oidcDiscoveryURL"
class="bg-transparent flex flex-1 py-2 px-4"
class="flex flex-1 px-4 py-2 bg-transparent"
placeholder="OpenID Connect Discovery URL"
name="oidcDiscoveryURL"
/>
</div>
<div class="border-b border-dividerLight flex">
<div class="flex border-b border-dividerLight">
<input
id="authURL"
v-model="authURL"
class="bg-transparent flex flex-1 py-2 px-4"
class="flex flex-1 px-4 py-2 bg-transparent"
placeholder="Authentication URL"
name="authURL"
/>
</div>
<div class="border-b border-dividerLight flex">
<div class="flex border-b border-dividerLight">
<input
id="accessTokenURL"
v-model="accessTokenURL"
class="bg-transparent flex flex-1 py-2 px-4"
class="flex flex-1 px-4 py-2 bg-transparent"
placeholder="Access Token URL"
name="accessTokenURL"
/>
</div>
<div class="border-b border-dividerLight flex">
<div class="flex border-b border-dividerLight">
<input
id="clientID"
v-model="clientID"
class="bg-transparent flex flex-1 py-2 px-4"
class="flex flex-1 px-4 py-2 bg-transparent"
placeholder="Client ID"
name="clientID"
/>
</div>
<div class="border-b border-dividerLight flex">
<div class="flex border-b border-dividerLight">
<input
id="scope"
v-model="scope"
class="bg-transparent flex flex-1 py-2 px-4"
class="flex flex-1 px-4 py-2 bg-transparent"
placeholder="Scope"
name="scope"
/>

View File

@@ -1,7 +1,7 @@
<template>
<AppSection label="parameters">
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.parameter_list") }}
@@ -39,9 +39,9 @@
<div v-if="bulkMode" ref="bulkEditor"></div>
<div v-else>
<div
v-for="(param, index) in params$"
v-for="(param, index) in workingParams"
:key="`param-${index}`"
class="divide-dividerLight divide-x border-b border-dividerLight flex"
class="flex border-b divide-x divide-dividerLight border-dividerLight"
>
<SmartEnvInput
v-model="param.key"
@@ -117,16 +117,16 @@
</span>
</div>
<div
v-if="params$.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
v-if="workingParams.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_files.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.parameters')}`"
/>
<span class="text-center pb-4">
<span class="pb-4 text-center">
{{ t("empty.parameters") }}
</span>
<ButtonSecondary
@@ -138,26 +138,17 @@
/>
</div>
</div>
</AppSection>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onBeforeUpdate } from "@nuxtjs/composition-api"
import { ref, watch } from "@nuxtjs/composition-api"
import { HoppRESTParam } from "@hoppscotch/data"
import isEqual from "lodash/isEqual"
import clone from "lodash/clone"
import { useCodemirror } from "~/helpers/editor/codemirror"
import {
useReadonlyStream,
useI18n,
useToast,
} from "~/helpers/utils/composables"
import {
restParams$,
addRESTParam,
updateRESTParam,
deleteRESTParam,
deleteAllRESTParams,
setRESTParams,
} from "~/newstore/RESTSession"
import { useI18n, useToast, useStream } from "~/helpers/utils/composables"
import { restParams$, setRESTParams } from "~/newstore/RESTSession"
const t = useI18n()
@@ -166,19 +157,7 @@ const toast = useToast()
const bulkMode = ref(false)
const bulkParams = ref("")
watch(bulkParams, () => {
try {
const transformation = bulkParams.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
value: item.substring(item.indexOf(":") + 1).trim(),
active: !item.trim().startsWith("//"),
}))
setRESTParams(transformation as HoppRESTParam[])
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const bulkEditor = ref<any | null>(null)
@@ -189,82 +168,163 @@ useCodemirror(bulkEditor, bulkParams, {
},
linter: null,
completer: null,
environmentHighlights: true,
})
const params$ = useReadonlyStream(restParams$, [])
// The functional parameters list (the parameters actually applied to the session)
const params = useStream(restParams$, [], setRESTParams)
watch(
params$,
(newValue) => {
if (!bulkMode.value)
if (
(newValue[newValue.length - 1]?.key !== "" ||
newValue[newValue.length - 1]?.value !== "") &&
newValue.length
)
addParam()
// The UI representation of the parameters list (has the empty end param)
const workingParams = ref<HoppRESTParam[]>([
{
key: "",
value: "",
active: true,
},
{ deep: true }
])
// Rule: Working Params always have last element is always an empty param
watch(workingParams, (paramsList) => {
if (paramsList.length > 0 && paramsList[paramsList.length - 1].key !== "") {
workingParams.value.push({
key: "",
value: "",
active: true,
})
}
})
// Sync logic between params and working params
watch(
params,
(newParamsList) => {
// Sync should overwrite working params
const filteredWorkingParams = workingParams.value.filter(
(e) => e.key !== ""
)
if (!isEqual(newParamsList, filteredWorkingParams)) {
workingParams.value = newParamsList
}
},
{ immediate: true }
)
onBeforeUpdate(() => editBulkParamsLine(-1, null))
watch(workingParams, (newWorkingParams) => {
const fixedParams = newWorkingParams.filter((e) => e.key !== "")
if (!isEqual(params.value, fixedParams)) {
params.value = fixedParams
}
})
const editBulkParamsLine = (index: number, item?: HoppRESTParam | null) => {
const params = params$.value
// Bulk Editor Syncing with Working Params
watch(bulkParams, () => {
try {
const transformation = bulkParams.value
.split("\n")
.filter((x) => x.trim().length > 0 && x.includes(":"))
.map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
bulkParams.value = params
.reduce((all, param, pIndex) => {
const current =
index === pIndex && item != null
? `${item.active ? "" : "//"}${item.key}: ${item.value}`
: `${param.active ? "" : "//"}${param.key}: ${param.value}`
return [...all, current]
}, [])
.join("\n")
}
const filteredParams = workingParams.value.filter((x) => x.key !== "")
const clearBulkEditor = () => {
bulkParams.value = ""
}
if (!isEqual(filteredParams, transformation)) {
workingParams.value = transformation
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
watch(workingParams, (newParamsList) => {
// If we are in bulk mode, don't apply direct changes
if (bulkMode.value) return
try {
const currentBulkParams = bulkParams.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredParams = newParamsList.filter((x) => x.key !== "")
if (!isEqual(currentBulkParams, filteredParams)) {
bulkParams.value = filteredParams
.map((param) => {
return `${param.active ? "" : "#"}${param.key}: ${param.value}`
})
.join("\n")
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const addParam = () => {
const empty = { key: "", value: "", active: true }
const index = params$.value.length
addRESTParam(empty)
editBulkParamsLine(index, empty)
workingParams.value.push({
key: "",
value: "",
active: true,
})
}
const updateParam = (index: number, item: HoppRESTParam) => {
updateRESTParam(index, item)
editBulkParamsLine(index, item)
const updateParam = (index: number, param: HoppRESTParam) => {
workingParams.value = workingParams.value.map((h, i) =>
i === index ? param : h
)
}
const deleteParam = (index: number) => {
const parametersBeforeDeletion = params$.value
const paramsBeforeDeletion = clone(workingParams.value)
deleteRESTParam(index)
editBulkParamsLine(index, null)
if (
!(
paramsBeforeDeletion.length > 0 &&
index === paramsBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
const deletedItem = parametersBeforeDeletion[index]
if (deletedItem.key || deletedItem.value) {
toast.success(`${t("state.deleted")}`, {
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
setRESTParams(parametersBeforeDeletion as HoppRESTParam[])
editBulkParamsLine(index, deletedItem)
workingParams.value = paramsBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingParams.value.splice(index, 1)
}
const clearContent = () => {
deleteAllRESTParams()
clearBulkEditor()
// set params list to the initial state
workingParams.value = [
{
key: "",
value: "",
active: true,
},
]
bulkParams.value = ""
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<AppSection id="script" :label="`${t('preRequest.script')}`">
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("preRequest.javascript_code") }}
@@ -29,14 +29,14 @@
/>
</div>
</div>
<div class="border-b border-dividerLight flex">
<div class="border-r border-dividerLight w-2/3">
<div ref="preRrequestEditor"></div>
<div class="flex border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">
<div ref="preRrequestEditor" class="h-full"></div>
</div>
<div
class="bg-primary h-full top-upperTertiaryStickyFold min-w-46 max-w-1/3 p-4 z-9 sticky overflow-auto"
class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
>
<div class="text-secondaryLight pb-2">
<div class="pb-2 text-secondaryLight">
{{ t("helpers.pre_request_script") }}
</div>
<SmartAnchor
@@ -44,7 +44,7 @@
to="https://docs.hoppscotch.io/features/pre-request-script"
blank
/>
<h4 class="font-bold text-secondaryLight pt-6">
<h4 class="pt-6 font-bold text-secondaryLight">
{{ t("preRequest.snippets") }}
</h4>
<div class="flex flex-col pt-4">
@@ -58,7 +58,7 @@
</div>
</div>
</div>
</AppSection>
</div>
</template>
<script setup lang="ts">
@@ -88,6 +88,7 @@ useCodemirror(
},
linter,
completer,
environmentHighlights: false,
})
)

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperTertiaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperTertiaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.raw_body") }}
@@ -38,7 +38,7 @@
<label for="payload">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('import.json')"
:title="t('import.title')"
svg="file-plus"
@click.native="$refs.payload.click()"
/>
@@ -57,26 +57,57 @@
</template>
<script setup lang="ts">
import { computed, reactive, ref } from "@nuxtjs/composition-api"
import {
computed,
reactive,
Ref,
ref,
watchEffect,
} from "@nuxtjs/composition-api"
import * as TO from "fp-ts/TaskOption"
import { pipe } from "fp-ts/function"
import { HoppRESTReqBody, ValidContentTypes } from "~/../hoppscotch-data/dist"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { getEditorLangForMimeType } from "~/helpers/editorutils"
import { pluckRef, useI18n, useToast } from "~/helpers/utils/composables"
import { isJSONContentType } from "~/helpers/utils/contenttypes"
import { useRESTRequestBody } from "~/newstore/RESTSession"
import jsonLinter from "~/helpers/editor/linting/json"
import { readFileAsText } from "~/helpers/functional/files"
type PossibleContentTypes = Exclude<
ValidContentTypes,
"multipart/form-data" | "application/x-www-form-urlencoded"
>
const t = useI18n()
const props = defineProps<{
contentType: string
contentType: PossibleContentTypes
}>()
const toast = useToast()
const rawParamsBody = pluckRef(useRESTRequestBody(), "body")
const rawParamsBody = pluckRef(
useRESTRequestBody() as Ref<
HoppRESTReqBody & {
contentType: PossibleContentTypes
}
>,
"body"
)
const prettifyIcon = ref("wand")
const rawInputEditorLang = computed(() =>
getEditorLangForMimeType(props.contentType)
)
const langLinter = computed(() =>
isJSONContentType(props.contentType) ? jsonLinter : null
)
watchEffect(() => console.log(rawInputEditorLang.value))
const linewrapEnabled = ref(true)
const rawBodyParameters = ref<any | null>(null)
@@ -87,10 +118,11 @@ useCodemirror(
extendedEditorConfig: {
lineWrapping: linewrapEnabled,
mode: rawInputEditorLang,
placeholder: t("request.raw_body"),
placeholder: t("request.raw_body").toString(),
},
linter: null,
linter: langLinter,
completer: null,
environmentHighlights: true,
})
)
@@ -98,28 +130,32 @@ const clearContent = () => {
rawParamsBody.value = ""
}
const uploadPayload = (e: InputEvent) => {
const file = e.target.files[0]
if (file !== undefined && file !== null) {
const reader = new FileReader()
reader.onload = ({ target }) => {
rawParamsBody.value = target?.result
}
reader.readAsText(file)
toast.success(`${t("state.file_imported")}`)
} else {
toast.error(`${t("action.choose_file")}`)
}
const uploadPayload = async (e: InputEvent) => {
await pipe(
(e.target as HTMLInputElement).files?.[0],
TO.of,
TO.chain(TO.fromPredicate((f): f is File => f !== undefined)),
TO.chain(readFileAsText),
TO.matchW(
() => toast.error(`${t("action.choose_file")}`),
(result) => {
rawParamsBody.value = result
toast.success(`${t("state.file_imported")}`)
}
)
)()
}
const prettifyRequestBody = () => {
try {
const jsonObj = JSON.parse(rawParamsBody.value)
rawParamsBody.value = JSON.stringify(jsonObj, null, 2)
prettifyIcon.value = "check"
setTimeout(() => (prettifyIcon.value = "wand"), 1000)
} catch (e) {
console.error(e)
prettifyIcon.value = "info"
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
setTimeout(() => (prettifyIcon.value = "wand"), 1000)
}
</script>

View File

@@ -1,9 +1,9 @@
<template>
<div
class="bg-primary flex space-x-2 p-4 top-0 z-10 sticky overflow-x-auto hide-scrollbar"
class="sticky top-0 z-10 flex p-4 space-x-2 overflow-x-auto bg-primary hide-scrollbar"
>
<div class="flex flex-1">
<div class="flex relative">
<div class="relative flex">
<label for="method">
<tippy
ref="methodOptions"
@@ -16,7 +16,7 @@
<span class="select-wrapper">
<input
id="method"
class="bg-primaryLight border border-divider rounded-l cursor-pointer flex font-semibold text-secondaryDark py-2 px-4 w-26 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
class="flex px-4 py-2 font-semibold border rounded-l cursor-pointer bg-primaryLight border-divider text-secondaryDark w-26 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
:value="newMethod"
:readonly="!isCustomMethod"
:placeholder="`${t('request.method')}`"
@@ -52,13 +52,14 @@
focus-visible:bg-transparent
"
@enter="newSendRequest()"
@paste="onPasteUrl($event)"
/>
</div>
</div>
<div class="flex">
<ButtonPrimary
id="send"
class="rounded-r-none flex-1 min-w-20"
class="flex-1 rounded-r-none min-w-20"
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
@click.native="!loading ? newSendRequest() : cancelRequest()"
/>
@@ -69,45 +70,61 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => sendTippyActions.focus()"
>
<template #trigger>
<ButtonPrimary class="rounded-l-none" filled svg="chevron-down" />
</template>
<SmartItem
:label="`${t('import.curl')}`"
svg="file-code"
@click.native="
() => {
showCurlImportModal = !showCurlImportModal
sendOptions.tippy().hide()
}
"
/>
<SmartItem
:label="`${t('show.code')}`"
svg="code-2"
@click.native="
() => {
showCodegenModal = !showCodegenModal
sendOptions.tippy().hide()
}
"
/>
<SmartItem
ref="clearAll"
:label="`${t('action.clear_all')}`"
svg="rotate-ccw"
@click.native="
() => {
clearContent()
sendOptions.tippy().hide()
}
"
/>
<div
ref="sendTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.c="curl.$el.click()"
@keyup.s="show.$el.click()"
@keyup.delete="clearAll.$el.click()"
@keyup.escape="sendOptions.tippy().hide()"
>
<SmartItem
ref="curl"
:label="`${t('import.curl')}`"
svg="file-code"
:shortcut="['C']"
@click.native="
() => {
showCurlImportModal = !showCurlImportModal
sendOptions.tippy().hide()
}
"
/>
<SmartItem
ref="show"
:label="`${t('show.code')}`"
svg="code-2"
:shortcut="['S']"
@click.native="
() => {
showCodegenModal = !showCodegenModal
sendOptions.tippy().hide()
}
"
/>
<SmartItem
ref="clearAll"
:label="`${t('action.clear_all')}`"
svg="rotate-ccw"
:shortcut="['⌫']"
@click.native="
() => {
clearContent()
sendOptions.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
<ButtonSecondary
class="rounded rounded-r-none ml-2"
class="ml-2 rounded rounded-r-none"
:label="
windowInnerWidth.x.value >= 768 && COLUMN_LAYOUT
? `${t('request.save')}`
@@ -124,6 +141,7 @@
trigger="click"
theme="popover"
arrow
:on-shown="() => saveTippyActions.focus()"
>
<template #trigger>
<ButtonSecondary
@@ -142,32 +160,44 @@
class="mb-2 input"
@keyup.enter="saveOptions.tippy().hide()"
/>
<SmartItem
ref="copyRequest"
:label="shareButtonText"
:svg="copyLinkIcon"
:loading="fetchingShareLink"
@click.native="
() => {
copyRequest()
}
"
/>
<SmartItem
ref="saveRequest"
:label="`${t('request.save_as')}`"
svg="folder-plus"
@click.native="
() => {
showSaveRequestModal = true
saveOptions.tippy().hide()
}
"
/>
<div
ref="saveTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.c="copyRequestAction.$el.click()"
@keyup.s="saveRequestAction.$el.click()"
@keyup.escape="saveOptions.tippy().hide()"
>
<SmartItem
ref="copyRequestAction"
:label="shareButtonText"
:svg="copyLinkIcon"
:loading="fetchingShareLink"
:shortcut="['C']"
@click.native="
() => {
copyRequest()
}
"
/>
<SmartItem
ref="saveRequestAction"
:label="`${t('request.save_as')}`"
svg="folder-plus"
:shortcut="['S']"
@click.native="
() => {
showSaveRequestModal = true
saveOptions.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
</div>
<HttpImportCurl
:text="curlText"
:show="showCurlImportModal"
@hide-modal="showCurlImportModal = false"
/>
@@ -239,6 +269,7 @@ const nuxt = useNuxt()
const { subscribeToStream } = useStreamSubscriber()
const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint)
const curlText = ref("")
const newMethod = useStream(restMethod$, "", updateRESTMethod)
const loading = ref(false)
@@ -253,6 +284,13 @@ const hasNavigatorShare = !!navigator.share
const methodOptions = ref<any | null>(null)
const saveOptions = ref<any | null>(null)
const sendOptions = ref<any | null>(null)
const sendTippyActions = ref<any | null>(null)
const saveTippyActions = ref<any | null>(null)
const curl = ref<any | null>(null)
const show = ref<any | null>(null)
const clearAll = ref<any | null>(null)
const copyRequestAction = ref<any | null>(null)
const saveRequestAction = ref<any | null>(null)
// Update Nuxt Loading bar
watch(loading, () => {
@@ -307,6 +345,27 @@ const newSendRequest = async () => {
}
}
const onPasteUrl = (e: { event: ClipboardEvent; previousValue: string }) => {
if (!e) return
const clipboardData = e.event.clipboardData
const pastedData = clipboardData?.getData("Text")
if (!pastedData) return
if (isCURL(pastedData)) {
e.event.preventDefault()
showCurlImportModal.value = true
curlText.value = pastedData
newEndpoint.value = e.previousValue
}
}
function isCURL(curl: string) {
return curl.includes("curl ")
}
const cancelRequest = () => {
loading.value = false
updateRESTResponse(null)

View File

@@ -1,11 +1,11 @@
<template>
<AppSection label="response">
<div class="flex flex-col flex-1">
<HttpResponseMeta :response="response" />
<LensesResponseBodyRenderer
v-if="!loading && hasResponse"
:response="response"
/>
</AppSection>
</div>
</template>
<script lang="ts">

View File

@@ -1,23 +1,23 @@
<template>
<div
class="bg-primary flex p-4 top-0 z-10 sticky items-center overflow-auto hide-scrollbar whitespace-nowrap"
class="sticky top-0 z-10 flex items-center p-4 overflow-auto bg-primary hide-scrollbar whitespace-nowrap"
>
<div
v-if="response == null"
class="flex flex-col flex-1 text-secondaryLight items-center justify-center"
class="flex flex-col items-center justify-center flex-1 text-secondaryLight"
>
<div class="flex space-x-2 my-4 pb-4">
<div class="flex flex-col space-y-4 text-right items-end">
<span class="flex flex-1 items-center">
<div class="flex pb-4 my-4 space-x-2">
<div class="flex flex-col items-end space-y-4 text-right">
<span class="flex items-center flex-1">
{{ t("shortcut.request.send_request") }}
</span>
<span class="flex flex-1 items-center">
<span class="flex items-center flex-1">
{{ t("shortcut.general.show_all") }}
</span>
<span class="flex flex-1 items-center">
<span class="flex items-center flex-1">
{{ t("shortcut.general.command_menu") }}
</span>
<span class="flex flex-1 items-center">
<span class="flex items-center flex-1">
{{ t("shortcut.general.help_menu") }}
</span>
</div>
@@ -57,66 +57,71 @@
</div>
<div
v-if="response.type === 'network_fail'"
class="flex flex-col flex-1 p-4 items-center justify-center"
class="flex flex-col items-center justify-center flex-1 p-4"
>
<img
:src="`/images/states/${$colorMode.value}/youre_lost.svg`"
loading="lazy"
class="flex-col object-contain object-center h-32 my-4 w-32 inline-flex"
class="inline-flex flex-col object-contain object-center w-32 h-32 my-4"
:alt="`${t('error.network_fail')}`"
/>
<span class="font-semibold text-center mb-2">
<span class="mb-2 font-semibold text-center">
{{ t("error.network_fail") }}
</span>
<span
class="max-w-sm text-secondaryLight text-center mb-4 whitespace-normal"
class="max-w-sm mb-6 text-center whitespace-normal text-secondaryLight"
>
{{ t("helpers.network_fail") }}
</span>
<AppInterceptor />
<AppInterceptor class="border rounded border-dividerLight" />
</div>
<div
v-if="response.type === 'script_fail'"
class="flex flex-col flex-1 p-4 items-center justify-center"
class="flex flex-col items-center justify-center flex-1 p-4"
>
<img
:src="`/images/states/${$colorMode.value}/youre_lost.svg`"
loading="lazy"
class="flex-col object-contain object-center h-32 my-4 w-32 inline-flex"
class="inline-flex flex-col object-contain object-center w-32 h-32 my-4"
:alt="`${t('error.script_fail')}`"
/>
<span class="font-semibold text-center mb-2">
<span class="mb-2 font-semibold text-center">
{{ t("error.script_fail") }}
</span>
<span
class="max-w-sm text-secondaryLight text-center mb-4 whitespace-normal"
class="max-w-sm mb-6 text-center whitespace-normal text-secondaryLight"
>
{{ t("helpers.script_fail") }}
</span>
<div
class="bg-primaryLight rounded font-mono w-full py-2 px-4 text-red-400 overflow-auto whitespace-normal"
class="w-full px-4 py-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
>
{{ response.error.name }}: {{ response.error.message }}<br />
{{ response.error.stack }}
</div>
</div>
<div
v-if="response.type === 'success' || 'fail'"
:class="statusCategory.className"
class="font-semibold space-x-4"
v-if="response.type === 'success' || response.type === 'fail'"
class="flex items-center font-semibold text-tiny"
>
<span v-if="response.statusCode">
<span class="text-secondary"> {{ t("response.status") }}: </span>
{{ response.statusCode || t("state.waiting_send_request") }}
</span>
<span v-if="response.meta && response.meta.responseDuration">
<span class="text-secondary"> {{ t("response.time") }}: </span>
{{ `${response.meta.responseDuration} ms` }}
</span>
<span v-if="response.meta && response.meta.responseSize">
<span class="text-secondary"> {{ t("response.size") }}: </span>
{{ `${response.meta.responseSize} B` }}
</span>
<div
:class="statusCategory.className"
class="inline-flex flex-1 space-x-4"
>
<span v-if="response.statusCode">
<span class="text-secondary"> {{ t("response.status") }}: </span>
{{ `${response.statusCode}\xA0 • \xA0`
}}{{ getStatusCodeReasonPhrase(response.statusCode) }}
</span>
<span v-if="response.meta && response.meta.responseDuration">
<span class="text-secondary"> {{ t("response.time") }}: </span>
{{ `${response.meta.responseDuration} ms` }}
</span>
<span v-if="response.meta && response.meta.responseSize">
<span class="text-secondary"> {{ t("response.size") }}: </span>
{{ `${response.meta.responseSize} B` }}
</span>
</div>
</div>
</div>
</div>
@@ -128,6 +133,7 @@ import findStatusGroup from "~/helpers/findStatusGroup"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { useI18n } from "~/helpers/utils/composables"
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
const t = useI18n()
@@ -139,20 +145,13 @@ const statusCategory = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail" ||
props.response.type === "script_fail"
props.response.type === "script_fail" ||
props.response.type === "fail"
)
return ""
return {
name: "error",
className: "text-red-500",
}
return findStatusGroup(props.response.statusCode)
})
</script>
<style lang="scss" scoped>
.shortcut-key {
@apply bg-dividerLight;
@apply rounded;
@apply ml-2;
@apply py-1;
@apply px-2;
@apply inline-flex;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<AppSection :label="`${t('test.results')}`">
<div>
<div
v-if="
testResults &&
@@ -7,7 +7,7 @@
"
>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-lowerSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("test.report") }}
@@ -19,8 +19,8 @@
@click.native="clearContent()"
/>
</div>
<div class="divide-dividerLight border-b border-dividerLight divide-y-4">
<div v-if="testResults.tests" class="divide-dividerLight divide-y-4">
<div class="border-b divide-y-4 divide-dividerLight border-dividerLight">
<div v-if="testResults.tests" class="divide-y-4 divide-dividerLight">
<HttpTestResultEntry
v-for="(result, index) in testResults.tests"
:key="`result-${index}`"
@@ -29,7 +29,7 @@
</div>
<div
v-if="testResults.expectResults"
class="divide-dividerLight divide-y"
class="divide-y divide-dividerLight"
>
<HttpTestResultReport
v-if="testResults.expectResults.length"
@@ -38,7 +38,7 @@
<div
v-for="(result, index) in testResults.expectResults"
:key="`result-${index}`"
class="flex py-2 px-4 items-center"
class="flex items-center px-4 py-2"
>
<i
class="mr-4 material-icons"
@@ -64,18 +64,18 @@
</div>
<div
v-else
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/validation.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.tests')}`"
/>
<span class="text-center pb-2">
<span class="pb-2 text-center">
{{ t("empty.tests") }}
</span>
<span class="text-center pb-4">
<span class="pb-4 text-center">
{{ t("helpers.tests") }}
</span>
<ButtonSecondary
@@ -88,7 +88,7 @@
class="my-4"
/>
</div>
</AppSection>
</div>
</template>
<script setup lang="ts">

View File

@@ -2,11 +2,11 @@
<div>
<span
v-if="testResults.description"
class="flex font-bold text-secondaryDark py-2 px-4 items-center"
class="flex items-center px-4 py-2 font-bold text-secondaryDark"
>
{{ testResults.description }}
</span>
<div v-if="testResults.expectResults" class="divide-dividerLight divide-y">
<div v-if="testResults.expectResults" class="divide-y divide-dividerLight">
<HttpTestResultReport
v-if="testResults.expectResults.length"
:test-results="testResults"
@@ -14,7 +14,7 @@
<div
v-for="(result, index) in testResults.expectResults"
:key="`result-${index}`"
class="flex py-2 px-4 items-center"
class="flex items-center px-4 py-2"
>
<i
class="mr-4 material-icons"

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex p-2 items-center">
<div class="flex items-center p-2">
<SmartProgressRing
class="text-red-500"
:radius="16"
@@ -20,12 +20,16 @@
<script setup lang="ts">
import { computed, PropType } from "@nuxtjs/composition-api"
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
import {
HoppTestExpectResult,
HoppTestResult,
} from "~/helpers/types/HoppTestResult"
const props = defineProps({
testResults: {
type: Object as PropType<HoppTestResult>,
required: true,
expectResults: [] as HoppTestExpectResult[],
},
})

View File

@@ -1,7 +1,7 @@
<template>
<AppSection id="script" :label="`${t('test.script')}`">
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("test.javascript_code") }}
@@ -29,14 +29,14 @@
/>
</div>
</div>
<div class="border-b border-dividerLight flex">
<div class="border-r border-dividerLight w-2/3">
<div ref="testScriptEditor"></div>
<div class="flex border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">
<div ref="testScriptEditor" class="h-full"></div>
</div>
<div
class="bg-primary h-full top-upperTertiaryStickyFold min-w-46 max-w-1/3 p-4 z-9 sticky overflow-auto"
class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
>
<div class="text-secondaryLight pb-2">
<div class="pb-2 text-secondaryLight">
{{ t("helpers.post_request_tests") }}
</div>
<SmartAnchor
@@ -44,7 +44,7 @@
to="https://docs.hoppscotch.io/features/tests"
blank
/>
<h4 class="font-bold text-secondaryLight pt-6">
<h4 class="pt-6 font-bold text-secondaryLight">
{{ t("test.snippets") }}
</h4>
<div class="flex flex-col pt-4">
@@ -58,7 +58,7 @@
</div>
</div>
</div>
</AppSection>
</div>
</template>
<script setup lang="ts">
@@ -88,6 +88,7 @@ useCodemirror(
},
linter,
completer,
environmentHighlights: false,
})
)

View File

@@ -0,0 +1,355 @@
<template>
<div>
<div
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperTertiaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/body"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
svg="edit"
:class="{ '!text-accent': bulkMode }"
@click.native="bulkMode = !bulkMode"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
svg="plus"
:disabled="bulkMode"
@click.native="addUrlEncodedParam"
/>
</div>
</div>
<div v-if="bulkMode" ref="bulkEditor"></div>
<div v-else>
<div
v-for="(param, index) in workingUrlEncodedParams"
:key="`param-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight"
>
<SmartEnvInput
v-model="param.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
styles="
bg-transparent
flex
flex-1
py-1
px-4
"
@change="
updateUrlEncodedParam(index, {
key: $event,
value: param.value,
active: param.active,
})
"
/>
<SmartEnvInput
v-model="param.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
styles="
bg-transparent
flex
flex-1
py-1
px-4
"
@change="
updateUrlEncodedParam(index, {
key: param.key,
value: $event,
active: param.active,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
param.hasOwnProperty('active')
? param.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:svg="
param.hasOwnProperty('active')
? param.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateUrlEncodedParam(index, {
key: param.key,
value: param.value,
active: !param.active,
})
"
/>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
svg="trash"
color="red"
@click.native="deleteUrlEncodedParam(index)"
/>
</span>
</div>
<div
v-if="workingUrlEncodedParams.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
>
<img
:src="`/images/states/${$colorMode.value}/add_category.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.body')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.body") }}
</span>
<ButtonSecondary
filled
:label="`${t('add.new')}`"
svg="plus"
class="mb-4"
@click.native="addUrlEncodedParam"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, Ref, ref, watch } from "@nuxtjs/composition-api"
import isEqual from "lodash/isEqual"
import clone from "lodash/clone"
import { HoppRESTReqBody } from "@hoppscotch/data"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { useRESTRequestBody } from "~/newstore/RESTSession"
import { pluckRef, useI18n, useToast } from "~/helpers/utils/composables"
import {
parseRawKeyValueEntries,
rawKeyValueEntriesToString,
RawKeyValueEntry,
} from "~/helpers/rawKeyValue"
const t = useI18n()
const toast = useToast()
const bulkMode = ref(false)
const bulkUrlEncodedParams = ref("")
const bulkEditor = ref<any | null>(null)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
useCodemirror(bulkEditor, bulkUrlEncodedParams, {
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
},
linter: null,
completer: null,
environmentHighlights: true,
})
// The functional urlEncodedParams list (the urlEncodedParams actually in the system)
const urlEncodedParamsRaw = pluckRef(
useRESTRequestBody() as Ref<
HoppRESTReqBody & { contentType: "application/x-www-form-urlencoded" }
>,
"body"
)
const urlEncodedParams = computed<RawKeyValueEntry[]>({
get() {
return parseRawKeyValueEntries(urlEncodedParamsRaw.value)
},
set(newValue) {
urlEncodedParamsRaw.value = rawKeyValueEntriesToString(newValue)
},
})
// The UI representation of the urlEncodedParams list (has the empty end urlEncodedParam)
const workingUrlEncodedParams = ref<RawKeyValueEntry[]>([
{
key: "",
value: "",
active: true,
},
])
// Rule: Working urlEncodedParams always have one empty urlEncodedParam or the last element is always an empty urlEncodedParams
watch(workingUrlEncodedParams, (urlEncodedParamList) => {
if (
urlEncodedParamList.length > 0 &&
urlEncodedParamList[urlEncodedParamList.length - 1].key !== ""
) {
workingUrlEncodedParams.value.push({
key: "",
value: "",
active: true,
})
}
})
// Sync logic between urlEncodedParams and working urlEncodedParams
watch(
urlEncodedParams,
(newurlEncodedParamList) => {
const filteredWorkingUrlEncodedParams =
workingUrlEncodedParams.value.filter((e) => e.key !== "")
if (!isEqual(newurlEncodedParamList, filteredWorkingUrlEncodedParams)) {
workingUrlEncodedParams.value = newurlEncodedParamList
}
},
{ immediate: true }
)
watch(workingUrlEncodedParams, (newWorkingUrlEncodedParams) => {
const fixedUrlEncodedParams = newWorkingUrlEncodedParams.filter(
(e) => e.key !== ""
)
if (!isEqual(urlEncodedParams.value, fixedUrlEncodedParams)) {
urlEncodedParams.value = fixedUrlEncodedParams
}
})
// Bulk Editor Syncing with Working urlEncodedParams
watch(bulkUrlEncodedParams, () => {
try {
const transformation = bulkUrlEncodedParams.value
.split("\n")
.filter((x) => x.trim().length > 0 && x.includes(":"))
.map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredUrlEncodedParams = workingUrlEncodedParams.value.filter(
(x) => x.key !== ""
)
if (!isEqual(filteredUrlEncodedParams, transformation)) {
workingUrlEncodedParams.value = transformation
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
watch(workingUrlEncodedParams, (newurlEncodedParamList) => {
if (bulkMode.value) return
try {
const currentBulkUrlEncodedParams = bulkUrlEncodedParams.value
.split("\n")
.map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredUrlEncodedParams = newurlEncodedParamList.filter(
(x) => x.key !== ""
)
if (!isEqual(currentBulkUrlEncodedParams, filteredUrlEncodedParams)) {
bulkUrlEncodedParams.value = filteredUrlEncodedParams
.map((param) => {
return `${param.active ? "" : "#"}${param.key}: ${param.value}`
})
.join("\n")
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const addUrlEncodedParam = () => {
workingUrlEncodedParams.value.push({
key: "",
value: "",
active: true,
})
}
const updateUrlEncodedParam = (index: number, param: RawKeyValueEntry) => {
workingUrlEncodedParams.value = workingUrlEncodedParams.value.map((p, i) =>
i === index ? param : p
)
}
const deleteUrlEncodedParam = (index: number) => {
const urlEncodedParamsBeforeDeletion = clone(workingUrlEncodedParams.value)
if (
!(
urlEncodedParamsBeforeDeletion.length > 0 &&
index === urlEncodedParamsBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingUrlEncodedParams.value = urlEncodedParamsBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingUrlEncodedParams.value.splice(index, 1)
}
const clearContent = () => {
// set urlEncodedParams list to the initial state
workingUrlEncodedParams.value = [
{
key: "",
value: "",
active: true,
},
]
bulkUrlEncodedParams.value = ""
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-lowerSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.header_list") }}
@@ -20,19 +20,19 @@
<div
v-for="(header, index) in headers"
:key="`header-${index}`"
class="divide-dividerLight divide-x border-b border-dividerLight flex group"
class="flex border-b divide-x divide-dividerLight border-dividerLight group"
>
<span
class="flex flex-1 min-w-0 py-2 px-4 transition group-hover:text-secondaryDark"
class="flex flex-1 min-w-0 px-4 py-2 transition group-hover:text-secondaryDark"
>
<span class="rounded-sm truncate select-all">
<span class="truncate rounded-sm select-all">
{{ header.key }}
</span>
</span>
<span
class="flex flex-1 min-w-0 py-2 px-4 transition group-hover:text-secondaryDark"
class="flex flex-1 min-w-0 px-4 py-2 transition group-hover:text-secondaryDark"
>
<span class="rounded-sm truncate select-all">
<span class="truncate rounded-sm select-all">
{{ header.value }}
</span>
</span>

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col flex-1">
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-lowerSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("response.body") }}
@@ -49,16 +49,20 @@
class="covers-response"
src="about:blank"
loading="lazy"
sandbox=""
></iframe>
</div>
</template>
<script setup lang="ts">
import { computed, ref, reactive } from "@nuxtjs/composition-api"
import { ref, reactive } from "@nuxtjs/composition-api"
import usePreview from "~/helpers/lenses/composables/usePreview"
import useResponseBody from "~/helpers/lenses/composables/useResponseBody"
import useDownloadResponse from "~/helpers/lenses/composables/useDownloadResponse"
import useCopyResponse from "~/helpers/lenses/composables/useCopyResponse"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { useI18n, useToast } from "~/helpers/utils/composables"
import { useI18n } from "~/helpers/utils/composables"
const t = useI18n()
@@ -66,31 +70,20 @@ const props = defineProps<{
response: HoppRESTResponse
}>()
const toast = useToast()
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
}
})
const downloadIcon = ref("download")
const copyIcon = ref("copy")
const previewEnabled = ref(false)
const previewFrame = ref<any | null>(null)
const url = ref("")
const htmlResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
const { responseBodyText } = useResponseBody(props.response)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"text/html",
responseBodyText
)
const { previewFrame, previewEnabled, togglePreview } = usePreview(
false,
responseBodyText
)
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
useCodemirror(
htmlResponse,
responseBodyText,
@@ -102,53 +95,9 @@ useCodemirror(
},
linter: null,
completer: null,
environmentHighlights: true,
})
)
const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: "text/html" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadIcon.value = "check"
toast.success(`${t("state.download_started")}`)
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadIcon.value = "download"
}, 1000)
}
const copyResponse = () => {
copyToClipboard(responseBodyText.value)
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
const togglePreview = () => {
previewEnabled.value = !previewEnabled.value
if (previewEnabled.value) {
if (previewFrame.value.getAttribute("data-previewing-url") === url.value)
return
// Use DOMParser to parse document HTML.
const previewDocument = new DOMParser().parseFromString(
responseBodyText.value,
"text/html"
)
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
previewDocument.head.innerHTML =
`<base href="${url.value}">` + previewDocument.head.innerHTML
// Finally, set the iframe source to the resulting HTML.
previewFrame.value.srcdoc = previewDocument.documentElement.outerHTML
previewFrame.value.setAttribute("data-previewing-url", url.value)
}
}
</script>
<style lang="scss" scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-lowerSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ $t("response.body") }}
@@ -18,7 +18,7 @@
</div>
</div>
<img
class="border-b border-dividerLight flex max-w-full flex-1"
class="flex flex-1 max-w-full border-b border-dividerLight"
:src="imageSource"
loading="lazy"
:alt="imageSource"

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-lowerSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">{{
t("response.body")
@@ -36,7 +36,7 @@
<div ref="jsonResponse"></div>
<div
v-if="outlinePath"
class="bg-primaryLight border-t border-dividerLight flex flex-nowrap flex-1 px-2 bottom-0 z-10 sticky overflow-auto hide-scrollbar"
class="sticky bottom-0 z-10 flex flex-1 px-2 overflow-auto border-t bg-primaryLight border-dividerLight flex-nowrap hide-scrollbar"
>
<div
v-for="(item, index) in outlinePath"
@@ -115,7 +115,7 @@
</tippy>
<i
v-if="index + 1 !== outlinePath.length"
class="text-secondaryLight opacity-50 material-icons"
class="opacity-50 text-secondaryLight material-icons"
>chevron_right</i
>
</div>
@@ -126,7 +126,6 @@
<script setup lang="ts">
import { computed, ref, reactive } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
import { getJSONOutlineAtPos } from "~/helpers/newOutline"
@@ -134,7 +133,10 @@ import {
convertIndexToLineCh,
convertLineChToIndex,
} from "~/helpers/editor/utils"
import { useI18n, useToast } from "~/helpers/utils/composables"
import { useI18n } from "~/helpers/utils/composables"
import useCopyResponse from "~/helpers/lenses/composables/useCopyResponse"
import useResponseBody from "~/helpers/lenses/composables/useResponseBody"
import useDownloadResponse from "~/helpers/lenses/composables/useDownloadResponse"
const t = useI18n()
@@ -142,24 +144,14 @@ const props = defineProps<{
response: HoppRESTResponse
}>()
const toast = useToast()
const { responseBodyText } = useResponseBody(props.response)
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
}
})
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
const downloadIcon = ref("download")
const copyIcon = ref("copy")
const { downloadIcon, downloadResponse } = useDownloadResponse(
"application/json",
responseBodyText
)
const jsonBodyText = computed(() => {
try {
@@ -193,6 +185,7 @@ const { cursor } = useCodemirror(
},
linter: null,
completer: null,
environmentHighlights: true,
})
)
@@ -202,25 +195,6 @@ const jumpCursor = (ast: JSONValue | JSONObjectMember) => {
cursor.value = pos
}
const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadIcon.value = "check"
toast.success(`${t("state.download_started")}`)
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadIcon.value = "download"
}, 1000)
}
const outlinePath = computed(() => {
if (ast.value) {
return getJSONOutlineAtPos(
@@ -229,13 +203,6 @@ const outlinePath = computed(() => {
)
} else return null
})
const copyResponse = () => {
copyToClipboard(responseBodyText.value)
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
</script>
<style lang="scss" scoped>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-lowerSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("response.body") }}
@@ -40,9 +40,11 @@
<script setup lang="ts">
import { ref, computed, reactive } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { useI18n, useToast } from "~/helpers/utils/composables"
import { useI18n } from "~/helpers/utils/composables"
import useResponseBody from "~/helpers/lenses/composables/useResponseBody"
import useDownloadResponse from "~/helpers/lenses/composables/useDownloadResponse"
import useCopyResponse from "~/helpers/lenses/composables/useCopyResponse"
const t = useI18n()
@@ -50,24 +52,7 @@ const props = defineProps<{
response: HoppRESTResponse
}>()
const toast = useToast()
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
}
})
const downloadIcon = ref("download")
const copyIcon = ref("copy")
const { responseBodyText } = useResponseBody(props.response)
const responseType = computed(() => {
return (
@@ -78,6 +63,13 @@ const responseType = computed(() => {
.toLowerCase()
})
const { downloadIcon, downloadResponse } = useDownloadResponse(
responseType.value,
responseBodyText
)
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
const rawResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
@@ -92,32 +84,7 @@ useCodemirror(
},
linter: null,
completer: null,
environmentHighlights: true,
})
)
const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: responseType.value })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadIcon.value = "check"
toast.success(`${t("state.download_started")}`)
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadIcon.value = "download"
}, 1000)
}
const copyResponse = () => {
copyToClipboard(responseBodyText.value)
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-lowerSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("response.body") }}
@@ -40,9 +40,11 @@
<script setup lang="ts">
import { computed, ref, reactive } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { useI18n, useToast } from "~/helpers/utils/composables"
import { useI18n } from "~/helpers/utils/composables"
import useResponseBody from "~/helpers/lenses/composables/useResponseBody"
import useDownloadResponse from "~/helpers/lenses/composables/useDownloadResponse"
import useCopyResponse from "~/helpers/lenses/composables/useCopyResponse"
const t = useI18n()
@@ -50,24 +52,7 @@ const props = defineProps<{
response: HoppRESTResponse
}>()
const toast = useToast()
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
}
})
const downloadIcon = ref("download")
const copyIcon = ref("copy")
const { responseBodyText } = useResponseBody(props.response)
const responseType = computed(() => {
return (
@@ -78,6 +63,13 @@ const responseType = computed(() => {
.toLowerCase()
})
const { downloadIcon, downloadResponse } = useDownloadResponse(
responseType.value,
responseBodyText
)
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
const xmlResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
@@ -92,32 +84,7 @@ useCodemirror(
},
linter: null,
completer: null,
environmentHighlights: true,
})
)
const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: responseType.value })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadIcon.value = "check"
toast.success(`${t("state.download_started")}`)
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadIcon.value = "download"
}, 1000)
}
const copyResponse = () => {
copyToClipboard(responseBodyText.value)
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
</script>

View File

@@ -1,12 +1,15 @@
<template>
<div class="cursor-pointer flex h-5 w-5 relative items-center justify-center">
<div
tabindex="0"
class="relative flex items-center justify-center w-5 h-5 rounded-full cursor-pointer focus:outline-none focus-visible:ring focus-visible:ring-primaryDark"
>
<img
class="bg-primaryDark rounded-full object-cover object-center h-5 transition w-5 absolute"
class="absolute object-cover object-center w-5 h-5 transition rounded-full bg-primaryDark"
:src="url"
:alt="alt"
loading="lazy"
/>
<div class="rounded-full shadow-inner inset-0 absolute"></div>
<div class="absolute inset-0 rounded-full shadow-inner"></div>
<span
v-if="indicator"
class="border-primary rounded-full border-2 h-2.5 -top-0.5 -right-0.5 w-2.5 absolute"

View File

@@ -1,9 +1,9 @@
<template>
<div class="flex flex-col">
<div
class="bg-primary border-b border-dividerLight flex flex-1 pl-4 top-0 z-10 sticky items-center justify-between"
class="sticky top-0 z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight"
>
<label for="log" class="font-semibold text-secondaryLight py-2">
<label for="log" class="py-2 font-semibold text-secondaryLight">
{{ title }}
</label>
</div>

View File

@@ -13,63 +13,57 @@
:size="COLUMN_LAYOUT ? 45 : 50"
class="hide-scrollbar !overflow-auto"
>
<AppSection label="request">
<div
class="bg-primary flex flex-col space-y-4 p-4 top-0 z-10 sticky"
>
<div class="space-x-2 flex-1 inline-flex">
<input
id="mqtt-url"
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
class="bg-primaryLight border border-divider rounded text-secondaryDark w-full py-2 px-4 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
:placeholder="$t('mqtt.url')"
:disabled="connectionState"
@keyup.enter="validUrl ? toggleConnection() : null"
/>
<ButtonPrimary
id="connect"
:disabled="!validUrl"
class="w-32"
:label="
connectionState
? $t('action.disconnect')
: $t('action.connect')
"
:loading="connectingState"
@click.native="toggleConnection"
/>
</div>
<div class="flex space-x-4">
<input
id="mqtt-username"
v-model="username"
type="text"
spellcheck="false"
class="input"
:placeholder="$t('authorization.username')"
/>
<input
id="mqtt-password"
v-model="password"
type="password"
spellcheck="false"
class="input"
:placeholder="$t('authorization.password')"
/>
</div>
<div class="sticky top-0 z-10 flex flex-col p-4 space-y-4 bg-primary">
<div class="inline-flex flex-1 space-x-2">
<input
id="mqtt-url"
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
:placeholder="$t('mqtt.url')"
:disabled="connectionState"
@keyup.enter="validUrl ? toggleConnection() : null"
/>
<ButtonPrimary
id="connect"
:disabled="!validUrl"
class="w-32"
:label="
connectionState
? $t('action.disconnect')
: $t('action.connect')
"
:loading="connectingState"
@click.native="toggleConnection"
/>
</div>
</AppSection>
<div class="flex space-x-4">
<input
id="mqtt-username"
v-model="username"
type="text"
spellcheck="false"
class="input"
:placeholder="$t('authorization.username')"
/>
<input
id="mqtt-password"
v-model="password"
type="password"
spellcheck="false"
class="input"
:placeholder="$t('authorization.password')"
/>
</div>
</div>
</Pane>
<Pane
:size="COLUMN_LAYOUT ? 65 : 50"
class="hide-scrollbar !overflow-auto"
>
<AppSection label="response">
<RealtimeLog :title="$t('mqtt.log')" :log="log" />
</AppSection>
<RealtimeLog :title="$t('mqtt.log')" :log="log" />
</Pane>
</Splitpanes>
</Pane>
@@ -79,75 +73,71 @@
min-size="20"
class="hide-scrollbar !overflow-auto"
>
<AppSection label="messages">
<div class="flex flex-col flex-1 p-4 inline-flex">
<label for="pub_topic" class="font-semibold text-secondaryLight">
{{ $t("mqtt.topic") }}
</label>
</div>
<div class="flex px-4">
<input
id="pub_topic"
v-model="pub_topic"
class="input"
:placeholder="$t('mqtt.topic_name')"
type="text"
autocomplete="off"
spellcheck="false"
/>
</div>
<div class="flex flex-1 p-4 items-center justify-between">
<label for="mqtt-message" class="font-semibold text-secondaryLight">
{{ $t("mqtt.communication") }}
</label>
</div>
<div class="flex space-x-2 px-4">
<input
id="mqtt-message"
v-model="msg"
class="input"
type="text"
autocomplete="off"
:placeholder="$t('mqtt.message')"
spellcheck="false"
/>
<ButtonPrimary
id="publish"
name="get"
:disabled="!canpublish"
:label="$t('mqtt.publish')"
@click.native="publish"
/>
</div>
<div
class="border-t border-dividerLight flex flex-col flex-1 mt-4 p-4 inline-flex"
>
<label for="sub_topic" class="font-semibold text-secondaryLight">
{{ $t("mqtt.topic") }}
</label>
</div>
<div class="flex space-x-2 px-4">
<input
id="sub_topic"
v-model="sub_topic"
type="text"
autocomplete="off"
:placeholder="$t('mqtt.topic_name')"
spellcheck="false"
class="input"
/>
<ButtonPrimary
id="subscribe"
name="get"
:disabled="!cansubscribe"
:label="
subscriptionState ? $t('mqtt.unsubscribe') : $t('mqtt.subscribe')
"
reverse
@click.native="toggleSubscription"
/>
</div>
</AppSection>
<div class="flex flex-col flex-1 p-4">
<label for="pub_topic" class="font-semibold text-secondaryLight">
{{ $t("mqtt.topic") }}
</label>
</div>
<div class="flex px-4">
<input
id="pub_topic"
v-model="pub_topic"
class="input"
:placeholder="$t('mqtt.topic_name')"
type="text"
autocomplete="off"
spellcheck="false"
/>
</div>
<div class="flex items-center justify-between flex-1 p-4">
<label for="mqtt-message" class="font-semibold text-secondaryLight">
{{ $t("mqtt.communication") }}
</label>
</div>
<div class="flex px-4 space-x-2">
<input
id="mqtt-message"
v-model="msg"
class="input"
type="text"
autocomplete="off"
:placeholder="$t('mqtt.message')"
spellcheck="false"
/>
<ButtonPrimary
id="publish"
name="get"
:disabled="!canpublish"
:label="$t('mqtt.publish')"
@click.native="publish"
/>
</div>
<div class="flex flex-col flex-1 p-4 mt-4 border-t border-dividerLight">
<label for="sub_topic" class="font-semibold text-secondaryLight">
{{ $t("mqtt.topic") }}
</label>
</div>
<div class="flex px-4 space-x-2">
<input
id="sub_topic"
v-model="sub_topic"
type="text"
autocomplete="off"
:placeholder="$t('mqtt.topic_name')"
spellcheck="false"
class="input"
/>
<ButtonPrimary
id="subscribe"
name="get"
:disabled="!cansubscribe"
:label="
subscriptionState ? $t('mqtt.unsubscribe') : $t('mqtt.subscribe')
"
reverse
@click.native="toggleSubscription"
/>
</div>
</Pane>
</Splitpanes>
</template>

View File

@@ -13,84 +13,209 @@
:size="COLUMN_LAYOUT ? 45 : 50"
class="hide-scrollbar !overflow-auto"
>
<AppSection label="request">
<div class="bg-primary flex p-4 top-0 z-10 sticky">
<div class="space-x-2 flex-1 inline-flex">
<div class="flex flex-1">
<label for="client-version">
<tippy
ref="versionOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<span class="select-wrapper">
<input
id="client-version"
v-tippy="{ theme: 'tooltip' }"
title="socket.io-client version"
class="bg-primaryLight border border-divider rounded-l cursor-pointer flex font-semibold text-secondaryDark py-2 px-4 w-26 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
:value="`Client ${clientVersion}`"
readonly
:disabled="connectionState"
/>
</span>
</template>
<SmartItem
v-for="(_, version) in socketIoClients"
:key="`client-${version}`"
:label="`Client ${version}`"
@click.native="onSelectVersion(version)"
/>
</tippy>
</label>
<input
id="socketio-url"
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
:class="{ error: !urlValid }"
class="bg-primaryLight border border-divider flex flex-1 text-secondaryDark w-full py-2 px-4 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
:placeholder="$t('socketio.url')"
:disabled="connectionState"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
<input
id="socketio-path"
v-model="path"
class="bg-primaryLight border border-divider rounded-r flex flex-1 text-secondaryDark w-full py-2 px-4 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
spellcheck="false"
:disabled="connectionState"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
</div>
<ButtonPrimary
id="connect"
:disabled="!urlValid"
name="connect"
class="w-32"
:label="
!connectionState
? $t('action.connect')
: $t('action.disconnect')
<div class="sticky top-0 z-10 flex p-4 bg-primary">
<div class="inline-flex flex-1 space-x-2">
<div class="flex flex-1">
<label for="client-version">
<tippy
ref="versionOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<span class="select-wrapper">
<input
id="client-version"
v-tippy="{ theme: 'tooltip' }"
title="socket.io-client version"
class="flex px-4 py-2 font-semibold border rounded-l cursor-pointer bg-primaryLight border-divider text-secondaryDark w-26 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
:value="`Client ${clientVersion}`"
readonly
:disabled="connectionState"
/>
</span>
</template>
<SmartItem
v-for="(_, version) in socketIoClients"
:key="`client-${version}`"
:label="`Client ${version}`"
@click.native="onSelectVersion(version)"
/>
</tippy>
</label>
<input
id="socketio-url"
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
:class="{ error: !urlValid }"
class="flex flex-1 w-full px-4 py-2 border bg-primaryLight border-divider text-secondaryDark hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
:placeholder="$t('socketio.url')"
:disabled="connectionState"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
<input
id="socketio-path"
v-model="path"
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
spellcheck="false"
:disabled="connectionState"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
</div>
<ButtonPrimary
id="connect"
:disabled="!urlValid"
name="connect"
class="w-32"
:label="
!connectionState
? $t('action.connect')
: $t('action.disconnect')
"
:loading="connectingState"
@click.native="toggleConnection"
/>
</div>
</div>
<div
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold text-secondaryLight">
{{ $t("authorization.type") }}
</label>
<tippy
ref="authTypeOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<span class="select-wrapper">
<ButtonSecondary
class="pr-8 ml-2 rounded-none"
:label="authType"
/>
</span>
</template>
<SmartItem
label="None"
:icon="
authType === 'None'
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:loading="connectingState"
@click.native="toggleConnection"
:active="authType === 'None'"
@click.native="
() => {
authType = 'None'
authTypeOptions.tippy().hide()
}
"
/>
<SmartItem
label="Bearer Token"
:icon="
authType === 'Bearer'
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:active="authType === 'Bearer'"
@click.native="
() => {
authType = 'Bearer'
authTypeOptions.tippy().hide()
}
"
/>
</tippy>
</span>
<div class="flex">
<SmartCheckbox
:on="authActive"
class="px-2"
@change="authActive = !authActive"
>
{{ $t("state.enabled") }}
</SmartCheckbox>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/authorization"
blank
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear')"
svg="trash-2"
@click.native="clearContent"
/>
</div>
</div>
<div
v-if="authType === 'None'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/login.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="$t('empty.authorization')"
/>
<span class="pb-4 text-center">
This SocketIO connection does not use any authentication.
</span>
<ButtonSecondary
outline
:label="$t('app.documentation')"
to="https://docs.hoppscotch.io/features/authorization"
blank
svg="external-link"
reverse
class="mb-4"
/>
</div>
<div
v-if="authType === 'Bearer'"
class="flex border-b border-dividerLight"
>
<div class="w-2/3 border-r border-dividerLight">
<div class="flex border-b border-dividerLight">
<SmartEnvInput
v-model="bearerToken"
placeholder="Token"
styles="bg-transparent flex flex-1 py-1 px-4"
/>
</div>
</div>
</AppSection>
<div
class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
>
<div class="p-2">
<div class="pb-2 text-secondaryLight">
{{ $t("helpers.authorization") }}
</div>
<SmartAnchor
class="link"
:label="`${$t('authorization.learn')} \xA0 →`"
to="https://docs.hoppscotch.io/features/authorization"
blank
/>
</div>
</div>
</div>
</Pane>
<Pane
:size="COLUMN_LAYOUT ? 65 : 50"
class="hide-scrollbar !overflow-auto"
>
<AppSection label="response">
<RealtimeLog :title="$t('socketio.log')" :log="log" />
</AppSection>
<RealtimeLog :title="$t('socketio.log')" :log="log" />
</Pane>
</Splitpanes>
</Pane>
@@ -100,80 +225,78 @@
min-size="20"
class="hide-scrollbar !overflow-auto"
>
<AppSection label="messages">
<div class="flex flex-col flex-1 p-4 inline-flex">
<label for="events" class="font-semibold text-secondaryLight">
{{ $t("socketio.events") }}
</label>
</div>
<div class="flex px-4">
<input
id="event_name"
v-model="communication.eventName"
class="input"
name="event_name"
:placeholder="$t('socketio.event_name')"
type="text"
autocomplete="off"
:disabled="!connectionState"
<div class="flex flex-col flex-1 p-4">
<label for="events" class="font-semibold text-secondaryLight">
{{ $t("socketio.events") }}
</label>
</div>
<div class="flex px-4">
<input
id="event_name"
v-model="communication.eventName"
class="input"
name="event_name"
:placeholder="$t('socketio.event_name')"
type="text"
autocomplete="off"
:disabled="!connectionState"
/>
</div>
<div class="flex items-center justify-between flex-1 p-4">
<label class="font-semibold text-secondaryLight">
{{ $t("socketio.communication") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('add.new')"
svg="plus"
@click.native="addCommunicationInput"
/>
</div>
<div class="flex flex-1 p-4 items-center justify-between">
<label class="font-semibold text-secondaryLight">
{{ $t("socketio.communication") }}
</label>
<div class="flex">
</div>
<div class="flex flex-col px-4 pb-4 space-y-2">
<div
v-for="(input, index) of communication.inputs"
:key="`input-${index}`"
>
<div class="flex space-x-2">
<input
v-model="communication.inputs[index]"
class="input"
name="message"
:placeholder="$t('count.message', { count: index + 1 })"
type="text"
autocomplete="off"
:disabled="!connectionState"
@keyup.enter="connectionState ? sendMessage() : null"
/>
<ButtonSecondary
v-if="index + 1 !== communication.inputs.length"
v-tippy="{ theme: 'tooltip' }"
:title="$t('add.new')"
svg="plus"
@click.native="addCommunicationInput"
:title="$t('action.remove')"
svg="trash"
color="red"
outline
@click.native="removeCommunicationInput({ index })"
/>
<ButtonPrimary
v-if="index + 1 === communication.inputs.length"
id="send"
name="send"
:disabled="!connectionState"
:label="$t('action.send')"
@click.native="sendMessage"
/>
</div>
</div>
<div class="flex flex-col space-y-2 px-4 pb-4">
<div
v-for="(input, index) of communication.inputs"
:key="`input-${index}`"
>
<div class="flex space-x-2">
<input
v-model="communication.inputs[index]"
class="input"
name="message"
:placeholder="$t('count.message', { count: index + 1 })"
type="text"
autocomplete="off"
:disabled="!connectionState"
@keyup.enter="connectionState ? sendMessage() : null"
/>
<ButtonSecondary
v-if="index + 1 !== communication.inputs.length"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.remove')"
svg="trash"
color="red"
outline
@click.native="removeCommunicationInput({ index })"
/>
<ButtonPrimary
v-if="index + 1 === communication.inputs.length"
id="send"
name="send"
:disabled="!connectionState"
:label="$t('action.send')"
@click.native="sendMessage"
/>
</div>
</div>
</div>
</AppSection>
</div>
</Pane>
</Splitpanes>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import { defineComponent, ref } from "@nuxtjs/composition-api"
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
// All Socket.IO client version imports
@@ -235,6 +358,7 @@ export default defineComponent({
),
io: useStream(SIOSocket$, null, setSIOSocket),
log: useStream(SIOLog$, [], setSIOLog),
authTypeOptions: ref(null),
}
},
data() {
@@ -244,6 +368,9 @@ export default defineComponent({
eventName: "",
inputs: [""],
},
authType: "None",
bearerToken: "",
authActive: true,
}
},
computed: {
@@ -303,7 +430,17 @@ export default defineComponent({
this.path = "/socket.io"
}
const Client = socketIoClients[this.clientVersion]
this.io = new Client(this.url, { path: this.path })
if (this.authActive && this.authType === "Bearer") {
this.io = new Client(this.url, {
path: this.path,
auth: {
token: this.bearerToken,
},
})
} else {
this.io = new Client(this.url, { path: this.path })
}
// Add ability to listen to all events
wildcard(Client.Manager)(this.io)
this.io.on("connect", () => {

View File

@@ -1,8 +1,8 @@
<template>
<Splitpanes class="smart-splitter" :horizontal="COLUMN_LAYOUT">
<Pane :size="COLUMN_LAYOUT ? 45 : 50" class="hide-scrollbar !overflow-auto">
<div class="bg-primary flex p-4 top-0 z-10 sticky">
<div class="space-x-2 flex-1 inline-flex">
<div class="sticky top-0 z-10 flex p-4 bg-primary">
<div class="inline-flex flex-1 space-x-2">
<div class="flex flex-1">
<input
id="server"
@@ -10,21 +10,21 @@
type="url"
autocomplete="off"
:class="{ error: !serverValid }"
class="bg-primaryLight border border-divider rounded-l flex flex-1 text-secondaryDark w-full py-2 px-4 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
class="flex flex-1 w-full px-4 py-2 border rounded-l bg-primaryLight border-divider text-secondaryDark hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
:placeholder="$t('sse.url')"
:disabled="connectionSSEState"
@keyup.enter="serverValid ? toggleSSEConnection() : null"
/>
<label
for="event-type"
class="bg-primaryLight border-t border-b border-divider font-semibold text-secondaryLight py-2 px-4 truncate"
class="px-4 py-2 font-semibold truncate border-t border-b bg-primaryLight border-divider text-secondaryLight"
>
{{ $t("sse.event_type") }}
</label>
<input
id="event-type"
v-model="eventType"
class="bg-primaryLight border border-divider rounded-r flex flex-1 text-secondaryDark w-full py-2 px-4 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
spellcheck="false"
:disabled="connectionSSEState"
@keyup.enter="serverValid ? toggleSSEConnection() : null"
@@ -45,14 +45,7 @@
</div>
</Pane>
<Pane :size="COLUMN_LAYOUT ? 65 : 50" class="hide-scrollbar !overflow-auto">
<AppSection label="response">
<ul>
<li>
<RealtimeLog :title="$t('sse.log')" :log="log" />
<div id="result"></div>
</li>
</ul>
</AppSection>
<RealtimeLog :title="$t('sse.log')" :log="log" />
</Pane>
</Splitpanes>
</template>

View File

@@ -13,135 +13,131 @@
:size="COLUMN_LAYOUT ? 45 : 50"
class="hide-scrollbar !overflow-auto"
>
<AppSection label="request">
<div class="bg-primary flex p-4 top-0 z-10 sticky">
<div class="space-x-2 flex-1 inline-flex">
<input
id="websocket-url"
v-model="url"
class="bg-primaryLight border border-divider rounded text-secondaryDark w-full py-2 px-4 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
type="url"
autocomplete="off"
spellcheck="false"
:class="{ error: !urlValid }"
:placeholder="$t('websocket.url')"
:disabled="connectionState"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
<ButtonPrimary
id="connect"
:disabled="!urlValid"
class="w-32"
name="connect"
:label="
!connectionState
? $t('action.connect')
: $t('action.disconnect')
"
:loading="connectingState"
@click.native="toggleConnection"
/>
</div>
</div>
<div
class="bg-primary border-b border-dividerLight flex flex-1 top-upperPrimaryStickyFold pl-4 z-10 sticky items-center justify-between"
>
<label class="font-semibold text-secondaryLight">
{{ $t("websocket.protocols") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear_all')"
svg="trash-2"
@click.native="clearContent"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('add.new')"
svg="plus"
@click.native="addProtocol"
/>
</div>
</div>
<div
v-for="(protocol, index) of protocols"
:key="`protocol-${index}`"
class="divide-dividerLight divide-x border-b border-dividerLight flex"
>
<div class="sticky top-0 z-10 flex p-4 bg-primary">
<div class="inline-flex flex-1 space-x-2">
<input
v-model="protocol.value"
class="bg-transparent flex flex-1 py-2 px-4"
:placeholder="$t('count.protocol', { count: index + 1 })"
name="message"
type="text"
id="websocket-url"
v-model="url"
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
type="url"
autocomplete="off"
@change="
spellcheck="false"
:class="{ error: !urlValid }"
:placeholder="$t('websocket.url')"
:disabled="connectionState"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
<ButtonPrimary
id="connect"
:disabled="!urlValid"
class="w-32"
name="connect"
:label="
!connectionState
? $t('action.connect')
: $t('action.disconnect')
"
:loading="connectingState"
@click.native="toggleConnection"
/>
</div>
</div>
<div
class="sticky z-10 flex items-center justify-between flex-1 pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ $t("websocket.protocols") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear_all')"
svg="trash-2"
@click.native="clearContent"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('add.new')"
svg="plus"
@click.native="addProtocol"
/>
</div>
</div>
<div
v-for="(protocol, index) of protocols"
:key="`protocol-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight"
>
<input
v-model="protocol.value"
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="$t('count.protocol', { count: index + 1 })"
name="message"
type="text"
autocomplete="off"
@change="
updateProtocol(index, {
value: $event.target.value,
active: protocol.active,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
protocol.hasOwnProperty('active')
? protocol.active
? $t('action.turn_off')
: $t('action.turn_on')
: $t('action.turn_off')
"
:svg="
protocol.hasOwnProperty('active')
? protocol.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateProtocol(index, {
value: $event.target.value,
active: protocol.active,
value: protocol.value,
active: !protocol.active,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
protocol.hasOwnProperty('active')
? protocol.active
? $t('action.turn_off')
: $t('action.turn_on')
: $t('action.turn_off')
"
:svg="
protocol.hasOwnProperty('active')
? protocol.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateProtocol(index, {
value: protocol.value,
active: !protocol.active,
})
"
/>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.remove')"
svg="trash"
color="red"
@click.native="deleteProtocol({ index })"
/>
</span>
</div>
<div
v-if="protocols.length === 0"
class="flex flex-col text-secondaryLight p-4 items-center justify-center"
>
<img
:src="`/images/states/${$colorMode.value}/add_category.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="$t('empty.protocols')"
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.remove')"
svg="trash"
color="red"
@click.native="deleteProtocol({ index })"
/>
<span class="text-center mb-4">
{{ $t("empty.protocols") }}
</span>
</div>
</AppSection>
</span>
</div>
<div
v-if="protocols.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_category.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="$t('empty.protocols')"
/>
<span class="mb-4 text-center">
{{ $t("empty.protocols") }}
</span>
</div>
</Pane>
<Pane
:size="COLUMN_LAYOUT ? 65 : 50"
class="hide-scrollbar !overflow-auto"
>
<AppSection label="response">
<RealtimeLog :title="$t('websocket.log')" :log="log" />
</AppSection>
<RealtimeLog :title="$t('websocket.log')" :log="log" />
</Pane>
</Splitpanes>
</Pane>
@@ -151,38 +147,36 @@
min-size="20"
class="hide-scrollbar !overflow-auto"
>
<AppSection label="messages">
<div class="flex flex-col flex-1 p-4 inline-flex">
<label
for="websocket-message"
class="font-semibold text-secondaryLight"
>
{{ $t("websocket.communication") }}
</label>
</div>
<div class="flex space-x-2 px-4">
<input
id="websocket-message"
v-model="communication.input"
name="message"
type="text"
autocomplete="off"
:disabled="!connectionState"
:placeholder="$t('websocket.message')"
class="input"
@keyup.enter="connectionState ? sendMessage() : null"
@keyup.up="connectionState ? walkHistory('up') : null"
@keyup.down="connectionState ? walkHistory('down') : null"
/>
<ButtonPrimary
id="send"
name="send"
:disabled="!connectionState"
:label="$t('action.send')"
@click.native="sendMessage"
/>
</div>
</AppSection>
<div class="flex flex-col flex-1 p-4">
<label
for="websocket-message"
class="font-semibold text-secondaryLight"
>
{{ $t("websocket.communication") }}
</label>
</div>
<div class="flex px-4 space-x-2">
<input
id="websocket-message"
v-model="communication.input"
name="message"
type="text"
autocomplete="off"
:disabled="!connectionState"
:placeholder="$t('websocket.message')"
class="input"
@keyup.enter="connectionState ? sendMessage() : null"
@keyup.up="connectionState ? walkHistory('up') : null"
@keyup.down="connectionState ? walkHistory('down') : null"
/>
<ButtonPrimary
id="send"
name="send"
:disabled="!connectionState"
:label="$t('action.send')"
@click.native="sendMessage"
/>
</div>
</Pane>
</Splitpanes>
</template>

View File

@@ -8,7 +8,6 @@
:placeholder="placeholder"
:spellcheck="spellcheck"
:autocapitalize="autocapitalize"
:autocorrect="spellcheck"
:class="styles"
@input="updateSuggestions"
@keyup="updateSuggestions"

View File

@@ -16,15 +16,23 @@
</span>
</template>
<NuxtLink
v-for="(locale, index) in $i18n.locales.filter(
({ code }) => code !== $i18n.locale
)"
:key="`locale-${String(index)}`"
v-for="(locale, index) in $i18n.locales"
:key="`locale-${index}`"
:to="switchLocalePath(locale.code)"
@click="$refs.language.tippy().hide()"
@click="language.tippy().hide()"
>
<SmartItem :label="locale.name" />
<SmartItem
:label="locale.name"
:active-info-icon="$i18n.locale === locale.code"
:info-icon="$i18n.locale === locale.code ? 'done' : null"
/>
</NuxtLink>
</tippy>
</span>
</template>
<script setup lang="ts">
import { ref } from "@nuxtjs/composition-api"
const language = ref<any | null>(null)
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="cursor-pointer flex-nowrap transition inline-flex items-center justify-center group hover:text-secondaryDark"
class="inline-flex items-center justify-center transition cursor-pointer flex-nowrap group hover:text-secondaryDark"
@click="$emit('change')"
>
<input
@@ -12,7 +12,7 @@
/>
<label
for="checkbox"
class="cursor-pointer font-semibold pl-0 align-middle"
class="pl-0 font-semibold align-middle cursor-pointer"
>
<slot></slot>
</label>

View File

@@ -16,6 +16,8 @@
@keyup="$emit('keyup', $event)"
@click="$emit('click', $event)"
@keydown="$emit('keydown', $event)"
@paste="handlePaste"
@compositionend="handleCompositionEnd"
></div>
</div>
</template>
@@ -71,7 +73,7 @@ export default defineComponent({
highlightEnabled: true,
highlightStyle: "",
caseSensitive: true,
fireOn: "keydown",
fireOn: "input",
fireOnEnabled: true,
}
},
@@ -113,11 +115,23 @@ export default defineComponent({
},
methods: {
handleChange() {
handleCompositionEnd() {
this.handleChange()
},
handlePaste(ev) {
this.handleChange()
this.$emit("paste", { event: ev, previousValue: this.internalValue })
},
handleChange(e = null) {
if (e && "inputType" in e && e.inputType === "insertCompositionText") {
return
}
this.debouncedHandler = debounce(function () {
if (this.internalValue !== this.$refs.editor.textContent) {
this.internalValue = this.$refs.editor.textContent
this.processHighlights()
if (this.$refs.editor) {
if (this.internalValue !== this.$refs.editor.textContent) {
this.internalValue = this.$refs.editor.textContent
this.processHighlights()
}
}
}, 5)
this.debouncedHandler()
@@ -479,7 +493,6 @@ export default defineComponent({
[contenteditable] {
@apply select-text;
@apply text-secondaryDark;
@apply font-medium;
&:empty {
@apply leading-loose;

View File

@@ -0,0 +1,26 @@
<template>
<div
class="relative flex flex-col space-y-2 overflow-hidden"
:class="expand ? 'h-full' : 'max-h-32'"
>
<slot name="body"></slot>
<div class="flex sticky bottom-0 inset-x-0 items-center justify-center">
<ButtonSecondary
:icon="expand ? 'expand_less' : 'expand_more'"
:label="expand ? t('action.less') : t('action.more')"
filled
rounded
@click.native="expand = !expand"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "@nuxtjs/composition-api"
import { useI18n } from "~/helpers/utils/composables"
const t = useI18n()
const expand = ref(false)
</script>

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