Compare commits

...

320 Commits

Author SHA1 Message Date
liyasthomas
d3a1898dad feat: support to move sidebar to left - fixed #1933 2021-11-04 09:58:15 +05:30
Andrew Bastin
45e508fc36 refactor: pwa install prompt (#1932) 2021-11-03 23:44:01 +05:30
0xc0Der
8edad7ded7 persist environment selection. (#1925)
* feat: persist environment selection

* refactor: minor change to selectedEnvIndex calculation

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-11-03 23:15:52 +05:30
liyasthomas
75e1adb7b3 refactor: empty state illustrations 2021-11-03 16:10:06 +05:30
Andrew Bastin
22ac13f2f0 fix: docker action issues 2021-11-02 21:22:37 +05:30
liyasthomas
7f0246eb47 Merge branch 'refactor/backend' of https://github.com/hoppscotch/hoppscotch into refactor/backend 2021-11-02 19:48:34 +05:30
liyasthomas
5f0800760f chore: clean up 2021-11-02 19:45:41 +05:30
liyasthomas
6db99c9e37 chore(deps): bump 2021-11-02 19:42:44 +05:30
liyasthomas
3b2cabd3f3 fix: typo 2021-11-02 19:39:39 +05:30
Andrew Bastin
b0dd6b0bd6 feat: fix gqlclient race condition 2021-11-02 19:33:09 +05:30
liyasthomas
874b846e60 fix: fix broken edit team modal
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-11-02 19:33:09 +05:30
liyasthomas
dbe2525c6f docs: updated feature list 2021-11-02 19:33:09 +05:30
Andrew Bastin
afd414fa3f feat: introducing gql polling 2021-11-02 19:33:09 +05:30
liyasthomas
94763dcb31 fix: prompt user on actions with no permission 2021-11-02 19:33:09 +05:30
Andrew Bastin
6314740f46 feat: reinit gql client after login 2021-11-02 19:33:09 +05:30
liyasthomas
d7332120e3 fix: hide popover on click on update role 2021-11-02 19:33:09 +05:30
liyasthomas
8c74fe9925 feat: user's roles prompt on invite modal 2021-11-02 19:33:09 +05:30
liyasthomas
c74ddeb530 fix: team invitation error states 2021-11-02 19:33:09 +05:30
Andrew Bastin
e31c0a9d02 feat: fix subscription auth refresh issues 2021-11-02 19:33:09 +05:30
Andrew Bastin
d9e5d4aec5 feat: add subscriptions for team invitations added and removed 2021-11-02 19:33:09 +05:30
liyasthomas
c1ee8f5dd0 feat: subscriptions on edit team modal 2021-11-02 19:33:09 +05:30
Andrew Bastin
dd59de3de0 feat: add subscription support for useGQLQuery 2021-11-02 19:33:07 +05:30
liyasthomas
511a3c55f3 fix: broken edit team modal 2021-11-02 19:32:49 +05:30
liyasthomas
c9021ab3ca fix: broken edit team action 2021-11-02 19:32:49 +05:30
liyasthomas
a765c4a7cc feat: successfully joined team prompt 2021-11-02 19:32:48 +05:30
Andrew Bastin
ea99732474 refactor: use the unpaginated teamMembers field instead of members 2021-11-02 19:32:48 +05:30
liyasthomas
6c64ffe833 feat: invite system error states and user prompts 2021-11-02 19:32:48 +05:30
Andrew Bastin
5fa6c6cdb3 fix: improper parsed error checks 2021-11-02 19:32:48 +05:30
Andrew Bastin
d94759870e refactor: add error info to send invite results 2021-11-02 19:32:48 +05:30
Andrew Bastin
f0a6fc641a feat: team invitation caching 2021-11-02 19:32:48 +05:30
Andrew Bastin
7ba00bee0b feat: update graphcache keys typings 2021-11-02 19:32:48 +05:30
liyasthomas
dc2bdf81b9 fix: invitation acceptance flow 2021-11-02 19:32:48 +05:30
liyasthomas
187a30abac feat: init loading states, minor ui improvements 2021-11-02 19:32:48 +05:30
liyasthomas
5b824ccb17 feat: minor ui improvements on invite flow 2021-11-02 19:32:48 +05:30
liyasthomas
3bdf2baf97 feat: init invitation wiring
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-11-02 19:32:48 +05:30
Andrew Bastin
9af8a24a89 feat: initial team invites implementation
Co-authored-by: Liyas Thomas <hi@liyasthomas.com>
2021-11-02 19:32:48 +05:30
liyasthomas
57c4759bdb chore: minor fixes and improvements 2021-11-02 19:32:48 +05:30
Andrew Bastin
d9d7261bc5 feat: team invitation mutations 2021-11-02 19:32:48 +05:30
Andrew Bastin
a12315d81a refactor: add graphcache codegen and update types 2021-11-02 19:32:47 +05:30
liyasthomas
9f0956556f feat: support updating user's display name 2021-11-02 19:32:13 +05:30
liyasthomas
748318d44e feat: compact mode for team component 2021-11-02 19:32:13 +05:30
liyasthomas
ff3062cdfc chore: clean up 2021-11-02 19:32:13 +05:30
liyasthomas
48d67fe7e1 fix: init local persistence 2021-11-02 19:32:13 +05:30
liyasthomas
2c9918f9a7 feat: init theming in empty layout 2021-11-02 19:32:13 +05:30
liyasthomas
ee03952201 feat: init teams invite page 2021-11-02 19:32:13 +05:30
liyasthomas
43dcd3c443 feat: invite modal mvp 2021-11-02 19:32:13 +05:30
Andrew Bastin
8f8c42a92a feat: gql codegen + caching + optimistic 2021-11-02 19:32:11 +05:30
liyasthomas
6496aded25 feat: teams modal wrapper 2021-11-02 19:25:36 +05:30
Andrew Bastin
eacf8113af feat: initial setup of new backend comms 2021-11-02 19:20:07 +05:30
liyasthomas
c4a1527153 chore(deps): bump 2021-11-02 19:17:30 +05:30
liyasthomas
4a89a6aafc feat: teams invite modal 2021-11-02 19:17:11 +05:30
Andrew Bastin
52539b084d feat: gql codegen + caching + optimistic 2021-11-02 19:17:09 +05:30
Andrew Bastin
8d5bd051a1 refactor: extract cache info to separate files and add more mutations 2021-11-02 19:15:44 +05:30
liyasthomas
3809e9853e chore(ui): minor ui improvements, empty teams state 2021-11-02 19:15:44 +05:30
Andrew Bastin
5f795acd61 feat: probable user is displayed as authenticated 2021-11-02 19:15:44 +05:30
liyasthomas
17c550404f refactor: updated lint script 2021-11-02 19:15:44 +05:30
Andrew Bastin
a840079119 feat: add persistent cache and optimistic updates 2021-11-02 19:15:42 +05:30
Andrew Bastin
2761894164 fix: queries not waiting for authentication 2021-11-02 19:15:20 +05:30
liyasthomas
6b8bc618dc feat: teams modal wrapper 2021-11-02 19:15:02 +05:30
liyasthomas
258f79604f refactor: remove EAInvite flag 2021-11-02 19:15:02 +05:30
Andrew Bastin
81ae70ee04 fix: fix issue with team queries not updating correctly after mutation 2021-11-02 19:15:02 +05:30
Andrew Bastin
6b02d290a5 feat: team mutation 2021-11-02 19:15:02 +05:30
Andrew Bastin
7ab1bbaf62 refactor: refactor create team modal to new system 2021-11-02 19:15:02 +05:30
liyasthomas
079083d0f2 fix: i18n usage 2021-11-02 19:15:02 +05:30
liyasthomas
0504707aab feat: init profile page 2021-11-02 19:14:59 +05:30
Andrew Bastin
fb4aab875d feat: introduce updated mutation system and team.vue rewrite 2021-11-02 19:14:25 +05:30
Andrew Bastin
7bb32ecf7e feat: initial setup of new backend comms 2021-11-02 19:14:21 +05:30
Andrew Bastin
e129a5c179 feat: fix gqlclient race condition 2021-11-02 19:11:16 +05:30
liyasthomas
8045f26c19 fix: fix broken edit team modal
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-11-02 14:25:38 +05:30
liyasthomas
86516421b5 fix(ci): run build command from root 2021-11-02 07:01:48 +05:30
liyasthomas
bce88ccd44 docs: updated feature list 2021-11-02 06:41:18 +05:30
0xc0Der
66d408b7db Allow duplication of global environment (#1924) 2021-11-02 06:16:16 +05:30
Andrew Bastin
297bf3205f feat: introducing gql polling 2021-11-01 22:27:06 +05:30
liyasthomas
7366b32349 fix: prompt user on actions with no permission 2021-11-01 17:51:01 +05:30
Andrew Bastin
b7e0169c9b feat: reinit gql client after login 2021-11-01 17:30:16 +05:30
liyasthomas
6b6f85cc7e fix: hide popover on click on update role 2021-11-01 15:25:19 +05:30
liyasthomas
2c014a2f4b feat: user's roles prompt on invite modal 2021-11-01 15:04:46 +05:30
liyasthomas
d6df675821 fix: team invitation error states 2021-11-01 14:20:35 +05:30
liyasthomas
427baf4c79 chore: bump dep + minor ui fixes 2021-11-01 13:10:48 +05:30
liyasthomas
4f2b682341 chore(deps): bump 2021-10-31 19:53:05 +05:30
Andrew Bastin
e03f888cb2 feat: fix subscription auth refresh issues 2021-10-31 17:40:03 +05:30
Andrew Bastin
513396d498 feat: add subscriptions for team invitations added and removed 2021-10-30 18:56:34 +05:30
liyasthomas
8f04f0758b fix: use hoppscotch's realtime echo servers - resolved #1913 2021-10-29 08:55:31 +05:30
liyasthomas
7a77bfc248 feat: subscriptions on edit team modal 2021-10-28 20:29:49 +05:30
Andrew Bastin
ddd29374ea feat: add subscription support for useGQLQuery 2021-10-27 22:41:32 +05:30
liyasthomas
4b0d7a6c3d fix: broken edit team modal 2021-10-27 18:32:11 +05:30
liyasthomas
20bfc02a4e fix: disable spellcheck on contenteditable - resolved #1908 2021-10-27 18:17:22 +05:30
Darío Hereñú
2511724b73 chore: updated spanish translation (#1907) 2021-10-27 06:10:17 +05:30
liyasthomas
a851ee3fab fix: broken edit team action 2021-10-26 23:16:56 +05:30
liyasthomas
40f6e6f8a8 fix: typo in test result 2021-10-26 15:36:59 +05:30
liyasthomas
e2fd104c2d feat: test results indicator in response section's tests tab 2021-10-26 13:48:09 +05:30
Andrew Bastin
a3eafa54fa fix: test script not parsing json object if json content type 2021-10-26 13:19:15 +05:30
Sergio Guzmán Mayorga
b90b4a1910 chore: update spanish translations (#1904) 2021-10-26 06:47:28 +05:30
liyasthomas
247df4d5b9 chore: bump deps + fix broken links 2021-10-25 21:04:28 +05:30
Burak Saraloglu
4a3889a76e chore (i18n): updated Turkish translations (#1893) 2021-10-25 05:02:30 +05:30
liyasthomas
224a6e069c chore(deps): bump 2021-10-24 20:06:22 +05:30
liyasthomas
2c3097eeb7 feat: successfully joined team prompt 2021-10-24 14:46:32 +05:30
Andrew Bastin
3289ede0e8 refactor: use the unpaginated teamMembers field instead of members 2021-10-22 15:34:20 +05:30
liyasthomas
ddf21c17d2 feat: invite system error states and user prompts 2021-10-21 18:17:32 +05:30
Andrew Bastin
c9a24a0d28 fix: improper parsed error checks 2021-10-20 16:49:58 +05:30
Andrew Bastin
fa4b130b18 refactor: add error info to send invite results 2021-10-20 15:50:21 +05:30
Andrew Bastin
8561a7547f feat: team invitation caching 2021-10-20 15:33:20 +05:30
liyasthomas
e85f7b8232 feat: collapse collections/folders on clicking side border 2021-10-20 13:55:18 +05:30
Chun-Hao Lien
2f91d25ed4 chore: add json file extension (#1889) 2021-10-20 12:06:01 +05:30
Andrew Bastin
ae304b5af7 feat: update graphcache keys typings 2021-10-19 23:12:47 +05:30
Andrew Bastin
1c51f8b32e fix: bug with search box key listeners still active after close 2021-10-19 14:42:39 +05:30
liyasthomas
aaff07bba2 Merge remote-tracking branch 'origin/keyboard-navigation-on-power-search' 2021-10-19 13:45:48 +05:30
Francisco Emanuel de Sales Pereira
4bc38d5e0f Feature: Adds arrow keys navigation on power search (#1888) 2021-10-19 13:44:14 +05:30
liyasthomas
fdfca00886 feat: set active on hover + minor ui improvements 2021-10-19 13:40:47 +05:30
Liyas Thomas
d872e393f8 Merge branch 'main' into keyboard-navigation-on-power-search 2021-10-19 10:56:00 +05:30
franciscoemanuel
686d8e5be7 feat: adds arrow keys navigation on powersearch 2021-10-19 02:20:30 -03:00
liyasthomas
4e30efd737 fix: invitation acceptance flow 2021-10-18 21:43:13 +05:30
rubjo
aa4935c505 chore(i18n): updated Norwegian translations (#1886) 2021-10-18 12:20:27 +05:30
liyasthomas
0e381ab850 feat: init loading states, minor ui improvements 2021-10-18 09:36:33 +05:30
liyasthomas
4a03ee4518 chore(deps): bump 2021-10-18 07:34:26 +05:30
Geert Wirken
be414d8279 Improve Dutch translations (#1880) 2021-10-17 22:43:18 +05:30
liyasthomas
c47b1f2413 chore(deps): bump 2021-10-17 19:42:53 +05:30
liyasthomas
ba9ee052a6 feat: minor ui improvements on invite flow 2021-10-17 18:53:24 +05:30
liyasthomas
b1c6708762 feat: init invitation wiring
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-10-17 16:45:48 +05:30
Andrew Bastin
0ba31b6c79 feat: initial team invites implementation
Co-authored-by: Liyas Thomas <hi@liyasthomas.com>
2021-10-17 15:05:43 +05:30
liyasthomas
14f402f186 feat: minor ui improvements to power search 2021-10-17 11:37:06 +05:30
liyasthomas
b811e97ea5 feat: set hoppscotch graphql echo server as default 2021-10-17 00:49:36 +05:30
Oscar Dominguez
6d167ce1d6 ci(workflow): add cache to workflows using actions/setup-node (#1872) 2021-10-16 22:50:16 +05:30
liyasthomas
9ac1d23fd9 chore: minor fixes and improvements 2021-10-16 20:54:19 +05:30
Andrew Bastin
dea3a34e3d feat: team invitation mutations 2021-10-16 15:14:07 +05:30
Andrew Bastin
39de34f083 refactor: add graphcache codegen and update types 2021-10-16 15:14:07 +05:30
liyasthomas
02d5f0fdf3 feat: support updating user's display name 2021-10-16 15:01:32 +05:30
liyasthomas
0bb7cbe8d9 feat: compact mode for team component 2021-10-16 11:14:33 +05:30
liyasthomas
3e3b88b8c2 feat: native email input validation on login modal 2021-10-16 00:00:33 +05:30
liyasthomas
1438beb93b chore: clean up 2021-10-15 08:15:43 +05:30
liyasthomas
c1ec5dc60d feat: promote environment aware input to stable 2021-10-14 13:00:32 +05:30
prasanth
3f513f2f4d fix(request): multipart payload not working while using proxy 2021-10-13 21:32:27 +05:30
liyasthomas
d1b573f6f9 fix: init local persistence 2021-10-13 19:42:13 +05:30
liyasthomas
0e08abc46f feat: init theming in empty layout 2021-10-13 17:00:56 +05:30
liyasthomas
49bdf9f203 feat: init teams invite page 2021-10-13 16:12:02 +05:30
liyasthomas
b3e9df4f3d fix: introduce X-Frame-Options header to prevent clickjacking 2021-10-13 08:58:40 +05:30
Andrew Bastin
3b8cf4a60a fix: single character shortcuts not working 2021-10-12 21:43:06 +05:30
Akash Hamirwasia
b7ccb9a34c feat: disable inputs in realtime and graphql connections when active (#1870) 2021-10-12 21:19:29 +05:30
Akash Hamirwasia
b8ffa872c7 feat: add socket.io-client version selector menu (#1867) 2021-10-12 11:22:47 +05:30
liyasthomas
ef866f7851 feat: invite modal mvp 2021-10-11 06:51:43 +05:30
liyasthomas
f27515bf1d Merge branch 'refactor/backend' of https://github.com/hoppscotch/hoppscotch into refactor/backend 2021-10-10 20:49:27 +05:30
liyasthomas
ddf74c5d7c chore(deps): bump 2021-10-10 20:48:16 +05:30
liyasthomas
8ea12695b3 feat: teams invite modal 2021-10-10 20:44:21 +05:30
Andrew Bastin
d6324e6ba6 feat: gql codegen + caching + optimistic 2021-10-10 20:44:18 +05:30
Andrew Bastin
2325982801 refactor: extract cache info to separate files and add more mutations 2021-10-10 20:43:19 +05:30
liyasthomas
b103c45e65 chore(ui): minor ui improvements, empty teams state 2021-10-10 20:43:18 +05:30
Andrew Bastin
409989eddb feat: probable user is displayed as authenticated 2021-10-10 20:43:18 +05:30
liyasthomas
3f5fcae280 refactor: updated lint script 2021-10-10 20:43:18 +05:30
Andrew Bastin
d5123c793a feat: add persistent cache and optimistic updates 2021-10-10 20:43:16 +05:30
Andrew Bastin
e6707c1e4a fix: queries not waiting for authentication 2021-10-10 20:42:46 +05:30
liyasthomas
41be5cc4a8 feat: teams modal wrapper 2021-10-10 20:41:57 +05:30
liyasthomas
e82a4a1d23 refactor: remove EAInvite flag 2021-10-10 20:41:57 +05:30
Andrew Bastin
e30e4edfce fix: fix issue with team queries not updating correctly after mutation 2021-10-10 20:41:57 +05:30
Andrew Bastin
bd72ef7950 feat: team mutation 2021-10-10 20:41:57 +05:30
Andrew Bastin
539034df2a refactor: refactor create team modal to new system 2021-10-10 20:41:57 +05:30
liyasthomas
6da6afc5a1 fix: i18n usage 2021-10-10 20:41:57 +05:30
liyasthomas
fea523972d feat: init profile page 2021-10-10 20:41:57 +05:30
Andrew Bastin
5cfc6c2949 feat: introduce updated mutation system and team.vue rewrite 2021-10-10 20:41:57 +05:30
Andrew Bastin
6dd0c25d49 feat: initial setup of new backend comms 2021-10-10 20:41:54 +05:30
liyasthomas
ab9b3e47b9 chore(deps): bump 2021-10-10 20:24:31 +05:30
James George
33ebdf2831 chore: cleanup .husky directory (#1865) 2021-10-10 15:00:57 +05:30
liyasthomas
ef95939763 feat: teams invite modal 2021-10-09 16:10:51 +05:30
Rishabh kalra
0394deaeef route navigation no longer happening in typable areas (#1862) 2021-10-09 06:45:17 +05:30
Andrew Bastin
337a60c8a4 feat: gql codegen + caching + optimistic 2021-10-08 23:51:06 +05:30
Andrew Bastin
47b341d50e fix: fix conditional tabs not rerendering 2021-10-06 21:55:05 +05:30
Andrew Bastin
a5fd39adf8 refactor: extract cache info to separate files and add more mutations 2021-10-06 19:32:27 +05:30
liyasthomas
5772117dc8 fix: respect localePath on router - fixed #1859 2021-10-06 12:38:50 +05:30
liyasthomas
f73c8a45d9 chore(ui): minor ui improvements, empty teams state 2021-10-05 22:17:29 +05:30
Andrew Bastin
109d4190ae feat: probable user is displayed as authenticated 2021-10-05 22:06:19 +05:30
liyasthomas
317de82be6 refactor: updated lint script 2021-10-05 21:48:10 +05:30
Boris K
3604d69463 chore(i18n): fixed french translations (#1858) 2021-10-05 15:16:36 +05:30
Andrew Bastin
96cf774652 feat: add persistent cache and optimistic updates 2021-10-05 02:09:39 +05:30
liyasthomas
d2b39976ba chore(deps): bump 2021-10-04 08:16:40 +05:30
Stephane
06161bc963 Show graphql error message (#1852)
Co-authored-by: StephaneBischoffSSENSE <stephane.bischoff@ssense.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2021-10-04 08:01:47 +05:30
bblumenwiese
2ab1d3dbfa docs: fix broken link to .env.example in README (#1854)
Refs #1852
2021-10-04 00:29:45 +05:30
Andrew Bastin
ecdc7919ae fix: queries not waiting for authentication 2021-10-03 19:14:32 +05:30
liyasthomas
a24541ac2b chore(deps): bump 2021-10-03 19:13:44 +05:30
liyasthomas
d832690548 refactor: better name for duplicated environment 2021-10-03 18:53:27 +05:30
Stephane
2844710ea8 feat: add ability to copy environments (#1848)
Co-authored-by: StephaneBischoffSSENSE <stephane.bischoff@ssense.com>
2021-10-03 18:38:36 +05:30
liyasthomas
84ad4071ad feat: teams modal wrapper 2021-10-03 18:14:52 +05:30
liyasthomas
7f501241f0 refactor: remove EAInvite flag 2021-10-03 15:49:04 +05:30
Manda Putra
5dcfa66c5d fix: parsing 2021-10-03 12:28:49 +05:30
Manda Putra
f428a21279 feat: parse body from url params 2021-10-03 12:28:49 +05:30
Andrew Bastin
ccdd4963cd fix: fix issue with team queries not updating correctly after mutation 2021-10-03 01:31:06 +05:30
Andrew Bastin
2092a3729c feat: team mutation 2021-10-02 13:50:08 +05:30
Andrew Bastin
a628420adb refactor: refactor create team modal to new system 2021-10-02 13:50:08 +05:30
liyasthomas
23de147ca1 fix: i18n usage 2021-10-02 13:46:51 +05:30
liyasthomas
93f55f5619 feat: init profile page 2021-10-02 13:22:07 +05:30
Liyas Thomas
b10933898f Merge pull request #1845 from YasiOnFire/fix-polish-localization
chore(i18n): fix polish translation
2021-10-02 07:34:24 +05:30
Yasio
1727b754d4 chore(i18n): fix polish translation 2021-10-01 20:36:12 +02:00
Andrew Bastin
ffb0c12c08 feat: introduce updated mutation system and team.vue rewrite 2021-10-01 15:13:48 +05:30
Andrew Bastin
c332808fe4 feat: initial setup of new backend comms 2021-10-01 13:46:22 +05:30
liyasthomas
8f0538c886 chore(i18n): updated translations 2021-10-01 11:24:34 +05:30
Khaleel Gibran
e6bb7e2ca9 Remove random </br>s in locale footers (#1844) 2021-10-01 10:12:10 +05:30
liyasthomas
2a012520d0 fix: proper type for ID 2021-09-30 16:05:40 +05:30
Andrew Bastin
c71333d9cb refactor: update ID based backend queries to proper types 2021-09-30 12:52:31 +05:30
liyasthomas
728515c225 chore: bump deps 2021-09-26 19:16:23 +05:30
Andrew Bastin
e0f88e01f9 Update js-sandbox README 2021-09-26 11:27:46 +05:30
Andrew Bastin
1aa94a12c0 Merge pull request #1836 from AndrewBastin/refactor/js-sandbox
Move to JS code execution sandbox (HOPP-67, HOPP-66)
2021-09-25 23:05:41 +05:30
Andrew Bastin
de2d3361a7 refactor: bump js-sandbox version to 1.0.0 2021-09-25 22:25:33 +05:30
Andrew Bastin
680937e50b feat: fix issue with the pre-request envs 2021-09-25 21:21:01 +05:30
liyasthomas
6751c50514 fix: build 2021-09-25 12:21:32 +05:30
liyasthomas
0c389701fe chore(docs): updated readme 2021-09-25 12:21:19 +05:30
Andrew Bastin
9454d8c100 feat: move testing code to js-sandbox 2021-09-25 01:09:48 +05:30
Andrew Bastin
166f9e817b feat: project level testing + add fp-ts and sandbox as deps 2021-09-25 01:07:24 +05:30
Andrew Bastin
d2865c637c refactor: bring js-sandbox project to the monorepo 2021-09-25 01:01:22 +05:30
liyasthomas
9698932bde refactor(ui): minor ui improvements 2021-09-20 21:33:36 +05:30
liyasthomas
44026fcd41 fix: show scrollbars inside menu 2021-09-20 14:56:25 +05:30
liyasthomas
d938af0c2c fix: better responsiveness on horizontal layout 2021-09-20 14:18:40 +05:30
liyasthomas
fd658400a6 refactor: updated logo 2021-09-20 11:15:09 +05:30
liyasthomas
dcbb17b164 feat: vertical and horizontal layout support 2021-09-20 10:17:31 +05:30
liyasthomas
49741875bd chore: minor ui improvements 2021-09-19 20:27:20 +05:30
liyasthomas
0fcd9733ff chore: bump deps 2021-09-19 20:26:25 +05:30
Andrew Bastin
62d50169d7 Merge pull request #1832 from AndrewBastin/refactor/teams-list
fix: teams list not properly showing (HOPP-61)
2021-09-19 00:48:30 +05:30
Andrew Bastin
1d3d5a1e6a Merge branch 'main' into refactor/teams-list 2021-09-19 00:47:58 +05:30
Liyas Thomas
d309fa745e Update packages/hoppscotch-app/components/teams/index.vue 2021-09-19 00:33:21 +05:30
liyasthomas
f7031992d5 fix: enforce name for SmartTab component 2021-09-19 00:09:35 +05:30
Andrew Bastin
fcdf68ea15 fix: teams list not properly showing 2021-09-18 23:56:19 +05:30
liyasthomas
b0a6692179 feat: vertical tabs for right sidebars 2021-09-18 23:50:42 +05:30
liyasthomas
e1e763575d perf: template literals 2021-09-18 16:09:58 +05:30
Andrew Bastin
4236d1179c fix: save request crashing on teams 2021-09-18 15:11:54 +05:30
Andrew Bastin
09365bcabe fix: environments not being written correctly 2021-09-18 14:39:43 +05:30
Andrew Bastin
be29ddcbd6 fix: volar shim gitignore issue 2021-09-17 18:49:38 +05:30
liyasthomas
522194ca8d chore: remove absolute files 2021-09-17 17:30:55 +05:30
liyasthomas
5af8f584f6 chore(deps): bump 2021-09-17 15:25:52 +05:30
liyasthomas
adc08f8865 perf(ci): remove absolute branch hooks 2021-09-17 15:20:30 +05:30
liyasthomas
0f39d54c3c fix: netlify builds 2021-09-17 14:17:48 +05:30
liyasthomas
9e6659e842 fix: netlify builds 2021-09-17 14:11:30 +05:30
liyasthomas
46a0f6e3f8 refactor: update build docs 2021-09-17 13:57:56 +05:30
Andrew Bastin
e90b26ebed fix: standardized build scripts 2021-09-17 13:46:23 +05:30
Andrew Bastin
4407f260ae fix: updated to better dockerfile 2021-09-17 13:12:49 +05:30
Andrew Bastin
d4392416c8 fix: fix broken tests 2021-09-16 22:59:29 +05:30
liyasthomas
2d3cbd26b8 fix: ci builds 2021-09-16 22:44:34 +05:30
Andrew Bastin
98b9660956 refactor: merge branch 'main' into refactor/monorepo 2021-09-16 22:24:21 +05:30
liyasthomas
4e8a4e8914 refactor: typescript support 2021-09-15 17:30:04 +05:30
liyasthomas
96bcbc80f8 fix(ci): bump deps + fix tests 2021-09-14 23:43:20 +05:30
liyasthomas
1dfc8e2973 perf: remove absolute files 2021-09-14 23:32:43 +05:30
liyasthomas
311886f6c9 Merge remote-tracking branch 'origin/feat/codemirror' 2021-09-14 23:30:04 +05:30
liyasthomas
4a332f40e5 fix: json outline line index 2021-09-14 23:28:39 +05:30
liyasthomas
93a97a2f4c refactor: json outline ui 2021-09-14 23:11:10 +05:30
Andrew Bastin
1dee098ca2 feat: fix outline 2021-09-14 21:33:13 +05:30
liyasthomas
a07cc7e560 refactor: minor ui improvements 2021-09-14 13:19:10 +05:30
Andrew Bastin
c26f7f5ebc fix: autocompletion eating non-identifier tokens 2021-09-13 09:18:37 +05:30
liyasthomas
5d801cf566 fix: missing '?' in query parameter string for code generators 2021-09-13 09:07:06 +05:30
Andrew Bastin
631b2d869e fix: autocompletion position messing up 2021-09-12 21:56:56 +05:30
liyasthomas
c02f54cc18 chore(deps): bump 2021-09-12 19:45:23 +05:30
liyasthomas
827a95515d feat: select suggestion on enter key for autocomplete 2021-09-12 10:51:46 +05:30
liyasthomas
9082152f1a fix: editor width 2021-09-12 02:08:37 +05:30
Liyas Thomas
0efbddeda4 Merge pull request #1822 from hoppscotch/fix/gql-save-req 2021-09-12 00:07:22 +05:30
liyasthomas
b2e186957c fix: repetitive use of () in setup script 2021-09-12 00:02:07 +05:30
Andrew Bastin
d855e5cffb fix: gql request save issue 2021-09-11 23:43:35 +05:30
Andrew Bastin
f3747edaa3 Merge pull request #1820 from hoppscotch/fix/gql-headers
fix: gql headers not passed properly (HOPP-55)
2021-09-11 21:15:17 +05:30
Andrew Bastin
752932ef3d fix: gql headers not passed properly 2021-09-11 21:01:48 +05:30
liyasthomas
948cf9dae3 perf: cleanup 2021-09-11 10:34:06 +05:30
liyasthomas
b2f93aa549 feat: highlight active line in codemirror when focused 2021-09-11 09:19:16 +05:30
liyasthomas
108f228edf refactor: minor ui improvements 2021-09-11 08:26:54 +05:30
liyasthomas
fe6030140f feat: graphql mode for schema editor 2021-09-10 19:57:52 +05:30
Andrew Bastin
003400cfa8 feat: introduce graphql mode for codemirror 2021-09-10 19:01:35 +05:30
liyasthomas
41a02f059d perf: remove ace editor 2021-09-10 18:52:58 +05:30
liyasthomas
b4ed6fd107 feat: codemirror for import curl and codegens 2021-09-10 18:27:31 +05:30
Andrew Bastin
36246da9e1 feat: fix up jest tests 2021-09-10 17:50:22 +05:30
liyasthomas
457b6b982c feat: codemirror for graphql query, scheme and response 2021-09-10 16:12:04 +05:30
liyasthomas
05a07dc4a1 perf: ci 2021-09-10 13:05:31 +05:30
Andrew Bastin
85889c2cb9 fix: fix tests.yml 2021-09-10 12:55:59 +05:30
liyasthomas
be6ceaab04 perf: ci 2021-09-10 12:46:50 +05:30
liyasthomas
f1b18688bb fix: ci 2021-09-10 12:15:08 +05:30
liyasthomas
80c7decb81 feat: husky + commitlint 2021-09-10 12:02:18 +05:30
liyasthomas
3ef5a1e21a feat: codemirror editor for pre-request and test scripts 2021-09-10 11:12:08 +05:30
liyasthomas
2eb0a4c754 feat: reactive syntax + line wrap on raw body 2021-09-10 10:46:58 +05:30
liyasthomas
10f5af5dda feat: codemirror editot for raw body 2021-09-10 01:35:46 +05:30
liyasthomas
8b27ebb96b feat: mixed html syntax, light theme for codemiror 2021-09-10 00:44:14 +05:30
Andrew Bastin
b28f82a881 refactor: monorepo+pnpm (removed husky) 2021-09-10 00:28:28 +05:30
liyasthomas
c921606f3f Merge remote-tracking branch 'origin/exp/volar-types' into feat/codemirror 2021-09-09 20:50:58 +05:30
liyasthomas
c6c08f6c60 feat: reactive line wrap on codemirror
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-09-09 20:50:04 +05:30
liyasthomas
02cf620090 feat: port ace editor to codemirror
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-09-09 17:47:27 +05:30
Andrew Bastin
4a12cc76fa feat: improve shim generation for index components 2021-09-09 01:56:43 +05:30
Andrew Bastin
f4f74e223f refactor: a volar types shim generator 2021-09-09 01:03:46 +05:30
liyasthomas
8b4535c131 Merge branch 'feat/codemirror' of https://github.com/hoppscotch/hoppscotch into feat/codemirror 2021-09-08 21:54:00 +05:30
liyasthomas
b15fd6c75a feat: port bulk editor textareas to codemirror 2021-09-08 21:52:26 +05:30
liyasthomas
e1a25fa894 fix: broken conditional rendering of codemirror
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-09-08 21:41:02 +05:30
Andrew Bastin
2bb3b71a70 refactor: map ctrl-space to autocomplete by default in codemirror 2021-09-08 21:41:02 +05:30
Andrew Bastin
4c55b9c304 fix: codemirror theme not changing when color mode is updated 2021-09-08 21:41:02 +05:30
Andrew Bastin
639a629809 feat: gql query autocompletion on codemirror 2021-09-08 21:41:02 +05:30
Andrew Bastin
d6e3bd09b4 refactor: pass current token position to auto completers on codemirror 2021-09-08 21:41:02 +05:30
Andrew Bastin
8d67a0d95f feat: test script auto completion for codemirror 2021-09-08 21:41:02 +05:30
Andrew Bastin
b9fc0175e7 feat: implement base autocomplete implementation for codemirror along with preRequest autocompletion 2021-09-08 21:41:02 +05:30
liyasthomas
dc5f52cc0d refactor: github flavored codemirror light theme 2021-09-08 21:40:55 +05:30
liyasthomas
602aabdeb8 feat: reactive codemirror theme 2021-09-08 21:40:11 +05:30
Andrew Bastin
2f8aa79ec1 feat: json linter support for codemirror 2021-09-08 21:40:11 +05:30
Andrew Bastin
8af90432cf feat: implement gql query linting in codemirror 2021-09-08 21:40:11 +05:30
Andrew Bastin
61da0733c2 feat: codemirror editor options are reactive 2021-09-08 21:40:11 +05:30
liyasthomas
33951482d5 feat: placeholder, auto-close brackets, search, line wrap 2021-09-08 21:40:11 +05:30
Andrew Bastin
4e8484ee7c feat: linter for prerequest and testscripts 2021-09-08 21:40:11 +05:30
Andrew Bastin
071761a61e feat: codemirror linting system 2021-09-08 21:40:11 +05:30
Andrew Bastin
10a11d6725 refactor: add types for esprima 2021-09-08 21:40:11 +05:30
Andrew Bastin
c81178ae26 refactor: extract common codemirror logic out to composable 2021-09-08 21:40:10 +05:30
liyasthomas
2bafae5397 feat: line wrap, auto close brackets, placeholder on codemirror 2021-09-08 21:40:10 +05:30
liyasthomas
6a1d201e0e refactor: ts codemirror 2021-09-08 21:40:10 +05:30
liyasthomas
8de544696d feat: init codemirror 2021-09-08 21:40:06 +05:30
liyasthomas
66c489da8f fix: broken conditional rendering of codemirror
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2021-09-08 20:27:36 +05:30
Andrew Bastin
26c8f35688 refactor: map ctrl-space to autocomplete by default in codemirror 2021-09-08 19:51:43 +05:30
Andrew Bastin
28aeac4533 fix: codemirror theme not changing when color mode is updated 2021-09-08 06:00:23 +05:30
Andrew Bastin
162b3d6192 feat: gql query autocompletion on codemirror 2021-09-08 05:38:03 +05:30
Andrew Bastin
b016d3fd9d refactor: pass current token position to auto completers on codemirror 2021-09-08 05:36:46 +05:30
Andrew Bastin
f64ff58dbc feat: test script auto completion for codemirror 2021-09-08 04:58:30 +05:30
Andrew Bastin
d4d3d96bbb feat: implement base autocomplete implementation for codemirror along with preRequest autocompletion 2021-09-07 22:15:47 +05:30
liyasthomas
a5197ee544 refactor: github flavored codemirror light theme 2021-09-07 16:26:26 +05:30
liyasthomas
8a5fd4f745 feat: reactive codemirror theme 2021-09-07 12:14:13 +05:30
Andrew Bastin
12cd7940c6 feat: json linter support for codemirror 2021-09-06 23:47:26 +05:30
Andrew Bastin
0c2cec46a7 feat: implement gql query linting in codemirror 2021-09-06 23:30:43 +05:30
Andrew Bastin
8430921e4e feat: codemirror editor options are reactive 2021-09-01 20:32:33 +05:30
liyasthomas
c938abf606 feat: placeholder, auto-close brackets, search, line wrap 2021-09-01 17:33:54 +05:30
Andrew Bastin
3addfe8d4b feat: linter for prerequest and testscripts 2021-09-01 16:45:49 +05:30
Andrew Bastin
5276556837 feat: codemirror linting system 2021-09-01 16:41:14 +05:30
Andrew Bastin
e47ad94666 refactor: add types for esprima 2021-09-01 16:34:02 +05:30
liyasthomas
7065763c7c Merge branch 'main' into feat/codemirror 2021-09-01 10:34:52 +05:30
Andrew Bastin
86489d95c2 refactor: extract common codemirror logic out to composable 2021-08-31 22:20:42 +05:30
liyasthomas
e2b1c83698 feat: line wrap, auto close brackets, placeholder on codemirror 2021-08-31 16:10:35 +05:30
liyasthomas
15373be63e refactor: ts codemirror 2021-08-31 10:47:00 +05:30
liyasthomas
8c9cd079b7 feat: init codemirror 2021-08-31 00:03:07 +05:30
595 changed files with 31694 additions and 70250 deletions

View File

@@ -2,13 +2,12 @@ name: Node.js CI
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
@@ -17,9 +16,13 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Install pnpm
run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
cache: pnpm
- name: Run tests
run: pnpm i && pnpm -r test

6
.gitignore vendored
View File

@@ -104,3 +104,9 @@ tests/*/screenshots
# Tests videos
tests/*/videos
# Local Netlify folder
.netlify
# Andrew's crazy Volar shim generator
shims-volar.d.ts

1
.husky/.gitignore vendored
View File

@@ -1 +0,0 @@
_

View File

@@ -1,4 +1,4 @@
FROM node:12-alpine
FROM node:lts-alpine
LABEL maintainer="Hoppscotch (support@hoppscotch.io)"
@@ -9,17 +9,19 @@ RUN apk add --update --no-cache \
# Create app directory
WORKDIR /app
COPY package*.json ./
RUN npm install
ADD . /app/
COPY . .
RUN npm install -g pnpm
RUN pnpm i --unsafe-perm=true
ENV HOST 0.0.0.0
EXPOSE 3000
RUN mv .env.example .env
RUN mv packages/hoppscotch-app/.env.example packages/hoppscotch-app/.env
CMD ["npm", "run", "dev"]
RUN pnpm run generate
CMD ["pnpm", "run", "start"]

View File

@@ -1,7 +1,7 @@
<div align="center">
<a href="https://hoppscotch.io">
<img
src="https://raw.githubusercontent.com/hoppscotch/hoppscotch/main/static/logo.png"
src="https://avatars.githubusercontent.com/u/56705483"
alt="Hoppscotch Logo"
height="64"
/>
@@ -89,7 +89,7 @@
- `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**
@@ -173,7 +173,7 @@ _Collections are synced with cloud / local session storage_
- Hide your IP address
- 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
- Access APIs served in non-HTTPS (`http://`) endpoints
- Use your own Proxy URL
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/privacy)**_
@@ -256,9 +256,12 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
👨‍👩‍👧‍👦 **Teams β:** Helps you collaborate across your team to design, develop, and test APIs faster.
- Unlimited team collections and shared requests
- Unlimited teams
- Unlimited shared collections
- Unlimited team members
- User roles
- Role-based access control
- Cloud sync
- Multiple devices
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
@@ -290,7 +293,7 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
## **Developing**
0. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/.env.example) file found in repository's root directory with your own keys and rename it to `.env`.
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)._
@@ -302,9 +305,10 @@ _Sample keys only works with the [production build](https://hoppscotch.io)._
### Local development environment
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
2. Install dependencies by running `npm install` within the directory that you cloned (probably `hoppscotch`).
3. Start the development server with `npm run dev`.
4. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
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.
### Docker compose
@@ -323,9 +327,10 @@ docker run --rm --name hoppscotch -p 3000:3000 hoppscotch/hoppscotch:latest
## **Releasing**
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
2. Install dependencies by running `npm install` within the directory that you cloned (probably `hoppscotch`).
3. Build the release files with `npm run generate`.
4. Find the built project in `./dist`.
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. Build the release files with `pnpm run generate`.
5. Find the built project in `packages/hoppscotch-app/dist`.
## **Contributing**

View File

@@ -10,10 +10,10 @@ if there is no existing translation, you can create a new one by following these
1. **[Fork the repository](https://github.com/hoppscotch/hoppscotch/fork).**
2. **Create a new branch for your translation.**
3. **Create target language file in the [`locales`](https://github.com/hoppscotch/hoppscotch/tree/main/locales) directory.**
4. **Copy the contents of the source file [`locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/locales/en.json) to the target language file.**
3. **Create target language file in the [`locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-app/locales) directory.**
4. **Copy the contents of the source file [`locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/locales/en.json) to the target language file.**
5. **Translate the strings in the target language file.**
6. **Add your language entry to [`languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/languages.json).**
6. **Add your language entry to [`languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/languages.json).**
7. **Save & commit changes.**
8. **Send a pull request.**

View File

@@ -1 +0,0 @@
<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" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>

Before

Width:  |  Height:  |  Size: 396 B

View File

@@ -1,166 +0,0 @@
<template>
<div>
<header
class="flex space-x-2 flex-1 py-2 px-2 items-center justify-between"
>
<div class="space-x-2 inline-flex items-center">
<ButtonSecondary
class="tracking-wide !font-bold !text-secondaryDark"
label="HOPPSCOTCH"
to="/"
/>
<AppGitHubStarButton class="mt-1.5 transition hidden sm:flex" />
</div>
<div class="space-x-2 inline-flex items-center">
<ButtonSecondary
id="installPWA"
v-tippy="{ theme: 'tooltip' }"
:title="$t('header.install_pwa')"
svg="download"
class="rounded"
@click.native="showInstallPrompt()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="`${$t('app.search')} <kbd>/</kbd>`"
svg="search"
class="rounded"
@click.native="showSearch = true"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="`${$t('support.title')} <kbd>?</kbd>`"
svg="life-buoy"
class="rounded"
@click.native="showSupport = true"
/>
<ButtonSecondary
v-if="currentUser === null"
svg="upload-cloud"
:label="$t('header.save_workspace')"
filled
class="hidden !font-semibold md:flex"
@click.native="showLogin = true"
/>
<ButtonPrimary
v-if="currentUser === null"
:label="$t('header.login')"
@click.native="showLogin = true"
/>
<span v-else class="px-2">
<tippy ref="user" interactive trigger="click" theme="popover" arrow>
<template #trigger>
<ProfilePicture
v-if="currentUser.photoURL"
v-tippy="{
theme: 'tooltip',
}"
:url="currentUser.photoURL"
:alt="currentUser.displayName"
:title="currentUser.displayName"
indicator
:indicator-styles="isOnLine ? 'bg-green-500' : 'bg-red-500'"
/>
<ButtonSecondary
v-else
v-tippy="{ theme: 'tooltip' }"
:title="$t('header.account')"
class="rounded"
svg="user"
/>
</template>
<SmartItem
to="/settings"
svg="settings"
:label="$t('navigation.settings')"
@click.native="$refs.user.tippy().hide()"
/>
<FirebaseLogout @confirm-logout="$refs.user.tippy().hide()" />
</tippy>
</span>
</div>
</header>
<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" />
</div>
</template>
<script>
import { defineComponent, ref } from "@nuxtjs/composition-api"
import intializePwa from "~/helpers/pwa"
import { currentUser$ } from "~/helpers/fb/auth"
import { getLocalConfig, setLocalConfig } from "~/newstore/localpersistence"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { defineActionHandler } from "~/helpers/actions"
export default defineComponent({
setup() {
const showSupport = ref(false)
const showSearch = ref(false)
defineActionHandler("modals.support.toggle", () => {
showSupport.value = !showSupport.value
})
defineActionHandler("modals.search.toggle", () => {
showSearch.value = !showSearch.value
})
return {
currentUser: useReadonlyStream(currentUser$, null),
showSupport,
showSearch,
}
},
data() {
return {
// Once the PWA code is initialized, this holds a method
// that can be called to show the user the installation
// prompt.
showInstallPrompt: null,
showLogin: false,
isOnLine: navigator.onLine,
}
},
async mounted() {
window.addEventListener("online", () => {
this.isOnLine = true
})
window.addEventListener("offline", () => {
this.isOnLine = false
})
// Initializes the PWA code - checks if the app is installed,
// etc.
this.showInstallPrompt = await intializePwa()
const cookiesAllowed = getLocalConfig("cookiesAllowed") === "yes"
if (!cookiesAllowed) {
this.$toast.show(this.$t("app.we_use_cookies").toString(), {
icon: "cookie",
duration: 0,
action: [
{
text: this.$t("action.learn_more").toString(),
onClick: (_, toastObject) => {
setLocalConfig("cookiesAllowed", "yes")
toastObject.goAway(0)
window
.open("https://docs.hoppscotch.io/privacy", "_blank")
.focus()
},
},
{
text: this.$t("action.dismiss").toString(),
onClick: (_, toastObject) => {
setLocalConfig("cookiesAllowed", "yes")
toastObject.goAway(0)
},
},
],
})
}
},
})
</script>

View File

@@ -1,243 +0,0 @@
<template>
<SmartModal v-if="show" :title="$t('collection.save_as')" @close="hideModal">
<template #body>
<div class="flex flex-col px-2">
<div class="flex relative">
<input
id="selectLabelSaveReq"
v-model="requestName"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveRequestAs"
/>
<label for="selectLabelSaveReq">
{{ $t("request.name") }}
</label>
</div>
<label class="px-4 pt-4 pb-4">
{{ $t("collection.select_location") }}
</label>
<CollectionsGraphql
v-if="mode === 'graphql'"
:doc="false"
:show-coll-actions="false"
:picked="picked"
:saving-mode="true"
@select="onSelect"
/>
<Collections
v-else
:picked="picked"
:save-request="true"
@select="onSelect"
@update-collection="collectionsType.type = $event"
@update-coll-type="onUpdateCollType"
/>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary
:label="$t('action.save')"
@click.native="saveRequestAs"
/>
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import * as teamUtils from "~/helpers/teams/utils"
import {
saveRESTRequestAs,
editRESTRequest,
editGraphqlRequest,
saveGraphqlRequestAs,
} from "~/newstore/collections"
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
import {
getRESTRequest,
useRESTRequestName,
setRESTSaveContext,
} from "~/newstore/RESTSession"
export default defineComponent({
props: {
// mode can be either "graphql" or "rest"
mode: { type: String, default: "rest" },
show: Boolean,
},
setup(props) {
return {
requestName:
props.mode === "rest" ? useRESTRequestName() : useGQLRequestName(),
}
},
data() {
return {
requestData: {
name: this.requestName,
collectionIndex: undefined,
folderName: undefined,
requestIndex: undefined,
},
collectionsType: {
type: "my-collections",
selectedTeam: undefined,
},
picked: null,
}
},
watch: {
"requestData.collectionIndex": function resetFolderAndRequestIndex() {
// if user has chosen some folder, than selected other collection, which doesn't have any folders
// than `requestUpdateData.folderName` won't be reseted
this.$data.requestData.folderName = undefined
this.$data.requestData.requestIndex = undefined
},
"requestData.folderName": function resetRequestIndex() {
this.$data.requestData.requestIndex = undefined
},
editingRequest({ name }) {
this.$data.requestData.name = name || this.$data.defaultRequestName
},
},
methods: {
onUpdateCollType(newCollType) {
this.collectionsType = newCollType
},
onSelect({ picked }) {
this.picked = picked
},
async saveRequestAs() {
if (!this.requestName) {
this.$toast.error(this.$t("error.empty_req_name"), {
icon: "error_outline",
})
return
}
if (this.picked == null) {
this.$toast.error(this.$t("collection.select"), {
icon: "error_outline",
})
return
}
const requestUpdated =
this.mode === "rest" ? getRESTRequest() : getGQLSession()
// Filter out all REST file inputs
if (this.mode === "rest" && requestUpdated.bodyParams) {
requestUpdated.bodyParams = requestUpdated.bodyParams.map((param) =>
param?.value?.[0] instanceof File ? { ...param, value: "" } : param
)
}
if (this.picked.pickedType === "my-request") {
editRESTRequest(
this.picked.folderPath,
this.picked.requestIndex,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: this.picked.folderPath,
requestIndex: this.picked.requestIndex,
})
} else if (this.picked.pickedType === "my-folder") {
const insertionIndex = saveRESTRequestAs(
this.picked.folderPath,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: this.picked.folderPath,
requestIndex: insertionIndex,
})
} else if (this.picked.pickedType === "my-collection") {
const insertionIndex = saveRESTRequestAs(
`${this.picked.collectionIndex}`,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: `${this.picked.collectionIndex}`,
requestIndex: insertionIndex,
})
} else if (this.picked.pickedType === "teams-request") {
teamUtils.overwriteRequestTeams(
this.$apollo,
JSON.stringify(requestUpdated),
requestUpdated.name,
this.picked.requestID
)
setRESTSaveContext({
originLocation: "team-collection",
requestID: this.picked.requestID,
})
} else if (this.picked.pickedType === "teams-folder") {
const req = await teamUtils.saveRequestAsTeams(
this.$apollo,
JSON.stringify(requestUpdated),
requestUpdated.name,
this.collectionsType.selectedTeam.id,
this.picked.folderID
)
if (req && req.id) {
setRESTSaveContext({
originLocation: "team-collection",
requestID: req.id,
teamID: this.collectionsType.selectedTeam.id,
collectionID: this.picked.folderID,
})
}
} else if (this.picked.pickedType === "teams-collection") {
const req = await teamUtils.saveRequestAsTeams(
this.$apollo,
JSON.stringify(requestUpdated),
requestUpdated.name,
this.collectionsType.selectedTeam.id,
this.picked.collectionID
)
if (req && req.id) {
setRESTSaveContext({
originLocation: "team-collection",
requestID: req.id,
teamID: this.collectionsType.selectedTeam.id,
collectionID: this.picked.collectionID,
})
}
} else if (this.picked.pickedType === "gql-my-request") {
editGraphqlRequest(
this.picked.folderPath,
this.picked.requestIndex,
requestUpdated
)
} else if (this.picked.pickedType === "gql-my-folder") {
saveGraphqlRequestAs(this.picked.folderPath, requestUpdated)
} else if (this.picked.pickedType === "gql-my-collection") {
saveGraphqlRequestAs(`${this.picked.collectionIndex}`, requestUpdated)
}
this.$toast.success(this.$t("request.added"), {
icon: "post_add",
})
this.hideModal()
},
hideModal() {
this.picked = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -1,254 +0,0 @@
<template>
<div class="opacity-0 show-if-initialized" :class="{ initialized }">
<pre ref="editor" :class="styles"></pre>
</div>
</template>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import "ace-builds/src-noconflict/ext-language_tools"
import "ace-builds/src-noconflict/mode-graphqlschema"
import * as gql from "graphql"
import { getAutocompleteSuggestions } from "graphql-language-service-interface"
import { defineComponent } from "@nuxtjs/composition-api"
import { defineGQLLanguageMode } from "~/helpers/syntax/gqlQueryLangMode"
import debounce from "~/helpers/utils/debounce"
export default defineComponent({
props: {
value: {
type: String,
default: "",
},
theme: {
type: String,
required: false,
default: null,
},
onRunGQLQuery: {
type: Function,
default: () => {},
},
options: {
type: Object,
default: () => {},
},
styles: {
type: String,
default: "",
},
},
data() {
return {
initialized: false,
editor: null,
cacheValue: "",
validationSchema: null,
}
},
computed: {
appFontSize() {
return getComputedStyle(document.documentElement).getPropertyValue(
"--body-font-size"
)
},
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1)
this.cacheValue = value
}
},
theme() {
this.initialized = false
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
},
options(value) {
this.editor.setOptions(value)
},
},
mounted() {
defineGQLLanguageMode(ace)
const langTools = ace.require("ace/ext/language_tools")
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/gql-query`,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
...this.options,
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
editor.setFontSize(this.appFontSize)
const completer = {
getCompletions: (
editor,
_session,
{ row, column },
_prefix,
callback
) => {
if (this.validationSchema) {
const completions = getAutocompleteSuggestions(
this.validationSchema,
editor.getValue(),
{
line: row,
character: column,
}
)
callback(
null,
completions.map(({ label, detail }) => ({
name: label,
value: label,
score: 1.0,
meta: detail,
}))
)
} else {
callback(null, [])
}
},
}
langTools.setCompleters([completer])
if (this.value) editor.setValue(this.value, 1)
this.editor = editor
this.cacheValue = this.value
editor.commands.addCommand({
name: "runGQLQuery",
exec: () => this.onRunGQLQuery(this.editor.getValue()),
bindKey: {
mac: "cmd-enter",
win: "ctrl-enter",
},
})
editor.commands.addCommand({
name: "prettifyGQLQuery",
exec: () => this.prettifyQuery(),
bindKey: {
mac: "cmd-p",
win: "ctrl-p",
},
})
editor.on("change", () => {
const content = editor.getValue()
this.$emit("input", content)
this.parseContents(content)
this.cacheValue = content
})
this.parseContents(this.value)
},
beforeDestroy() {
this.editor.destroy()
},
methods: {
prettifyQuery() {
try {
this.$emit("update-query", gql.print(gql.parse(this.editor.getValue())))
} catch (e) {
this.$toast.error(this.$t("error.gql_prettify_invalid_query"), {
icon: "error_outline",
})
}
},
defineTheme() {
if (this.theme) {
return this.theme
}
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-theme")
)
},
setValidationSchema(schema) {
this.validationSchema = schema
this.parseContents(this.cacheValue)
},
parseContents: debounce(function (content) {
if (content !== "") {
try {
const doc = gql.parse(content)
if (this.validationSchema) {
this.editor.session.setAnnotations(
gql
.validate(this.validationSchema, doc)
.map(({ locations, message }) => ({
row: locations[0].line - 1,
column: locations[0].column - 1,
text: message,
type: "error",
}))
)
}
} catch (e) {
this.editor.session.setAnnotations([
{
row: e.locations[0].line - 1,
column: e.locations[0].column - 1,
text: e.message,
type: "error",
},
])
}
} else {
this.editor.session.setAnnotations([])
}
}, 2000),
},
})
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>

View File

@@ -1,188 +0,0 @@
<template>
<AppSection ref="response" label="response">
<div
v-if="responseString"
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("response.title") }}
</label>
<div class="flex">
<ButtonSecondary
ref="downloadResponse"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
:svg="downloadResponseIcon"
@click.native="downloadResponse"
/>
<ButtonSecondary
ref="copyResponseButton"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.copy')"
:svg="copyResponseIcon"
@click.native="copyResponse"
/>
</div>
</div>
<SmartAceEditor
v-if="responseString"
:value="responseString"
:lang="'json'"
:lint="false"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<div
v-else
class="
flex flex-col flex-1
text-secondaryLight
p-4
items-center
justify-center
"
>
<div class="flex space-x-2 pb-4">
<div class="flex flex-col space-y-4 items-end">
<span class="flex flex-1 items-center">
{{ $t("shortcut.request.send_request") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.show_all") }}
</span>
<!-- <span class="flex flex-1 items-center">
{{ $t("shortcut.general.command_menu") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.help_menu") }}
</span> -->
</div>
<div class="flex flex-col space-y-4">
<div class="flex">
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">G</span>
</div>
<div class="flex">
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">K</span>
</div>
<!-- <div class="flex">
<span class="shortcut-key">/</span>
</div>
<div class="flex">
<span class="shortcut-key">?</span>
</div> -->
</div>
</div>
<ButtonSecondary
:label="$t('app.documentation')"
to="https://docs.hoppscotch.io"
svg="external-link"
blank
outline
reverse
/>
</div>
</AppSection>
</template>
<script lang="ts">
import {
defineComponent,
PropType,
ref,
useContext,
} from "@nuxtjs/composition-api"
import { GQLConnection } from "~/helpers/GQLConnection"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { gqlResponse$ } from "~/newstore/GQLSession"
export default defineComponent({
props: {
conn: {
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup() {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const responseString = useReadonlyStream(gqlResponse$, "")
const downloadResponseIcon = ref("download")
const copyResponseIcon = ref("copy")
const copyResponse = () => {
copyToClipboard(responseString.value!)
copyResponseIcon.value = "check"
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
}
const downloadResponse = () => {
const dataToWrite = responseString.value
const file = new Blob([dataToWrite!], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadResponseIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadResponseIcon.value = "download"
}, 1000)
}
return {
responseString,
downloadResponseIcon,
copyResponseIcon,
downloadResponse,
copyResponse,
getSpecialKey: getPlatformSpecialKey,
}
},
})
</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,470 +0,0 @@
<template>
<aside>
<SmartTabs styles="sticky bg-primary z-10 top-0">
<SmartTab :id="'docs'" :label="`Docs`" :selected="true">
<AppSection label="docs">
<div class="bg-primary flex top-sidebarPrimaryStickyFold 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-dividerLight bg-primary sticky z-10 top-sidebarSecondaryStickyFold"
>
<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
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
"
>
<i class="opacity-75 pb-2 material-icons">link</i>
<span class="text-center">
{{ $t("empty.schema") }}
</span>
</div>
</AppSection>
</SmartTab>
<SmartTab :id="'history'" :label="$t('tab.history')">
<History
ref="graphqlHistoryComponent"
:page="'graphql'"
@useHistory="handleUseHistory"
/>
</SmartTab>
<SmartTab :id="'collections'" :label="$t('tab.collections')">
<CollectionsGraphql />
</SmartTab>
<SmartTab :id="'schema'" :label="`Schema`">
<AppSection ref="schema" label="schema">
<div
v-if="schemaString"
class="
bg-primary
flex flex-1
top-sidebarPrimaryStickyFold
pl-4
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' }"
to="https://docs.hoppscotch.io/quickstart/graphql"
blank
:title="$t('app.wiki')"
svg="help-circle"
/>
<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>
<SmartAceEditor
v-if="schemaString"
v-model="schemaString"
:lang="'graphqlschema'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<div
v-else
class="
flex flex-col
text-secondaryLight
p-4
items-center
justify-center
"
>
<i class="opacity-75 pb-2 material-icons">link</i>
<span class="text-center">
{{ $t("empty.schema") }}
</span>
</div>
</AppSection>
</SmartTab>
</SmartTabs>
</aside>
</template>
<script lang="ts">
import {
computed,
defineComponent,
nextTick,
PropType,
ref,
useContext,
} from "@nuxtjs/composition-api"
import { GraphQLField, GraphQLType } from "graphql"
import { map } from "rxjs/operators"
import { GQLConnection } from "~/helpers/GQLConnection"
import { GQLHeader } from "~/helpers/types/HoppGQLRequest"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream } from "~/helpers/utils/composables"
import {
setGQLHeaders,
setGQLQuery,
setGQLResponse,
setGQLURL,
setGQLVariables,
} from "~/newstore/GQLSession"
function isTextFoundInGraphqlFieldObject(
text: string,
field: GraphQLField<any, any>
) {
const normalizedText = text.toLowerCase()
const isFilterTextFoundInDescription = field.description
? field.description.toLowerCase().includes(normalizedText)
: false
const isFilterTextFoundInName = field.name
.toLowerCase()
.includes(normalizedText)
return isFilterTextFoundInDescription || isFilterTextFoundInName
}
function getFilteredGraphqlFields(
filterText: string,
fields: GraphQLField<any, any>[]
) {
if (!filterText) return fields
return fields.filter((field) =>
isTextFoundInGraphqlFieldObject(filterText, field)
)
}
function getFilteredGraphqlTypes(filterText: string, types: GraphQLType[]) {
if (!filterText) return types
return types.filter((type) => {
const isFilterTextMatching = isTextFoundInGraphqlFieldObject(
filterText,
type as any
)
if (isFilterTextMatching) {
return true
}
const isFilterTextMatchingAtLeastOneField = Object.values(
(type as any)._fields || {}
).some((field) => isTextFoundInGraphqlFieldObject(filterText, field as any))
return isFilterTextMatchingAtLeastOneField
})
}
function resolveRootType(type: GraphQLType) {
let t: any = type
while (t.ofType) t = t.ofType
return t
}
type GQLHistoryEntry = {
url: string
headers: GQLHeader[]
query: string
response: string
variables: string
}
export default defineComponent({
props: {
conn: {
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup(props) {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const queryFields = useReadonlyStream(
props.conn.queryFields$.pipe(map((x) => x ?? [])),
[]
)
const mutationFields = useReadonlyStream(
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
[]
)
const subscriptionFields = useReadonlyStream(
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
[]
)
const graphqlTypes = useReadonlyStream(
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
[]
)
const downloadSchemaIcon = ref("download")
const copySchemaIcon = ref("copy")
const graphqlFieldsFilterText = ref("")
const gqlTabs = ref<any | null>(null)
const typesTab = ref<any | null>(null)
const filteredQueryFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
queryFields.value as any
)
})
const filteredMutationFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
mutationFields.value as any
)
})
const filteredSubscriptionFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
subscriptionFields.value as any
)
})
const filteredGraphqlTypes = computed(() => {
return getFilteredGraphqlTypes(
graphqlFieldsFilterText.value,
graphqlTypes.value as any
)
})
const isGqlTypeHighlighted = (gqlType: GraphQLType) => {
if (!graphqlFieldsFilterText.value) return false
return isTextFoundInGraphqlFieldObject(
graphqlFieldsFilterText.value,
gqlType as any
)
}
const getGqlTypeHighlightedFields = (gqlType: GraphQLType) => {
if (!graphqlFieldsFilterText.value) return []
const fields = Object.values((gqlType as any)._fields || {})
if (!fields || fields.length === 0) return []
return fields.filter((field) =>
isTextFoundInGraphqlFieldObject(
graphqlFieldsFilterText.value,
field as any
)
)
}
const handleJumpToType = async (type: GraphQLType) => {
gqlTabs.value.selectTab(typesTab.value)
await nextTick()
const rootTypeName = resolveRootType(type).name
const target = document.getElementById(`type_${rootTypeName}`)
if (target) {
gqlTabs.value.$el
.querySelector(".gqlTabs")
.scrollTo({ top: target.offsetTop, behavior: "smooth" })
}
}
const schemaString = useReadonlyStream(
props.conn.schemaString$.pipe(map((x) => x ?? "")),
""
)
const downloadSchema = () => {
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
const file = new Blob([dataToWrite], { type: "application/graphql" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${
url.split("/").pop()!.split("#")[0].split("?")[0]
}.graphql`
document.body.appendChild(a)
a.click()
downloadSchemaIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadSchemaIcon.value = "download"
}, 1000)
}
const copySchema = () => {
if (!schemaString.value) return
copyToClipboard(schemaString.value)
copySchemaIcon.value = "check"
setTimeout(() => (copySchemaIcon.value = "copy"), 1000)
}
const handleUseHistory = (entry: GQLHistoryEntry) => {
const url = entry.url
const headers = entry.headers
const gqlQueryString = entry.query
const variableString = entry.variables
const responseText = entry.response
setGQLURL(url)
setGQLHeaders(headers)
setGQLQuery(gqlQueryString)
setGQLVariables(variableString)
setGQLResponse(responseText)
props.conn.reset()
}
return {
queryFields,
mutationFields,
subscriptionFields,
graphqlTypes,
schemaString,
graphqlFieldsFilterText,
filteredQueryFields,
filteredMutationFields,
filteredSubscriptionFields,
filteredGraphqlTypes,
isGqlTypeHighlighted,
getGqlTypeHighlightedFields,
gqlTabs,
typesTab,
handleJumpToType,
downloadSchema,
downloadSchemaIcon,
copySchemaIcon,
copySchema,
handleUseHistory,
}
},
})
</script>

View File

@@ -1,137 +0,0 @@
<template>
<SmartModal v-if="show" :title="$t('import.curl')" @close="hideModal">
<template #body>
<div class="flex flex-col px-2">
<textarea-autosize
id="import-curl"
v-model="curl"
class="font-mono textarea floating-input"
autofocus
rows="8"
placeholder=" "
/>
<label for="import-curl">
{{ $t("request.enter_curl") }}
</label>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary
:label="$t('import.title')"
@click.native="handleImport"
/>
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import parseCurlCommand from "~/helpers/curlparser"
import {
HoppRESTHeader,
HoppRESTParam,
makeRESTRequest,
} from "~/helpers/types/HoppRESTRequest"
import { setRESTRequest } from "~/newstore/RESTSession"
export default defineComponent({
props: {
show: Boolean,
},
emits: ["hide-modal"],
data() {
return {
curl: "",
}
},
methods: {
hideModal() {
this.$emit("hide-modal")
},
handleImport() {
const text = this.curl
try {
const parsedCurl = parseCurlCommand(text)
const { origin, pathname } = new URL(
parsedCurl.url.replace(/"/g, "").replace(/'/g, "")
)
const endpoint = origin + pathname
const headers: HoppRESTHeader[] = []
const params: HoppRESTParam[] = []
if (parsedCurl.query) {
for (const key of Object.keys(parsedCurl.query)) {
const val = parsedCurl.query[key]!
if (Array.isArray(val)) {
val.forEach((value) => {
params.push({
key,
value,
active: true,
})
})
} else {
params.push({
key,
value: val!,
active: true,
})
}
}
}
if (parsedCurl.headers) {
for (const key of Object.keys(parsedCurl.headers)) {
headers.push({
key,
value: parsedCurl.headers[key],
active: true,
})
}
}
const method = parsedCurl.method.toUpperCase()
// let rawInput = false
// let rawParams: any | null = null
// if (parsedCurl.data) {
// rawInput = true
// rawParams = parsedCurl.data
// }
this.showCurlImportModal = false
setRESTRequest(
makeRESTRequest({
name: "Untitled request",
endpoint,
method,
params,
headers,
preRequestScript: "",
testScript: "",
auth: {
authType: "none",
authActive: true,
},
body: {
contentType: "application/json",
body: "",
},
})
)
} catch (e) {
console.error(e)
this.$toast.error(this.$t("error.curl_invalid_format").toString(), {
icon: "error_outline",
})
}
this.hideModal()
},
},
})
</script>

View File

@@ -1,136 +0,0 @@
<template>
<div>
<div
class="
bg-primary
border-b border-dividerLight
flex flex-1
top-upperTertiaryStickyFold
pl-4
z-10
sticky
items-center
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("request.raw_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')"
svg="trash-2"
@click.native="clearContent('rawParams', $event)"
/>
<ButtonSecondary
v-if="contentType && contentType.endsWith('json')"
ref="prettifyRequest"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.prettify')"
:svg="prettifyIcon"
@click.native="prettifyRequestBody"
/>
<label for="payload">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('import.json')"
svg="file-plus"
@click.native="$refs.payload.click()"
/>
</label>
<input
ref="payload"
class="input"
name="payload"
type="file"
@change="uploadPayload"
/>
</div>
</div>
<div class="relative">
<SmartAceEditor
v-model="rawParamsBody"
:lang="rawInputEditorLang"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import { getEditorLangForMimeType } from "~/helpers/editorutils"
import { pluckRef } from "~/helpers/utils/composables"
import { useRESTRequestBody } from "~/newstore/RESTSession"
export default defineComponent({
props: {
contentType: {
type: String,
required: true,
},
},
setup() {
return {
rawParamsBody: pluckRef(useRESTRequestBody(), "body"),
prettifyIcon: "align-left",
}
},
computed: {
rawInputEditorLang() {
return getEditorLangForMimeType(this.contentType)
},
},
methods: {
clearContent() {
this.rawParamsBody = ""
},
uploadPayload() {
const file = this.$refs.payload.files[0]
if (file !== undefined && file !== null) {
const reader = new FileReader()
reader.onload = ({ target }) => {
this.rawParamsBody = target.result
}
reader.readAsText(file)
this.$toast.success(this.$t("state.file_imported"), {
icon: "attach_file",
})
} else {
this.$toast.error(this.$t("action.choose_file"), {
icon: "attach_file",
})
}
this.$refs.payload.value = ""
},
prettifyRequestBody() {
try {
const jsonObj = JSON.parse(this.rawParamsBody)
this.rawParamsBody = JSON.stringify(jsonObj, null, 2)
this.prettifyIcon = "check"
setTimeout(() => (this.prettifyIcon = "align-left"), 1000)
} catch (e) {
console.error(e)
this.$toast.error(`${this.$t("error.json_prettify_invalid_body")}`, {
icon: "error_outline",
})
}
},
},
})
</script>

View File

@@ -1,153 +0,0 @@
<template>
<div>
<div
class="
bg-primary
border-b border-dividerLight
flex flex-1
top-lowerSecondaryStickyFold
pl-4
z-10
sticky
items-center
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="
previewEnabled ? $t('hide.preview') : $t('response.preview_html')
"
:svg="!previewEnabled ? 'eye' : 'eye-off'"
@click.native.prevent="togglePreview"
/>
<ButtonSecondary
v-if="response.body"
ref="downloadResponse"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
:svg="downloadIcon"
@click.native="downloadResponse"
/>
<ButtonSecondary
v-if="response.body"
ref="copyResponse"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.copy')"
:svg="copyIcon"
@click.native="copyResponse"
/>
</div>
</div>
<div class="relative">
<SmartAceEditor
:value="responseBodyText"
:lang="'html'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<iframe
ref="previewFrame"
:class="{ hidden: !previewEnabled }"
class="covers-response"
src="about:blank"
></iframe>
</div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
import { copyToClipboard } from "~/helpers/utils/clipboard"
export default defineComponent({
mixins: [TextContentRendererMixin],
props: {
response: { type: Object, default: () => {} },
},
data() {
return {
downloadIcon: "download",
copyIcon: "copy",
previewEnabled: false,
}
},
methods: {
downloadResponse() {
const dataToWrite = this.responseBodyText
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()
this.downloadIcon = "check"
this.$toast.success(this.$t("state.download_started"), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
this.downloadIcon = "download"
}, 1000)
},
copyResponse() {
copyToClipboard(this.responseBodyText)
this.copyIcon = "check"
this.$toast.success(this.$t("state.copied_to_clipboard"), {
icon: "content_paste",
})
setTimeout(() => (this.copyIcon = "copy"), 1000)
},
togglePreview() {
this.previewEnabled = !this.previewEnabled
if (this.previewEnabled) {
if (
this.$refs.previewFrame.getAttribute("data-previewing-url") ===
this.url
)
return
// Use DOMParser to parse document HTML.
const previewDocument = new DOMParser().parseFromString(
this.responseBodyText,
"text/html"
)
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
previewDocument.head.innerHTML =
`<base href="${this.url}">` + previewDocument.head.innerHTML
// Finally, set the iframe source to the resulting HTML.
this.$refs.previewFrame.srcdoc =
previewDocument.documentElement.outerHTML
this.$refs.previewFrame.setAttribute("data-previewing-url", this.url)
}
},
},
})
</script>
<style lang="scss" scoped>
.covers-response {
@apply absolute;
@apply inset-0;
@apply bg-white;
@apply h-full;
@apply w-full;
@apply border;
@apply border-dividerLight;
}
</style>

View File

@@ -1,123 +0,0 @@
<template>
<div>
<div
class="
bg-primary
border-b border-dividerLight
flex flex-1
top-lowerSecondaryStickyFold
pl-4
z-10
sticky
items-center
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
ref="downloadResponse"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
:svg="downloadIcon"
@click.native="downloadResponse"
/>
<ButtonSecondary
v-if="response.body"
ref="copyResponse"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.copy')"
:svg="copyIcon"
@click.native="copyResponse"
/>
</div>
</div>
<div class="relative">
<SmartAceEditor
:value="jsonBodyText"
:lang="'json'"
:provide-outline="true"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
import { copyToClipboard } from "~/helpers/utils/clipboard"
export default defineComponent({
mixins: [TextContentRendererMixin],
props: {
response: { type: Object, default: () => {} },
},
data() {
return {
downloadIcon: "download",
copyIcon: "copy",
}
},
computed: {
jsonBodyText() {
try {
return JSON.stringify(JSON.parse(this.responseBodyText), null, 2)
} catch (e) {
// Most probs invalid JSON was returned, so drop prettification (should we warn ?)
return this.responseBodyText
}
},
responseType() {
return (
this.response.headers.find(
(h) => h.key.toLowerCase() === "content-type"
).value || ""
)
.split(";")[0]
.toLowerCase()
},
},
methods: {
downloadResponse() {
const dataToWrite = this.responseBodyText
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()
this.downloadIcon = "check"
this.$toast.success(this.$t("state.download_started"), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
this.downloadIcon = "download"
}, 1000)
},
copyResponse() {
copyToClipboard(this.responseBodyText)
this.copyIcon = "check"
this.$toast.success(this.$t("state.copied_to_clipboard"), {
icon: "content_paste",
})
setTimeout(() => (this.copyIcon = "copy"), 1000)
},
},
})
</script>

View File

@@ -1,114 +0,0 @@
<template>
<div>
<div
class="
bg-primary
border-b border-dividerLight
flex flex-1
top-lowerSecondaryStickyFold
pl-4
z-10
sticky
items-center
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
ref="downloadResponse"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
:svg="downloadIcon"
@click.native="downloadResponse"
/>
<ButtonSecondary
v-if="response.body"
ref="copyResponse"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.copy')"
:svg="copyIcon"
@click.native="copyResponse"
/>
</div>
</div>
<div class="relative">
<SmartAceEditor
:value="responseBodyText"
:lang="'plain_text'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
import { copyToClipboard } from "~/helpers/utils/clipboard"
export default defineComponent({
mixins: [TextContentRendererMixin],
props: {
response: { type: Object, default: () => {} },
},
data() {
return {
downloadIcon: "download",
copyIcon: "copy",
}
},
computed: {
responseType() {
return (
this.response.headers.find(
(h) => h.key.toLowerCase() === "content-type"
).value || ""
)
.split(";")[0]
.toLowerCase()
},
},
methods: {
downloadResponse() {
const dataToWrite = this.responseBodyText
const file = new Blob([dataToWrite], { type: this.responseType })
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()
this.downloadIcon = "check"
this.$toast.success(this.$t("state.download_started"), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
this.downloadIcon = "download"
}, 1000)
},
copyResponse() {
copyToClipboard(this.responseBodyText)
this.copyIcon = "check"
this.$toast.success(this.$t("state.copied_to_clipboard"), {
icon: "content_paste",
})
setTimeout(() => (this.copyIcon = "copy"), 1000)
},
},
})
</script>

View File

@@ -1,114 +0,0 @@
<template>
<div>
<div
class="
bg-primary
border-b border-dividerLight
flex flex-1
top-lowerSecondaryStickyFold
pl-4
z-10
sticky
items-center
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
ref="downloadResponse"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.download_file')"
:svg="downloadIcon"
@click.native="downloadResponse"
/>
<ButtonSecondary
v-if="response.body"
ref="copyResponse"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.copy')"
:svg="copyIcon"
@click.native="copyResponse"
/>
</div>
</div>
<div class="relative">
<SmartAceEditor
:value="responseBodyText"
:lang="'xml'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
import { copyToClipboard } from "~/helpers/utils/clipboard"
export default defineComponent({
mixins: [TextContentRendererMixin],
props: {
response: { type: Object, default: () => {} },
},
data() {
return {
copyIcon: "copy",
downloadIcon: "download",
}
},
computed: {
responseType() {
return (
this.response.headers.find(
(h) => h.key.toLowerCase() === "content-type"
).value || ""
)
.split(";")[0]
.toLowerCase()
},
},
methods: {
downloadResponse() {
const dataToWrite = this.responseBodyText
const file = new Blob([dataToWrite], { type: this.responseType })
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()
this.downloadIcon = "check"
this.$toast.success(this.$t("state.download_started"), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
this.downloadIcon = "download"
}, 1000)
},
copyResponse() {
copyToClipboard(this.responseBodyText)
this.copyIcon = "check"
this.$toast.success(this.$t("state.copied_to_clipboard"), {
icon: "content_paste",
})
setTimeout(() => (this.copyIcon = "copy"), 1000)
},
},
})
</script>

View File

@@ -1,282 +0,0 @@
<template>
<div class="show-if-initialized" :class="{ initialized }">
<pre ref="editor" :class="styles"></pre>
<div
v-if="provideOutline"
class="
bg-primaryLight
border-t border-divider
flex flex-nowrap flex-1
py-1
px-4
bottom-0
z-10
sticky
overflow-auto
hide-scrollbar
"
>
<div
v-for="(p, index) in currentPath"
:key="`p-${index}`"
class="
cursor-pointer
flex-grow-0 flex-shrink-0
text-secondaryLight
inline-flex
items-center
hover:text-secondary
"
>
<span @click="onBlockClick(index)">
{{ p }}
</span>
<i v-if="index + 1 !== currentPath.length" class="mx-2 material-icons">
chevron_right
</i>
<tippy
v-if="siblingDropDownIndex == index"
ref="options"
interactive
trigger="click"
theme="popover"
arrow
>
<SmartItem
v-for="(sibling, siblingIndex) in currentSibling"
:key="`p-${index}-sibling-${siblingIndex}`"
:label="sibling.key ? sibling.key.value : i"
@click.native="goToSibling(sibling)"
/>
</tippy>
</div>
</div>
</div>
</template>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import { defineComponent } from "@nuxtjs/composition-api"
import jsonParse from "~/helpers/jsonParse"
import debounce from "~/helpers/utils/debounce"
import outline from "~/helpers/outline"
export default defineComponent({
props: {
provideOutline: {
type: Boolean,
default: false,
required: false,
},
value: {
type: String,
default: "",
},
theme: {
type: String,
required: false,
default: null,
},
lang: {
type: String,
default: "json",
},
lint: {
type: Boolean,
default: true,
required: false,
},
options: {
type: Object,
default: () => {},
},
styles: {
type: String,
default: "",
},
},
data() {
return {
initialized: false,
editor: null,
cacheValue: "",
outline: outline(),
currentPath: [],
currentSibling: [],
siblingDropDownIndex: null,
}
},
computed: {
appFontSize() {
return getComputedStyle(document.documentElement).getPropertyValue(
"--body-font-size"
)
},
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1)
this.cacheValue = value
if (this.lint) this.provideLinting(value)
}
},
theme() {
this.initialized = false
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
},
lang(value) {
this.editor.getSession().setMode(`ace/mode/${value}`)
},
options(value) {
this.editor.setOptions(value)
},
},
mounted() {
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/${this.lang}`,
...this.options,
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
editor.setFontSize(this.appFontSize)
if (this.value) editor.setValue(this.value, 1)
this.editor = editor
this.cacheValue = this.value
if (this.lang === "json" && this.provideOutline)
this.initOutline(this.value)
editor.on("change", () => {
const content = editor.getValue()
this.$emit("input", content)
this.cacheValue = content
if (this.provideOutline) debounce(this.initOutline(content), 500)
if (this.lint) this.provideLinting(content)
})
if (this.lang === "json" && this.provideOutline) {
editor.session.selection.on("changeCursor", () => {
const index = editor.session.doc.positionToIndex(
editor.selection.getCursor(),
0
)
const path = this.outline.genPath(index)
if (path.success) {
this.currentPath = path.res
}
})
}
// Disable linting, if lint prop is false
if (this.lint) this.provideLinting(this.value)
},
destroyed() {
this.editor.destroy()
},
methods: {
defineTheme() {
if (this.theme) {
return this.theme
}
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-theme")
)
},
provideLinting: debounce(function (code) {
if (this.lang === "json") {
try {
jsonParse(code)
this.editor.session.setAnnotations([])
} catch (e) {
const pos = this.editor.session
.getDocument()
.indexToPosition(e.start, 0)
this.editor.session.setAnnotations([
{
row: pos.row,
column: pos.column,
text: e.message,
type: "error",
},
])
}
}
}, 2000),
onBlockClick(index) {
if (this.siblingDropDownIndex === index) {
this.clearSiblingList()
} else {
this.currentSibling = this.outline.getSiblings(index)
if (this.currentSibling.length) this.siblingDropDownIndex = index
}
},
clearSiblingList() {
this.currentSibling = []
this.siblingDropDownIndex = null
},
goToSibling(obj) {
this.clearSiblingList()
if (obj.start) {
const pos = this.editor.session.doc.indexToPosition(obj.start, 0)
if (pos) {
this.editor.session.selection.moveCursorTo(pos.row, pos.column, true)
this.editor.session.selection.clearSelection()
this.editor.scrollToLine(pos.row, false, true, null)
}
}
},
initOutline: debounce(function (content) {
if (this.lang === "json") {
try {
this.outline.init(content)
if (content[0] === "[") this.currentPath.push("[]")
else this.currentPath.push("{}")
} catch (e) {
console.log("Outline error: ", e)
}
}
}),
},
})
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>

View File

@@ -1,292 +0,0 @@
<template>
<div class="show-if-initialized" :class="{ initialized }">
<pre ref="editor" :class="styles"></pre>
</div>
</template>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import "ace-builds/src-noconflict/ext-language_tools"
import "ace-builds/src-noconflict/mode-graphqlschema"
import * as esprima from "esprima"
import { defineComponent } from "@nuxtjs/composition-api"
import debounce from "~/helpers/utils/debounce"
import {
getPreRequestScriptCompletions,
getTestScriptCompletions,
performPreRequestLinting,
performTestLinting,
} from "~/helpers/tern"
export default defineComponent({
props: {
value: {
type: String,
default: "",
},
theme: {
type: String,
required: false,
default: null,
},
options: {
type: Object,
default: () => {},
},
styles: {
type: String,
default: "",
},
completeMode: {
type: String,
required: true,
default: "none",
},
},
data() {
return {
initialized: false,
editor: null,
cacheValue: "",
}
},
computed: {
appFontSize() {
return getComputedStyle(document.documentElement).getPropertyValue(
"--body-font-size"
)
},
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1)
this.cacheValue = value
if (this.lint) this.provideLinting(value)
}
},
theme() {
this.initialized = false
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick()
.then(() => {
this.initialized = true
})
.catch(() => {
// nextTick shouldn't really ever throw but still
this.initialized = true
})
})
},
options(value) {
this.editor.setOptions(value)
},
},
mounted() {
// const langTools = ace.require("ace/ext/language_tools")
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/javascript`,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
...this.options,
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick()
.then(() => {
this.initialized = true
})
.catch(() => {
// nextTIck shouldn't really ever throw but still
this.initialized = true
})
})
editor.setFontSize(this.appFontSize)
const completer = {
getCompletions: (
editor,
_session,
{ row, column },
_prefix,
callback
) => {
if (this.completeMode === "pre") {
getPreRequestScriptCompletions(editor.getValue(), row, column)
.then((res) => {
callback(
null,
res.completions.map((r, index, arr) => ({
name: r.name,
value: r.name,
score: (arr.length - index) / arr.length,
meta: r.type,
}))
)
})
.catch(() => callback(null, []))
} else if (this.completeMode === "test") {
getTestScriptCompletions(editor.getValue(), row, column)
.then((res) => {
callback(
null,
res.completions.map((r, index, arr) => ({
name: r.name,
value: r.name,
score: (arr.length - index) / arr.length,
meta: r.type,
}))
)
})
.catch(() => callback(null, []))
}
},
}
editor.completers = [completer]
if (this.value) editor.setValue(this.value, 1)
this.editor = editor
this.cacheValue = this.value
editor.on("change", () => {
const content = editor.getValue()
this.$emit("input", content)
this.cacheValue = content
this.provideLinting(content)
})
this.provideLinting(this.value)
},
destroyed() {
this.editor.destroy()
},
methods: {
defineTheme() {
if (this.theme) {
return this.theme
}
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-theme")
)
},
provideLinting: debounce(function (code) {
let results = []
const lintFunc =
this.completeMode === "pre"
? performPreRequestLinting
: performTestLinting
lintFunc(code)
.then((semanticLints) => {
results = results.concat(
semanticLints.map((lint) => ({
row: lint.from.line,
column: lint.from.ch,
text: `[semantic] ${lint.message}`,
type: "error",
}))
)
try {
const res = esprima.parseScript(code, { tolerant: true })
if (res.errors && res.errors.length > 0) {
results = results.concat(
res.errors.map((err) => {
const pos = this.editor.session
.getDocument()
.indexToPosition(err.index, 0)
return {
row: pos.row,
column: pos.column,
text: `[syntax] ${err.description}`,
type: "error",
}
})
)
}
} catch (e) {
const pos = this.editor.session
.getDocument()
.indexToPosition(e.index, 0)
results = results.concat([
{
row: pos.row,
column: pos.column,
text: `[syntax] ${e.description}`,
type: "error",
},
])
}
this.editor.session.setAnnotations(results)
})
.catch(() => {
try {
const res = esprima.parseScript(code, { tolerant: true })
if (res.errors && res.errors.length > 0) {
results = results.concat(
res.errors.map((err) => {
const pos = this.editor.session
.getDocument()
.indexToPosition(err.index, 0)
return {
row: pos.row,
column: pos.column,
text: `[syntax] ${err.description}`,
type: "error",
}
})
)
}
} catch (e) {
const pos = this.editor.session
.getDocument()
.indexToPosition(e.index, 0)
results = results.concat([
{
row: pos.row,
column: pos.column,
text: `[syntax] ${e.description}`,
type: "error",
},
])
}
this.editor.session.setAnnotations(results)
})
}, 2000),
},
})
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>

View File

@@ -1,22 +0,0 @@
<template>
<svg
class="h-4 animate-spin w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</template>

View File

@@ -1,82 +0,0 @@
<template>
<SmartModal v-if="show" :title="$t('team.new')" @close="hideModal">
<template #body>
<div class="flex flex-col px-2">
<input
id="selectLabelTeamAdd"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addNewTeam"
/>
<label for="selectLabelTeamAdd">
{{ $t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary :label="$t('action.save')" @click.native="addNewTeam" />
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import * as teamUtils from "~/helpers/teams/utils"
export default defineComponent({
props: {
show: Boolean,
},
data() {
return {
name: null,
}
},
methods: {
addNewTeam() {
// We save the user input in case of an error
const name = this.name
// We clear it early to give the UI a snappy feel
this.name = ""
if (!name) {
this.$toast.error(this.$t("empty.team_name"), {
icon: "error_outline",
})
return
}
if (name !== null && name.replace(/\s/g, "").length < 6) {
this.$toast.error(this.$t("team.name_length_insufficient"), {
icon: "error_outline",
})
return
}
// Call to the graphql mutation
teamUtils
.createTeam(this.$apollo, name)
.then(() => {
this.hideModal()
})
.catch((e) => {
console.error(e)
// We restore the initial user input
this.name = name
})
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -1,388 +0,0 @@
<template>
<SmartModal v-if="show" :title="$t('team.edit')" @close="hideModal">
<template #body>
<div class="flex flex-col px-2">
<div class="flex relative">
<input
id="selectLabelTeamEdit"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveTeam"
/>
<label for="selectLabelTeamEdit">
{{ $t("action.label") }}
</label>
</div>
<div class="flex flex-1 justify-between items-center">
<label for="memberList" class="p-4">
{{ $t("team.members") }}
</label>
<div class="flex">
<ButtonSecondary
svg="plus"
:label="$t('add.new')"
@click.native="addTeamMember"
/>
</div>
</div>
<div class="divide-y divide-dividerLight border-divider border rounded">
<div
v-for="(member, index) in members"
:key="`member-${index}`"
class="divide-x divide-dividerLight flex"
>
<input
class="bg-transparent flex flex-1 py-2 px-4"
:placeholder="$t('team.email')"
:name="'param' + index"
:value="member.user.email"
readonly
/>
<span>
<tippy
:ref="`memberOptions-${index}`"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<span class="select-wrapper">
<input
class="
bg-transparent
cursor-pointer
flex flex-1
py-2
px-4
"
:placeholder="$t('team.permissions')"
:name="'value' + index"
:value="
typeof member.role === 'string'
? member.role
: JSON.stringify(member.role)
"
readonly
/>
</span>
</template>
<SmartItem
label="OWNER"
@click.native="updateMemberRole(index, 'OWNER')"
/>
<SmartItem
label="EDITOR"
@click.native="updateMemberRole(index, 'EDITOR')"
/>
<SmartItem
label="VIEWER"
@click.native="updateMemberRole(index, 'VIEWER')"
/>
</tippy>
</span>
<div class="flex">
<ButtonSecondary
id="member"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.remove')"
svg="trash"
color="red"
@click.native="removeExistingTeamMember(member.user.uid)"
/>
</div>
</div>
<div
v-for="(member, index) in newMembers"
:key="`new-member-${index}`"
class="divide-x divide-dividerLight flex"
>
<input
v-model="member.key"
class="bg-transparent flex flex-1 py-2 px-4"
:placeholder="$t('team.email')"
:name="'member' + index"
autofocus
/>
<span>
<tippy
:ref="`newMemberOptions-${index}`"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<span class="select-wrapper">
<input
class="
bg-transparent
cursor-pointer
flex flex-1
py-2
px-4
"
:placeholder="$t('team.permissions')"
:name="'value' + index"
:value="
typeof member.value === 'string'
? member.value
: JSON.stringify(member.value)
"
readonly
/>
</span>
</template>
<SmartItem
label="OWNER"
@click.native="updateNewMemberRole(index, 'OWNER')"
/>
<SmartItem
label="EDITOR"
@click.native="updateNewMemberRole(index, 'EDITOR')"
/>
<SmartItem
label="VIEWER"
@click.native="updateNewMemberRole(index, 'VIEWER')"
/>
</tippy>
</span>
<div class="flex">
<ButtonSecondary
id="member"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.remove')"
svg="trash"
color="red"
@click.native="removeTeamMember(index)"
/>
</div>
</div>
<div
v-if="members.length === 0 && newMembers.length === 0"
class="
flex flex-col
text-secondaryLight
p-4
items-center
justify-center
"
>
<SmartIcon class="opacity-75 pb-2" name="users" />
<span class="text-center pb-4">
{{ $t("empty.members") }}
</span>
<ButtonSecondary
:label="$t('add.new')"
filled
@click.native="addTeamMember"
/>
</div>
</div>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary :label="$t('action.save')" @click.native="saveTeam" />
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script>
import cloneDeep from "lodash/cloneDeep"
import { defineComponent } from "@nuxtjs/composition-api"
import * as teamUtils from "~/helpers/teams/utils"
import TeamMemberAdapter from "~/helpers/teams/TeamMemberAdapter"
export default defineComponent({
props: {
show: Boolean,
editingTeam: { type: Object, default: () => {} },
editingteamID: { type: String, default: null },
},
data() {
return {
rename: null,
members: [],
newMembers: [],
membersAdapter: new TeamMemberAdapter(null),
}
},
computed: {
editingTeamCopy() {
return this.editingTeam
},
name: {
get() {
return this.editingTeam.name
},
set(name) {
this.rename = name
},
},
},
watch: {
editingteamID(teamID) {
this.membersAdapter.changeTeamID(teamID)
},
},
mounted() {
this.membersAdapter.members$.subscribe((list) => {
this.members = cloneDeep(list)
})
},
methods: {
updateMemberRole(id, role) {
this.members[id].role = role
this.$refs[`memberOptions-${id}`][0].tippy().hide()
},
updateNewMemberRole(id, role) {
this.newMembers[id].value = role
this.$refs[`newMemberOptions-${id}`][0].tippy().hide()
},
addTeamMember() {
const member = { key: "", value: "" }
this.newMembers.push(member)
},
removeExistingTeamMember(userID) {
teamUtils
.removeTeamMember(this.$apollo, userID, this.editingteamID)
.then(() => {
this.$toast.success(this.$t("team.member_removed"), {
icon: "done",
})
this.hideModal()
})
.catch((e) => {
this.$toast.error(this.$t("error.something_went_wrong"), {
icon: "error_outline",
})
console.error(e)
})
},
removeTeamMember(index) {
this.newMembers.splice(index, 1)
},
validateEmail(emailID) {
if (
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(
emailID
)
) {
return true
}
return false
},
saveTeam() {
if (
this.$data.rename !== null &&
this.$data.rename.replace(/\s/g, "").length < 6
) {
this.$toast.error(this.$t("team.name_length_insufficient"), {
icon: "error_outline",
})
return
}
let invalidEmail = false
this.$data.newMembers.forEach((element) => {
if (!this.validateEmail(element.key)) {
this.$toast.error(this.$t("team.invalid_email_format"), {
icon: "error_outline",
})
invalidEmail = true
}
})
if (invalidEmail) return
let invalidPermission = false
this.$data.newMembers.forEach((element) => {
if (!element.value) {
this.$toast.error(this.$t("invalid_member_permission"), {
icon: "error_outline",
})
invalidPermission = true
}
})
if (invalidPermission) return
this.$data.newMembers.forEach((element) => {
// Call to the graphql mutation
teamUtils
.addTeamMemberByEmail(
this.$apollo,
element.value,
element.key,
this.editingteamID
)
.then(() => {
this.$toast.success(this.$t("team.saved"), {
icon: "done",
})
})
.catch((e) => {
this.$toast.error(e, {
icon: "error_outline",
})
console.error(e)
})
})
this.members.forEach((element) => {
teamUtils
.updateTeamMemberRole(
this.$apollo,
element.user.uid,
element.role,
this.editingteamID
)
.then(() => {
this.$toast.success(this.$t("team.member_role_updated"), {
icon: "done",
})
})
.catch((e) => {
this.$toast.error(e, {
icon: "error_outline",
})
console.error(e)
})
})
if (this.$data.rename !== null) {
const newName =
this.name === this.$data.rename ? this.name : this.$data.rename
if (!/\S/.test(newName))
return this.$toast.error(this.$t("empty.team_name"), {
icon: "error_outline",
})
// Call to the graphql mutation
if (this.name !== this.rename)
teamUtils
.renameTeam(this.$apollo, newName, this.editingteamID)
.then(() => {
this.$toast.success(this.$t("team.saved"), {
icon: "done",
})
})
.catch((e) => {
this.$toast.error(this.$t("error.something_went_wrong"), {
icon: "error_outline",
})
console.error(e)
})
}
this.hideModal()
},
hideModal() {
this.rename = null
this.newMembers = []
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -1,111 +0,0 @@
<template>
<div class="border border-dividerLight rounded flex flex-1 items-end">
<div class="flex flex-1 items-start">
<div class="p-4">
<label
class="cursor-pointer transition hover:text-secondaryDark"
@click="team.myRole === 'OWNER' ? $emit('edit-team') : ''"
>
{{ team.name || $t("state.nothing_found") }}
</label>
<div class="flex -space-x-1 mt-2 overflow-hidden">
<img
v-for="(member, index) in team.members"
:key="`member-${index}`"
v-tippy="{ theme: 'tooltip' }"
:title="member.user.displayName"
:src="member.user.photoURL"
:alt="member.user.displayName"
class="rounded-full h-5 ring-primary ring-2 w-5 inline-block"
/>
</div>
</div>
</div>
<span>
<tippy ref="options" interactive trigger="click" theme="popover" arrow>
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.more')"
svg="more-vertical"
/>
</template>
<SmartItem
v-if="team.myRole === 'OWNER'"
svg="edit"
:label="$t('action.edit')"
@click.native="
$emit('edit-team')
$refs.options.tippy().hide()
"
/>
<SmartItem
v-if="team.myRole === 'OWNER'"
svg="trash-2"
color="red"
:label="$t('action.delete')"
@click.native="
deleteTeam()
$refs.options.tippy().hide()
"
/>
<SmartItem
v-if="!(team.myRole === 'OWNER' && team.ownersCount == 1)"
svg="trash"
:label="$t('team.exit')"
@click.native="
exitTeam()
$refs.options.tippy().hide()
"
/>
</tippy>
</span>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import * as teamUtils from "~/helpers/teams/utils"
export default defineComponent({
props: {
team: { type: Object, default: () => {} },
teamID: { type: String, default: null },
},
methods: {
deleteTeam() {
if (!confirm(this.$t("confirm.remove_team"))) return
// Call to the graphql mutation
teamUtils
.deleteTeam(this.$apollo, this.teamID)
.then(() => {
this.$toast.success(this.$t("team.deleted"), {
icon: "done",
})
})
.catch((e) => {
this.$toast.error(this.$t("error.something_went_wrong"), {
icon: "error_outline",
})
console.error(e)
})
},
exitTeam() {
if (!confirm("Are you sure you want to exit this team?")) return
teamUtils
.exitTeam(this.$apollo, this.teamID)
.then(() => {
this.$toast.success(this.$t("team.left"), {
icon: "done",
})
})
.catch((e) => {
this.$toast.error(this.$t("error.something_went_wrong"), {
icon: "error_outline",
})
console.error(e)
})
},
},
})
</script>

View File

@@ -1,128 +0,0 @@
<template>
<AppSection label="teams">
<h4 class="text-secondaryDark">
{{ $t("team.title") }}
</h4>
<div class="mt-1 text-secondaryLight">
<SmartAnchor
:label="$t('team.join_beta')"
to="https://hoppscotch.io/beta"
blank
class="link"
/>
</div>
<div class="space-y-4 mt-4">
<ButtonSecondary
:label="$t('team.create_new')"
outline
@click.native="displayModalAdd(true)"
/>
<p v-if="$apollo.queries.myTeams.loading">
{{ $t("state.loading") }}
</p>
<div v-if="myTeams.length === 0" class="flex items-center">
<i class="mr-4 material-icons">help_outline</i>
{{ $t("empty.teams") }}
</div>
<div v-else class="grid gap-4 sm:grid-cols-2 md:grid-cols-3">
<TeamsTeam
v-for="(team, index) in myTeams"
:key="`team-${index}`"
:team-i-d="team.id"
:team="team"
@edit-team="editTeam(team, team.id)"
/>
</div>
</div>
<TeamsAdd :show="showModalAdd" @hide-modal="displayModalAdd(false)" />
<TeamsEdit
:team="myTeams[0]"
:show="showModalEdit"
:editing-team="editingTeam"
:editingteam-i-d="editingteamID"
@hide-modal="displayModalEdit(false)"
/>
</AppSection>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import gql from "graphql-tag"
import { currentUser$ } from "~/helpers/fb/auth"
import { useReadonlyStream } from "~/helpers/utils/composables"
export default defineComponent({
setup() {
return {
currentUser: useReadonlyStream(currentUser$, null),
}
},
data() {
return {
showModalAdd: false,
showModalEdit: false,
editingTeam: {},
editingteamID: "",
me: {},
myTeams: [],
}
},
apollo: {
me: {
query: gql`
query GetMe {
me {
uid
eaInvited
}
}
`,
pollInterval: 100000,
},
myTeams: {
query: gql`
query GetMyTeams {
myTeams {
id
name
myRole
ownersCount
members {
user {
photoURL
displayName
email
uid
}
role
}
}
}
`,
pollInterval: 10000,
},
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener)
},
methods: {
displayModalAdd(shouldDisplay) {
this.showModalAdd = shouldDisplay
},
displayModalEdit(shouldDisplay) {
this.showModalEdit = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
editTeam(team, teamID) {
this.editingTeam = team
this.editingteamID = teamID
this.displayModalEdit(true)
},
resetSelectedData() {
this.$data.editingTeam = undefined
this.$data.editingteamID = undefined
},
},
})
</script>

View File

@@ -4,8 +4,10 @@
"indexes": "firestore.indexes.json"
},
"hosting": {
"predeploy": ["mv .env.example .env && npm ci && npm run generate"],
"public": "dist",
"predeploy": [
"cd packages/hoppscotch-app && mv .env.example .env && cd ../.. && npm install -g pnpm && pnpm i && pnpm run generate"
],
"public": "packages/hoppscotch-app/dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{

View File

@@ -1,155 +0,0 @@
import { Observable } from "rxjs"
import { filter } from "rxjs/operators"
import getEnvironmentVariablesFromScript from "./preRequest"
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
import { HoppRESTResponse } from "./types/HoppRESTResponse"
import { createRESTNetworkRequestStream } from "./network"
import runTestScriptWithVariables, {
transformResponseForTesting,
} from "./postwomanTesting"
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession"
/**
* Runs a REST network request along with all the
* other side processes (like running test scripts)
*/
export function runRESTRequest$(): Observable<HoppRESTResponse> {
const envs = getEnvironmentVariablesFromScript(
getRESTRequest().preRequestScript
)
const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
name: "Env",
variables: Object.keys(envs).map((key) => {
return {
key,
value: envs[key],
}
}),
})
const stream = createRESTNetworkRequestStream(effectiveRequest)
// Run Test Script when request ran successfully
const subscription = stream
.pipe(filter((res) => res.type === "success"))
.subscribe((res) => {
const testReport: {
report: "" // ¯\_(ツ)_/¯
testResults: Array<
| {
result: "FAIL"
message: string
styles: { icon: "close"; class: "cl-error-response" }
}
| {
result: "PASS"
message: string
styles: { icon: "check"; class: "success-response" }
}
| { startBlock: string; styles: { icon: ""; class: "" } }
| { endBlock: true; styles: { icon: ""; class: "" } }
>
errors: [] // ¯\_(ツ)_/¯
} = runTestScriptWithVariables(effectiveRequest.testScript, {
response: transformResponseForTesting(res),
}) as any
setRESTTestResults(translateToNewTestResults(testReport))
subscription.unsubscribe()
})
return stream
}
function isTestPass(x: any): x is {
result: "PASS"
styles: { icon: "check"; class: "success-response" }
} {
return x.result !== undefined && x.result === "PASS"
}
function isTestFail(x: any): x is {
result: "FAIL"
message: string
styles: { icon: "close"; class: "cl-error-response" }
} {
return x.result !== undefined && x.result === "FAIL"
}
function isStartBlock(
x: any
): x is { startBlock: string; styles: { icon: ""; class: "" } } {
return x.startBlock !== undefined
}
function isEndBlock(
x: any
): x is { endBlock: true; styles: { icon: ""; class: "" } } {
return x.endBlock !== undefined
}
function translateToNewTestResults(testReport: {
report: "" // ¯\_(ツ)_/¯
testResults: Array<
| {
result: "FAIL"
message: string
styles: { icon: "close"; class: "cl-error-response" }
}
| {
result: "PASS"
message: string
styles: { icon: "check"; class: "success-response" }
}
| { startBlock: string; styles: { icon: ""; class: "" } }
| { endBlock: true; styles: { icon: ""; class: "" } }
>
errors: [] // ¯\_(ツ)_/¯
}): HoppTestResult {
// Build a stack of test data which we eventually build up based on the results
const testsStack: HoppTestData[] = [
{
description: "root",
tests: [],
expectResults: [],
},
]
testReport.testResults.forEach((result) => {
// This is a test block start, push an empty test to the stack
if (isStartBlock(result)) {
testsStack.push({
description: result.startBlock,
tests: [],
expectResults: [],
})
} else if (isEndBlock(result)) {
// End of the block, pop the stack and add it as a child to the current stack top
const testData = testsStack.pop()!
testsStack[testsStack.length - 1].tests.push(testData)
} else if (isTestPass(result)) {
// A normal PASS expectation
testsStack[testsStack.length - 1].expectResults.push({
status: "pass",
message: result.message,
})
} else if (isTestFail(result)) {
// A normal FAIL expectation
testsStack[testsStack.length - 1].expectResults.push({
status: "fail",
message: result.message,
})
}
})
// We should end up with only the root stack entry
if (testsStack.length !== 1) throw new Error("Invalid test result structure")
return {
expectResults: testsStack[0].expectResults,
tests: testsStack[0].tests,
}
}

View File

@@ -1,12 +0,0 @@
const mimeToMode = {
"text/plain": "plain_text",
"text/html": "html",
"application/xml": "xml",
"application/hal+json": "json",
"application/vnd.api+json": "json",
"application/json": "json",
}
export function getEditorLangForMimeType(mimeType) {
return mimeToMode[mimeType] || "plain_text"
}

View File

@@ -1,28 +0,0 @@
import jsonLens from "./jsonLens"
import rawLens from "./rawLens"
import imageLens from "./imageLens"
import htmlLens from "./htmlLens"
import xmlLens from "./xmlLens"
export const lenses = [jsonLens, imageLens, htmlLens, xmlLens, rawLens]
export function getSuitableLenses(response) {
const contentType = response.headers.find((h) => h.key === "content-type")
if (!contentType) return [rawLens]
const result = []
for (const lens of lenses) {
if (lens.isSupportedContentType(contentType.value)) result.push(lens)
}
return result
}
export function getLensRenderers() {
const response = {}
for (const lens of lenses) {
response[lens.renderer] = lens.rendererImport
}
return response
}

View File

@@ -1,8 +0,0 @@
const rawLens = {
lensName: "response.raw",
isSupportedContentType: () => true,
renderer: "raw",
rendererImport: () => import("~/components/lenses/renderers/RawLensRenderer"),
}
export default rawLens

View File

@@ -1,124 +0,0 @@
import jsonParse from "./jsonParse"
export default () => {
let jsonAST = {}
let path = []
const init = (jsonStr) => {
jsonAST = jsonParse(jsonStr)
linkParents(jsonAST)
}
const setNewText = (jsonStr) => {
init(jsonStr)
path = []
}
const linkParents = (node) => {
if (node.kind === "Object") {
if (node.members) {
node.members.forEach((m) => {
m.parent = node
linkParents(m)
})
}
} else if (node.kind === "Array") {
if (node.values) {
node.values.forEach((v) => {
v.parent = node
linkParents(v)
})
}
} else if (node.kind === "Member") {
if (node.value) {
node.value.parent = node
linkParents(node.value)
}
}
}
const genPath = (index) => {
let output = {}
path = []
let current = jsonAST
if (current.kind === "Object") {
path.push({ label: "{}", obj: "root" })
} else if (current.kind === "Array") {
path.push({ label: "[]", obj: "root" })
}
let over = false
try {
while (!over) {
if (current.kind === "Object") {
let i = 0
let found = false
while (i < current.members.length) {
const m = current.members[i]
if (m.start <= index && m.end >= index) {
path.push({ label: m.key.value, obj: m })
current = current.members[i]
found = true
break
}
i++
}
if (!found) over = true
} else if (current.kind === "Array") {
if (current.values) {
let i = 0
let found = false
while (i < current.values.length) {
const m = current.values[i]
if (m.start <= index && m.end >= index) {
path.push({ label: `[${i.toString()}]`, obj: m })
current = current.values[i]
found = true
break
}
i++
}
if (!found) over = true
} else over = true
} else if (current.kind === "Member") {
if (current.value) {
if (current.value.start <= index && current.value.end >= index) {
current = current.value
} else over = true
} else over = true
} else if (
current.kind === "String" ||
current.kind === "Number" ||
current.kind === "Boolean" ||
current.kind === "Null"
) {
if (current.start <= index && current.end >= index) {
path.push({ label: `${current.value}`, obj: current })
}
over = true
}
}
output = { success: true, res: path.map((p) => p.label) }
} catch (e) {
output = { success: false, res: e }
}
return output
}
const getSiblings = (index) => {
const parent = path[index]?.obj?.parent
if (!parent) return []
else if (parent.kind === "Object") {
return parent.members
} else if (parent.kind === "Array") {
return parent.values
} else return []
}
return {
init,
genPath,
getSiblings,
setNewText,
}
}

View File

@@ -1,321 +0,0 @@
import { HoppRESTResponse } from "./types/HoppRESTResponse"
const styles = {
PASS: { icon: "check", class: "success-response" },
FAIL: { icon: "close", class: "cl-error-response" },
ERROR: { icon: "close", class: "cl-error-response" },
}
type TestScriptResponse = {
body: any
headers: any[]
status: number
__newRes: HoppRESTResponse
}
type TestScriptVariables = {
response: TestScriptResponse
}
type TestReportStartBlock = {
startBlock: string
}
type TestReportEndBlock = {
endBlock: true
}
type TestReportEntry = {
result: "PASS" | "FAIL" | "ERROR"
message: string
styles: {
icon: string
}
}
type TestReport = TestReportStartBlock | TestReportEntry | TestReportEndBlock
export default function runTestScriptWithVariables(
script: string,
variables?: TestScriptVariables
) {
const pw = {
_errors: [],
_testReports: [] as TestReport[],
_report: "",
expect(value: any) {
try {
return expect(value, this._testReports)
} catch (e) {
pw._testReports.push({
result: "ERROR",
message: e.toString(),
styles: styles.ERROR,
})
}
},
test: (descriptor: string, func: () => void) =>
test(descriptor, func, pw._testReports),
// globals that the script is allowed to have access to.
}
Object.assign(pw, variables)
// run pre-request script within this function so that it has access to the pw object.
// eslint-disable-next-line no-new-func
new Function("pw", script)(pw)
return {
report: pw._report,
errors: pw._errors,
testResults: pw._testReports,
}
}
function test(
descriptor: string,
func: () => void,
_testReports: TestReport[]
) {
_testReports.push({ startBlock: descriptor })
try {
func()
} catch (e) {
_testReports.push({ result: "ERROR", message: e, styles: styles.ERROR })
}
_testReports.push({ endBlock: true })
// TODO: Organize and generate text report of each {descriptor: true} section in testReports.
// add checkmark or x depending on if each testReport is pass=true or pass=false
}
function expect(expectValue: any, _testReports: TestReport[]) {
return new Expectation(expectValue, null, _testReports)
}
class Expectation {
private expectValue: any
private not: true | Expectation
private _testReports: TestReport[]
constructor(
expectValue: any,
_not: boolean | null,
_testReports: TestReport[]
) {
this.expectValue = expectValue
this.not = _not || new Expectation(this.expectValue, true, _testReports)
this._testReports = _testReports // this values is used within Test.it, which wraps Expectation and passes _testReports value.
}
private _satisfies(expectValue: any, targetValue?: any): boolean {
// Used for testing if two values match the expectation, which could be === OR !==, depending on if not
// was used. Expectation#_satisfies prevents the need to have an if(this.not) branch in every test method.
// Signature is _satisfies([expectValue,] targetValue): if only one argument is given, it is assumed the targetValue, and expectValue is set to this.expectValue
if (!targetValue) {
targetValue = expectValue
expectValue = this.expectValue
}
if (this.not === true) {
// test the inverse. this.not is always truthly, but an Expectation that is inverted will always be strictly `true`
return expectValue !== targetValue
} else {
return expectValue === targetValue
}
}
_fmtNot(message: string) {
// given a string with "(not)" in it, replaces with "not" or "", depending if the expectation is expecting the positive or inverse (this._not)
if (this.not === true) {
return message.replace("(not)", "not ")
} else {
return message.replace("(not)", "")
}
}
_fail(message: string) {
return this._testReports.push({
result: "FAIL",
message,
styles: styles.FAIL,
})
}
_pass(message: string) {
return this._testReports.push({
result: "PASS",
message,
styles: styles.PASS,
})
}
// TEST METHODS DEFINED BELOW
// these are the usual methods that would follow expect(...)
toBe(value: any) {
return this._satisfies(value)
? this._pass(
this._fmtNot(`${this.expectValue} do (not)match with ${value}`)
)
: this._fail(
this._fmtNot(`Expected ${this.expectValue} (not)to be ${value}`)
)
}
toHaveProperty(value: string) {
return this._satisfies(
Object.prototype.hasOwnProperty.call(this.expectValue, value),
true
)
? this._pass(
this._fmtNot(`${this.expectValue} do (not)have property ${value}`)
)
: this._fail(
this._fmtNot(
`Expected object ${this.expectValue} to (not)have property ${value}`
)
)
}
toBeLevel2xx() {
const code = parseInt(this.expectValue, 10)
if (Number.isNaN(code)) {
return this._fail(
`Expected 200-level status but could not parse value ${this.expectValue}`
)
}
return this._satisfies(code >= 200 && code < 300, true)
? this._pass(
this._fmtNot(`${this.expectValue} is (not)a 200-level status`)
)
: this._fail(
this._fmtNot(
`Expected ${this.expectValue} to (not)be 200-level status`
)
)
}
toBeLevel3xx() {
const code = parseInt(this.expectValue, 10)
if (Number.isNaN(code)) {
return this._fail(
`Expected 300-level status but could not parse value ${this.expectValue}`
)
}
return this._satisfies(code >= 300 && code < 400, true)
? this._pass(
this._fmtNot(`${this.expectValue} is (not)a 300-level status`)
)
: this._fail(
this._fmtNot(
`Expected ${this.expectValue} to (not)be 300-level status`
)
)
}
toBeLevel4xx() {
const code = parseInt(this.expectValue, 10)
if (Number.isNaN(code)) {
return this._fail(
`Expected 400-level status but could not parse value ${this.expectValue}`
)
}
return this._satisfies(code >= 400 && code < 500, true)
? this._pass(
this._fmtNot(`${this.expectValue} is (not)a 400-level status`)
)
: this._fail(
this._fmtNot(
`Expected ${this.expectValue} to (not)be 400-level status`
)
)
}
toBeLevel5xx() {
const code = parseInt(this.expectValue, 10)
if (Number.isNaN(code)) {
return this._fail(
`Expected 500-level status but could not parse value ${this.expectValue}`
)
}
return this._satisfies(code >= 500 && code < 600, true)
? this._pass(
this._fmtNot(`${this.expectValue} is (not)a 500-level status`)
)
: this._fail(
this._fmtNot(
`Expected ${this.expectValue} to (not)be 500-level status`
)
)
}
toHaveLength(expectedLength: number) {
const actualLength = this.expectValue.length
return this._satisfies(actualLength, expectedLength)
? this._pass(
this._fmtNot(
`Length expectation of (not)being ${expectedLength} is kept`
)
)
: this._fail(
this._fmtNot(
`Expected length to be ${expectedLength} but actual length was ${actualLength}`
)
)
}
toBeType(expectedType: string) {
const actualType = typeof this.expectValue
if (
![
"string",
"boolean",
"number",
"object",
"undefined",
"bigint",
"symbol",
"function",
].includes(expectedType)
) {
return this._fail(
this._fmtNot(
`Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"`
)
)
}
return this._satisfies(actualType, expectedType)
? this._pass(this._fmtNot(`The type is (not)"${expectedType}"`))
: this._fail(
this._fmtNot(
`Expected type to be "${expectedType}" but actual type was "${actualType}"`
)
)
}
}
export function transformResponseForTesting(
response: HoppRESTResponse
): TestScriptResponse {
if (response.type === "loading") {
throw new Error("Cannot transform loading responses")
}
if (response.type === "network_fail") {
throw new Error("Cannot transform failed responses")
}
let body: any = new TextDecoder("utf-8").decode(response.body)
// Try parsing to JSON
try {
body = JSON.parse(body)
} catch (_) {}
return {
body,
headers: response.headers,
status: response.statusCode,
__newRes: response,
}
}

View File

@@ -1,42 +0,0 @@
import {
getCurrentEnvironment,
getGlobalVariables,
} from "~/newstore/environments"
export default function getEnvironmentVariablesFromScript(script: string) {
const _variables: Record<string, string> = {}
const currentEnv = getCurrentEnvironment()
for (const variable of currentEnv.variables) {
_variables[variable.key] = variable.value
}
const globalEnv = getGlobalVariables()
if (globalEnv) {
for (const variable of globalEnv) {
_variables[variable.key] = variable.value
}
}
try {
// the pw object is the proxy by which pre-request scripts can pass variables to the request.
// for security and control purposes, this is the only way a pre-request script should modify variables.
const pw = {
environment: {
set: (key: string, value: string) => (_variables[key] = value),
},
env: {
set: (key: string, value: string) => (_variables[key] = value),
},
// globals that the script is allowed to have access to.
}
// run pre-request script within this function so that it has access to the pw object.
// eslint-disable-next-line no-new-func
new Function("pw", script)(pw)
} catch (_e) {}
return _variables
}

View File

@@ -1,12 +0,0 @@
export function getSourcePrefix(source) {
const sourceEmojis = {
// Source used for info messages.
info: "\t [INFO]:\t",
// Source used for client to server messages.
client: "\t⬅ [SENT]:\t",
// Source used for server to client messages.
server: "\t➡ [RECEIVED]:\t",
}
if (Object.keys(sourceEmojis).includes(source)) return sourceEmojis[source]
return ""
}

View File

@@ -1,3 +0,0 @@
<template>
<Nuxt />
</template>

View File

@@ -1,6 +1,17 @@
[build.environment]
NODE_VERSION = "14"
NPM_FLAGS = "--prefix=/dev/null"
[build]
functions = "netlify/"
environment = { NODE_VERSION = "14" }
base = "/"
publish = "packages/hoppscotch-app/dist"
command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run generate"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
[[redirects]]
from = "/discord"
@@ -22,7 +33,7 @@
[[redirects]]
from = "/careers"
to = "https://www.notion.so/hoppscotch/3b9d5d5239a043bfb91701faabf5b8f0"
to = "https://hoppscotch.notion.site/3b9d5d5239a043bfb91701faabf5b8f0"
status = 301
force = true
@@ -37,3 +48,9 @@
to = "https://twitter.com/hoppscotch_io"
status = 301
force = true
[[redirects]]
from = "/github"
to = "https://github.com/hoppscotch/hoppscotch"
status = 301
force = true

View File

@@ -1,21 +0,0 @@
// Docs on event and context https://www.netlify.com/docs/functions/#the-handler-method
exports.handler = (event) => {
switch (event.httpMethod) {
case "GET":
try {
const name = event.queryStringParameters.name || "World"
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message: `Hello ${name}` }),
}
} catch (e) {
return { statusCode: 500, body: e.toString() }
}
default:
return { statusCode: 405, body: "Method Not Allowed" }
}
}

63650
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,110 +1,29 @@
{
"name": "hoppscotch",
"name": "hoppscotch-app",
"version": "2.0.0",
"description": "Open source API development ecosystem",
"author": "Hoppscotch (support@hoppscotch.io)",
"private": true,
"scripts": {
"dev": "nuxt",
"build": "vue-tsc --noEmit && nuxt build",
"start": "nuxt start",
"generate": "nuxt generate --modern",
"analyze": "npx nuxt build -a",
"lint:script": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
"lint:style": "stylelint **/*.{css,scss,vue} --ignore-path .gitignore",
"lint": "npm run lint:script && npm run lint:style",
"lintfix": "eslint --ext .ts,.js,.vue --ignore-path .gitignore . --fix",
"test": "jest",
"preinstall": "npx only-allow pnpm",
"prepare": "husky install",
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{ts,js,vue}": "eslint",
"*.{css,scss,vue}": "stylelint"
"dev": "pnpm -r do-dev",
"generate": "pnpm -r do-build-prod",
"start": "pnpm -r do-prod-start",
"lint": "pnpm -r do-lint",
"lintfix": "pnpm -r do-lintfix",
"pre-commit": "pnpm -r do-lint",
"test": "pnpm -r do-test"
},
"workspaces": [
"./packages/*"
],
"dependencies": {
"@apollo/client": "^3.4.10",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.28.0",
"@nuxtjs/gtm": "^2.4.0",
"@nuxtjs/i18n": "^7.0.3",
"@nuxtjs/robots": "^2.5.0",
"@nuxtjs/sitemap": "^2.4.0",
"@nuxtjs/toast": "^3.3.1",
"ace-builds": "^1.4.12",
"acorn": "^8.5.0",
"acorn-walk": "^8.2.0",
"core-js": "^3.17.2",
"esprima": "^4.0.1",
"firebase": "^9.0.1",
"fuse.js": "^6.4.6",
"graphql": "^15.5.0",
"graphql-language-service-interface": "^2.8.4",
"json-loader": "^0.5.7",
"lodash": "^4.17.21",
"mustache": "^4.2.0",
"node-interval-tree": "^1.3.3",
"nuxt": "^2.15.8",
"paho-mqtt": "^1.1.0",
"rxjs": "^7.3.0",
"socket.io-client": "^4.2.0",
"socketio-wildcard": "^2.0.0",
"splitpanes": "^2.3.8",
"tern": "^0.24.3",
"vue-apollo": "^3.0.7",
"vue-cli-plugin-apollo": "^0.22.2",
"vue-functional-data-merge": "^3.1.0",
"vue-github-button": "^1.3.0",
"vue-textarea-autosize": "^1.1.1",
"vue-tippy": "^4.11.0",
"vuejs-auto-complete": "^0.9.0",
"yargs-parser": "^20.2.9"
"husky": "^7.0.4",
"lint-staged": "^11.2.6"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/preset-env": "^7.15.4",
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@nuxt/types": "^2.15.8",
"@nuxt/typescript-build": "^2.1.0",
"@nuxtjs/color-mode": "^2.1.1",
"@nuxtjs/dotenv": "^1.4.1",
"@nuxtjs/eslint-config-typescript": "^6.0.1",
"@nuxtjs/google-analytics": "^2.4.0",
"@nuxtjs/google-fonts": "^1.3.0",
"@nuxtjs/pwa": "^3.3.5",
"@nuxtjs/stylelint-module": "^4.0.0",
"@nuxtjs/svg": "^0.2.0",
"@testing-library/jest-dom": "^5.14.1",
"@types/cookie": "^0.4.1",
"@types/lodash": "^4.14.172",
"@types/splitpanes": "^2.2.1",
"@vue/runtime-dom": "^3.2.10",
"@vue/test-utils": "^1.2.2",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^27.1.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-nuxt": ">=2.0.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.17.0",
"husky": "^7.0.2",
"jest": "^27.1.0",
"jest-serializer-vue": "^2.0.2",
"lint-staged": "^11.1.2",
"nuxt-windicss": "^1.2.3",
"prettier": "^2.3.2",
"pretty-quick": "^3.1.1",
"raw-loader": "^4.0.2",
"sass": "^1.39.0",
"sass-loader": "^10.2.0",
"stylelint": "^13.12.0",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"ts-jest": "^27.0.5",
"typescript": "^4.2",
"unplugin-vue2-script-setup": "^0.5.8",
"vue-jest": "^3.0.7",
"worker-loader": "^3.0.8"
"@commitlint/cli": "^13.2.1",
"@commitlint/config-conventional": "^13.2.0"
}
}

View File

@@ -18,6 +18,9 @@ module.exports = {
"plugin:prettier/recommended",
"plugin:nuxt/recommended",
],
ignorePatterns: [
"helpers/backend/graphql.ts"
],
plugins: ["vue", "prettier"],
// add your custom rules here
rules: {

118
packages/hoppscotch-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,118 @@
# Created by .ignore support plugin (hsz.mobi)
# Firebase
.firebase
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# Mac OSX
.DS_Store
# Vim swap files
*.swp
# Build data
.hoppscotch
# File explorer
.directory
# Tests screenshots
tests/*/screenshots
# Tests videos
tests/*/videos
# Local Netlify folder
.netlify
# Andrew's crazy Volar shim generator
shims-volar.d.ts
# Hoppscotch Backend Schema Introspection JSON
helpers/backend/backend-schema.json
# GraphQL Type Generation
helpers/backend/graphql.ts

View File

@@ -0,0 +1 @@
<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"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 362 B

View File

Before

Width:  |  Height:  |  Size: 330 B

After

Width:  |  Height:  |  Size: 330 B

View File

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 278 B

View File

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 279 B

View File

Before

Width:  |  Height:  |  Size: 291 B

After

Width:  |  Height:  |  Size: 291 B

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 306 B

After

Width:  |  Height:  |  Size: 306 B

View File

Before

Width:  |  Height:  |  Size: 317 B

After

Width:  |  Height:  |  Size: 317 B

View File

@@ -0,0 +1 @@
<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 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 435 B

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 311 B

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 292 B

After

Width:  |  Height:  |  Size: 292 B

View File

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 233 B

View File

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 233 B

View File

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 233 B

View File

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1 @@
<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"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>

After

Width:  |  Height:  |  Size: 275 B

View File

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 251 B

View File

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1 @@
<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 3h7a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-7m0-18H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7m0-18v18"></path></svg>

After

Width:  |  Height:  |  Size: 295 B

View File

Before

Width:  |  Height:  |  Size: 323 B

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -0,0 +1 @@
<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="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg>

After

Width:  |  Height:  |  Size: 274 B

View File

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 338 B

View File

Before

Width:  |  Height:  |  Size: 337 B

After

Width:  |  Height:  |  Size: 337 B

View File

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 351 B

View File

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 429 B

View File

Before

Width:  |  Height:  |  Size: 289 B

After

Width:  |  Height:  |  Size: 289 B

View File

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 398 B

View File

Before

Width:  |  Height:  |  Size: 440 B

After

Width:  |  Height:  |  Size: 440 B

View File

Before

Width:  |  Height:  |  Size: 309 B

After

Width:  |  Height:  |  Size: 309 B

View File

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 325 B

View File

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 370 B

View File

Before

Width:  |  Height:  |  Size: 281 B

After

Width:  |  Height:  |  Size: 281 B

View File

Before

Width:  |  Height:  |  Size: 453 B

After

Width:  |  Height:  |  Size: 453 B

View File

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 497 B

View File

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 380 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 330 B

After

Width:  |  Height:  |  Size: 330 B

View File

Before

Width:  |  Height:  |  Size: 304 B

After

Width:  |  Height:  |  Size: 304 B

View File

Before

Width:  |  Height:  |  Size: 376 B

After

Width:  |  Height:  |  Size: 376 B

View File

Before

Width:  |  Height:  |  Size: 319 B

After

Width:  |  Height:  |  Size: 319 B

View File

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 325 B

View File

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 335 B

View File

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 542 B

View File

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 325 B

View File

Before

Width:  |  Height:  |  Size: 584 B

After

Width:  |  Height:  |  Size: 584 B

View File

Before

Width:  |  Height:  |  Size: 293 B

After

Width:  |  Height:  |  Size: 293 B

View File

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 336 B

View File

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 326 B

View File

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 366 B

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