Compare commits

..

168 Commits

Author SHA1 Message Date
Akash K
eeee8af806 chore: changes to support id based syncing for collections (#2977) 2023-04-11 14:30:01 +05:30
Andrew Bastin
134441a6e7 chore: update CODEOWNERS 2023-04-09 21:54:51 +05:30
Andrew Bastin
80a5d21576 chore: set web and common versions to 2023.4.0 and remove workspace package version specifiers 2023-04-09 21:42:25 +05:30
Akash K
f1a812dae2 refactor: add optional ids to environments (#2974) 2023-04-09 14:30:42 +05:30
Anwarul Islam
65a194a6d2 fix: shortcode data do not fetch or render (#2972) 2023-04-07 03:11:07 +05:30
Anwarul Islam
55e3dd3c18 fix: reset save context issue on delete collection or folder of team (#2971) 2023-04-07 03:06:30 +05:30
Nivedin
b88f496f4e fix: team environment bug when logout (#2970) 2023-04-07 03:00:30 +05:30
Anwarul Islam
8caf9f110b feat: added scrollbar for tabs (#2969) 2023-04-06 15:20:16 +05:30
Akash K
1370b53726 chore: enable/disable features in platforms (#2968) 2023-04-06 15:06:33 +05:30
Nivedin
8590a9a110 fix: reordering last request bug and its UX (#2967)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-04-05 21:35:14 +05:30
Anwarul Islam
62058d5dfe fix: tabhead and scrolling issue (#2966)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-04-05 21:26:42 +05:30
Nivedin
1d397af674 fix: move collection and request to bottom of list (#2964) 2023-04-05 15:38:57 +05:30
Nivedin
141a468808 chore: update header UI (#2965) 2023-04-05 15:30:47 +05:30
Akash K
a24d724e2b chore: load terms of service & privacy policy links from env variables (#2963) 2023-04-04 20:59:31 +05:30
Anwarul Islam
dd72eacd21 fix: no page rendering issue after navigating from rest page (#2962) 2023-04-04 20:48:32 +05:30
Akash K
c3c3fc6720 chore: use IDs instead of Strings in graphql queries (#2961) 2023-04-04 04:10:32 +05:30
Akash K
37a3b72025 chore: move analytics to platform (#2960) 2023-04-04 02:15:20 +05:30
Anwarul Islam
defece95fc feat: rest revamp (#2918)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-03-31 00:45:42 +05:30
Akash K
dbb45e7253 chore: add removeDuplicateEntry dispatcher to history store (#2955) 2023-03-30 15:13:25 +05:30
Akash K
cc802b1e9f chore: move history firebase things to hoppscotch-web (#2954) 2023-03-30 00:09:08 +05:30
Akash K
a66a2f5645 chore: move settings firebase things to platform (#2953) 2023-03-29 23:59:28 +05:30
Akash K
39afeab5f8 refactor: make editFolder, editCollection take Partial collection as parameter (#2952) 2023-03-29 12:00:20 +05:30
Andrew Bastin
1583c86c78 chore: add dotenv as dev dependency to fix staging issues 2023-03-14 22:32:28 +05:30
Akash K
1372681b87 refactor: store changes for collections (#2947) 2023-03-14 14:16:45 +05:30
Nivedin
2179ce6fff fix: reordering bugs and UX fixes (#2948) 2023-03-14 14:01:47 +05:30
Akash K
7e1b26c6a9 chore: move collections sync system to platform (#2940) 2023-03-13 17:08:05 +05:30
Akash K
ae9b7183b5 refactor: optional variables to createEnvironment and fixing the order of initializing GqlClient (#2944) 2023-03-07 16:12:11 +05:30
Akash K
3fa4052538 chore: move environments firebase things to hoppscotch-web (#2939) 2023-03-02 18:50:13 +05:30
Liyas Thomas
f2de0dc673 chore: minor ui improvements 2023-02-28 16:25:46 +05:30
Nivedin
7e686a8882 feat: global workspace selector (#2922)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-02-24 23:20:02 +05:30
Nivedin
4ca6e9ec3a feat: added reordering and moving for collection (#2916) 2023-02-24 19:09:07 +05:30
Andrew Bastin
dcd441f15e fix: link not rendering and UI storybook build issues 2023-02-24 15:51:10 +05:30
Andrew Bastin
90c8fbeee4 fix: issues with ui histoire building and modal not having close button 2023-02-24 14:35:42 +05:30
Andrew Bastin
cae1840506 refactor: update hopp-ui to be independent (#2927)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-02-24 13:20:12 +05:30
Jesvin Jose
82c6f6f6bc fix: response time for requests via extension has incorrect value (#2921)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-02-17 19:14:50 +05:30
Andrew Bastin
2545262fc2 chore: improve DispatchingStore typings for dispatch streams 2023-02-17 16:04:29 +05:30
Andrew Bastin
b27fe871c4 refactor: improve type checking for DispatchingStore dispatch payloads 2023-02-14 14:17:00 +05:30
Andrew Bastin
cb5fff0310 fix: graphql collections not syncing on login 2023-02-14 10:29:43 +05:30
Liyas Thomas
d15caba4a6 chore: improve ui responsiveness 2023-02-08 18:57:52 +05:30
Liyas Thomas
536c8128dd docs: update package description [skip ci] 2023-02-08 18:50:32 +05:30
Andrew Bastin
99918ee0c0 chore: prettier version bump and related fixes 2023-02-08 18:35:27 +05:30
Liyas Thomas
864d40d934 chore: improved theme colors 2023-02-08 15:13:24 +05:30
Jesvin Jose
a227af05d9 feat: active tab no longer resets after request (#2917)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Closes https://github.com/hoppscotch/hoppscotch/issues/2080
2023-02-08 10:29:18 +05:30
Andrew Bastin
ce0898956d chore: reintroduce updated auth mechanism 2023-02-07 19:21:06 +05:30
Jesvin Jose
cd72851289 refactor: cli updates (#2907)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-02-07 17:47:54 +05:30
Nivedin
f676f94278 fix: graphql save request emit payload (#2913) 2023-02-01 15:20:13 +05:30
Liyas Thomas
cd6e40f01c chore: uniform ui in rest and graphql collections 2023-01-31 22:39:24 +05:30
Nivedin
59a8a22e8a fix: search on collections > empty state ui (#2912)
fix: collection search filter ui
2023-01-31 22:33:10 +05:30
Nivedin
2910164d5a feat : smart tree component (#2865)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-01-31 17:15:03 +05:30
Liyas Thomas
b95e2b365a fix: broken environment highlight color 2023-01-30 10:01:33 +05:30
Liyas Thomas
73e788b513 chore: fix broken RouterLink component 2023-01-28 09:49:14 +05:30
Petro S
15d135c11b chore: fixed i18n grammatical errors (#2908) 2023-01-28 08:44:31 +05:30
Anwarul Islam
0fcda0be1a refactor: hoppscotch ui (#2887)
* feat: hopp ui initialized

* feat: button components added

* feat: windi css integration

* chore: package removed from hopp ui

* feat: storybook added

* feat: move all smart components hoppscotch-ui

* fix: import issue from components/smart

* fix: env input component import

* feat: add hoppui to windicss config

* fix: remove storybook

* feat: move components from hoppscotch-ui

* feat: storybook added

* feat: storybook progress

* feat: themeing storybook

* feat: add stories

* chore: package updated

* chore: stories added

* feat: stories added

* feat: stories added

* feat: icons resolved

* feat: i18n composable resolved

* feat: histoire added

* chore: resolved prettier issue

* feat: radio story added

* feat: story added for all components

* feat: new components added to stories

* fix: resolved issues

* feat: readme.md added

* feat: context/provider added

* chore: removed app component registry

* chore: remove importing of all components in hopp-ui to allow code splitting

* chore: fix vite config errors

* chore: jsdoc added

* chore: any replaced with smart-item

* chore: i18n added to ui components

* chore: clean up - removed a duplicate button

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-01-28 08:27:00 +05:30
Andrew Bastin
9d7052c626 chore: add CODEOWNERS 2023-01-13 21:53:37 -05:00
Masaki Tagawa
5841d2eb66 chore(i18n): update i18n translations 2023-01-09 20:53:12 +05:30
tzhangm
ee07a90b5e chore: update i18n translations 2023-01-05 13:27:17 +05:30
5idereal
70d2f1e3d9 chore: update i18n translations (#2892) 2023-01-03 12:51:33 +05:30
Liyas Thomas
acafc072db chore: minor ui improvements 2022-12-29 11:10:16 +05:30
Anwarul Islam
51e40581b0 fix: login modal not visible in small screen 2022-12-26 01:40:55 +06:00
Andrew Bastin
1e5dd1cc53 chore: introduce platform object for platform specific code 2022-12-21 19:21:52 -05:00
Liyas Thomas
3d7b057026 chore: updated i18n translation, minor ux improvements 2022-12-17 09:57:57 +05:30
Anwarul Islam
d36ab337d7 feat: ability to delete user account and data (#2863)
* feat: add gql mutation

* feat: added delete account section in profile page

* feat: separate shortcodes section to a component

* feat: delete user modal

* feat: delete user account

* feat: navigate to homepage after delete

* chore: improve ui

* fix: delete user mutation

* chore: minor ui improvements

* chore: correct grammar in certain i18n strings

* feat: delection section separated to component

* feat: separate user delete section into component

* feat: defer fetch my teams

* feat: disable delete account button on loading state

* Update Shortcodes.vue

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-12-17 09:31:39 +05:30
Liyas Thomas
012f9b5314 feat: prettify xml request body - fixed #2878 2022-12-15 17:06:18 +05:30
Liyas Thomas
ba6069324f chore: minor ui improvements 2022-12-14 19:29:04 +05:30
Liyas Thomas
0d26d4cdbd ci: updated workflow comments 2022-12-14 19:10:52 +05:30
Liyas Thomas
4b920feffa ci: maximize build space 2022-12-14 16:33:33 +05:30
Andrew Bastin
830373efb3 chore: reintroduce sitemap generation (#2874) 2022-12-10 21:10:45 -05:00
Akash K
c3f18671ec fix: cannot write to body when a request is loaded from history (#2873)
* fix: cannot write body when a request is loaded from history

* fix: import `toRaw()` from vue

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2022-12-09 20:39:36 +05:30
Liyas Thomas
0d33758ba4 ci: introduce staging deployment actions 2022-12-07 12:11:06 +05:30
Akash K
e7e8c397ef fix: circular watcher dependencies on invite.vue causing infinite loop (#2871) 2022-12-06 15:59:38 -05:00
Liyas Thomas
b04b12c7a0 fix: broken links 2022-12-06 12:09:20 +05:30
Liyas Thomas
a1d69b3210 chore: minor ui improvements 2022-12-03 13:01:47 +05:30
Liyas Thomas
dcbc2f1145 ci: use latest workflow versions 2022-12-03 00:51:49 +05:30
Andrew Bastin
36903b338a fix: broken Dockerfile and final start command 2022-12-02 13:34:46 -05:00
Andrew Bastin
9d8d6832af chore: fix broken ci 2022-12-02 11:01:53 -05:00
Andrew Bastin
3d004f2322 chore: split app to commons and web (squash commit) 2022-12-02 03:05:35 -05:00
Liyas Thomas
fb827e3586 Create deploy-preview-netlify.yml 2022-12-02 03:02:56 -05:00
Liyas Thomas
ccca183e08 chore: minor ui improvements 2022-12-01 17:47:39 +05:30
Liyas Thomas
237455ab21 chore: minor ui improvements 2022-11-29 13:50:58 +05:30
Liyas Thomas
6141073137 chore: minor ui improvements 2022-11-27 23:19:19 +05:30
Liyas Thomas
740691417f chore: updated translation 2022-11-27 03:52:15 +05:30
Anwarul Islam
2ed709796a MQTT Revamp (#2381)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2022-11-27 02:43:24 +05:30
Akash K
75c0350584 fix: delete not working properly on request body (#2861) 2022-11-24 22:00:43 -05:00
Liyas Thomas
17d72b9922 fix: typo 2022-11-17 09:58:52 +05:30
Liyas Thomas
1796fae3d1 chore: updated tech stack list 2022-11-16 09:45:55 +05:30
Akash K
1ecd22204d chore: remove unwanted debug info (#2851) 2022-11-09 17:37:05 -05:00
Akash K
356fe4591f fix: fix cursor going out of bounds when filtering response (#2850) 2022-11-09 17:23:28 -05:00
Andrew Bastin
0230942a3d chore: introduce devcontainer support 2022-11-08 15:51:26 -05:00
Andrew Bastin
325793eebc fix: onLoggedIn called when id token is not yet resolved for auth users 2022-11-07 22:36:12 -05:00
Nivedin
39d1256f68 refactor: move global environment selector to top (#2848)
* refactor: moved global env to top

* fix: change to my collection and env when logedout

* fix: merge fix

* refactor: change v-show to v-if

* chore: minor type change

* chore: pass variable name to edit

* chore: improve logic

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>

* chore: minor ui improvements

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-11-03 10:13:06 +05:30
Andrew Bastin
fd2472d34b chore: add vue-tsc dep and introduce lint:ts script for prelim ts fixes 2022-11-02 13:08:25 -04:00
Andrew Bastin
5e8fbc6552 chore: fix some type errors in '/r' route 2022-11-02 11:03:08 -04:00
Liyas Thomas
3084a40729 fix: set focus to newly added environment key field 2022-11-02 18:11:13 +05:30
Francisco Emanuel de Sales Pereira
0069f51ea4 feat: added inline environment variable edit button (#2813)
* refactor: changes v-if render to v-show on Environments tabs

* feat: adds selectText prop to EnvInput

* feat: adds editing variable name to env Details modal

* feat: adds actions to invoke edit env modals

* feat: adds edit action to tooltip env

* refactor: adds destructuring assignment on action handlers for edit env modals

* refactor: fix comment on environment modals action

* chore: minor ui improvements

* refactor: change text selecion prop on EnvInput to something more meaningful

* refactor: removes comment on HoppEnvironment extension

* refactor: renames isTextSelected EnvInput prop to selectTextOnMount

* refactor: remove type definition of automatic inferrable variables

* refactor: edit environment call to only allow accepted types

* feat: introduce type safe action arguments

* fix: revert v-show to v-if

* chore: minor ui improvements

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-11-02 17:55:22 +05:30
Liyas Thomas
696c612489 fix: broken tippy focus event 2022-11-02 15:05:57 +05:30
Liyas Thomas
4a5a4077af chore: improvements to tooltips and popovers 2022-11-02 14:42:00 +05:30
Liyas Thomas
28bcb899e7 chore: improve ui consistency 2022-11-01 19:05:13 +05:30
Liyas Thomas
4062a7089a perf: temporarily disable dependabot 2022-11-01 09:40:14 +05:30
Akash K
ad86221c7d fix: linting erroring out on graphql queries (#2846) 2022-10-31 18:45:53 -04:00
Akash K
53938248de fix: errors on opening json outline (#2847) 2022-10-31 18:28:31 -04:00
Liyas Thomas
c018b639ad chore: minor ui improvements 2022-11-01 00:42:35 +05:30
Liyas Thomas
eb2145c7da fix: updated prop name 2022-10-30 17:50:17 +05:30
Liyas Thomas
2f4c39d310 feat: filter and group history entries 2022-10-30 17:05:32 +05:30
biondizzle
79ada82223 feat: use environment variable to specify proxyscotch access token (#2791) 2022-10-29 18:16:40 -04:00
Liyas Thomas
c67463fb3b chore: updated translations
commit 2adcfe9804ede1fea584db24e0a27dc1b5e034e3
Author: Archontis Kostis <arxontisk02@gmail.com>
Date:   Sat Oct 29 22:04:01 2022 +0300

    chore: updated translation (#2837)

commit 56b8a5cb38
Author: 5idereal <nelson22768384@gmail.com>
Date:   Sat Oct 29 11:58:25 2022 +0800

    chore: updated translation (#2835)
2022-10-30 00:45:37 +05:30
Akash K
d162471555 chore: add polyfill for replaceAll (#2836) 2022-10-28 20:29:07 -04:00
Nivedin
c99797bcef fix: import hopp collection (#2824) 2022-10-27 22:29:46 -04:00
Nivedin
9739cdbbaa fix: codemirror field overflow (#2827)
* fix: codemirror editor overflow

* chore: minor ui improvements

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2022-10-27 08:12:23 +05:30
Akash K
7f6db561f5 fix: fix missing analytics (#2826)
fix: typo in environment variable name
2022-10-26 18:50:03 +05:30
Akash K
9eac00b303 fix: useCodemirror getting non strings as value (#2810) 2022-10-22 12:36:07 +05:30
Oliver Zhou (毓杰)
b61df04c1b fix: socket.io v3/v4 incomming message issue (#2811) 2022-10-22 12:31:31 +05:30
Liyas Thomas
b587e21c90 chore: updated translations
commit 1673c36cec5ac0e949bdf85c8e780f02ae25c06d
Author: Roberth González <63687573+rxb3rth@users.noreply.github.com>
Date:   Thu Oct 20 15:45:00 2022 +0200

    chore(i18n): updated translations (#2788)

commit af70ae2c43558cf7f658cf8bfcca35475e7786c7
Author: Fiqri Dwi <79080077+fiqridwi@users.noreply.github.com>
Date:   Thu Oct 20 18:41:53 2022 +0700

    chore: updated translations (#2805)
2022-10-20 19:47:57 +05:30
Liyas Thomas
02d66ee9fd chore: minor ui improvements 2022-10-20 19:47:17 +05:30
Akash K
a950e08ef1 chore: add temporary debug info for code mirror (#2806)
chore: temp debug info
2022-10-20 17:17:54 +05:30
Liyas Thomas
6e7d28db7b fix: overflow on log entries - closed #2738 2022-10-15 09:57:49 +05:30
Andrew Bastin
a0ea00d0a3 fix: form-data requests on proxy failing 2022-10-14 15:39:28 +05:30
Liyas Thomas
beb5606862 chore: updated translations
commit b8350ae25d85ec71be6a8ca2580fbeeebd73c5f6
Author: Andrii Bodnar <29282228+andrii-bodnar@users.noreply.github.com>
Date:   Fri Oct 14 06:47:18 2022 +0300

    chore: updated translations (#2781)

commit 04dd400871
Author: Daniel Krásný <53856821+DanielKrasny@users.noreply.github.com>
Date:   Wed Oct 12 14:26:02 2022 +0200

    chore: remove translations of brand names (#2777)
2022-10-14 09:21:20 +05:30
Anwarul Islam
44f11f93a4 fix: realtime connect/disconnect issue (#2768)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2022-10-13 17:33:46 +05:30
Hau Nguyen
7b61f267dd chore: migration vite import.meta.glob (#2778) 2022-10-12 15:14:43 +05:30
Akash K
971238cedb chore: serialize when logging errors (#2775) 2022-10-12 02:19:27 +05:30
Akash K
4d19b9249b fix: fix auth/graphql errors causing errors on logout (#2772) 2022-10-11 14:06:22 +05:30
Liyas Thomas
4046b91609 fix: resolved #2771 2022-10-11 10:35:14 +05:30
Nivedin
e6652109c5 fix: team environments import (#2770)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2022-10-10 20:15:01 +05:30
Liyas Thomas
e9cfc066a5 fix: resolved #2758 2022-10-10 14:50:53 +05:30
Liyas Thomas
9ce078c1d3 chore: updated translations 2022-10-09 21:22:36 +05:30
Liyas Thomas
2bcc1675e8 chore: updated translations
commit 2756922cc0
Author: islamzeki <islamzeki@users.noreply.github.com>
Date:   Sun Oct 9 18:31:47 2022 +0300

    Update tr.json (#2750)

commit fb13fae385
Author: Cheese <seojeee@gmail.com>
Date:   Mon Oct 10 00:28:34 2022 +0900

    add and fix translations in ko.json (#2752)

commit c28ffd604d
Author: Akhmad Thoriq <51510460+itstor@users.noreply.github.com>
Date:   Sun Oct 9 22:27:15 2022 +0700

    fix wrong translation and typo in Indonesia language (#2757)

commit fe18aa1310
Author: Fiki Maulana <fikimaul@gmail.com>
Date:   Wed Oct 5 21:40:19 2022 +0700

    translate and fix some Indonesia language (#2739)

commit 9f8a2c0cd3
Author: Dhravya Shah <dhravyashah@gmail.com>
Date:   Tue Oct 4 12:19:12 2022 +0530

    i18n: add Hindi to locales (#2734)
2022-10-09 21:04:57 +05:30
Liyas Thomas
a87c2347c9 chore: minor ui improvements 2022-10-09 20:55:22 +05:30
Liyas Thomas
8f2810db30 ci: use latest workflow version - superseded #2754 2022-10-09 16:44:44 +05:30
Andrew Bastin
528d0b0429 fix: history component crashing on special characters (fixes #2743) 2022-10-07 23:00:32 +05:30
Andrew Bastin
f759014315 fix: broken pr evaluation scripts 2022-10-07 22:20:05 +05:30
Nivedin
a568610c28 feat: team environments (#2512)
Co-authored-by: amk-dev <akash.k.mohan98@gmail.com>
Co-authored-by: liyasthomas <liyascthomas@gmail.com>
Co-authored-by: islamzeki <islamzeki@users.noreply.github.com>
Co-authored-by: Jesvin Jose <aitchnyu@users.noreply.github.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-10-07 22:05:39 +05:30
Damanpreet Singh
c35a85db12 fix: allow posting empty key files in multipart/form-data (#2664) 2022-10-07 02:29:15 +05:30
Andrew Bastin
fcd61436c8 chore: fix issues with broken ci scripts 2022-10-07 01:51:32 +05:30
Andrew Bastin
0798063213 chore: fix broken docker ci builds 2022-10-07 01:42:39 +05:30
jerbob92
80de63323d build: use GraphQL URL from env in gql-codegen (#2749)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-10-07 01:17:25 +05:30
jerbob92
7d0219b11d chore: allow configuration of shortcode base (#2748) 2022-10-07 00:50:48 +05:30
Akash K
d42434ddc0 feat: use email as fallback for display name (#2746)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-10-07 00:45:34 +05:30
Liyas Thomas
cbf6d23c24 chore: minor improvements 2022-10-06 21:21:24 +05:30
Akash K
568c05b4b0 fix: fix type error when generating profile picture initials (#2742)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-10-06 17:56:30 +05:30
Akash K
59ee4babeb fix: fix ref access when using v-for on Invite.vue (#2744)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-10-06 16:06:14 +05:30
Liyas Thomas
604ef4d004 fix: resolved #2740 2022-10-05 21:24:15 +05:30
Liyas Thomas
dc80cc80e6 build: v3.0.1 2022-10-05 08:42:23 +05:30
Liyas Thomas
f5a0c3cfca chore: minor ui improvements 2022-10-05 08:40:09 +05:30
Liyas Thomas
f8e9563392 fix: resolved #2736 2022-10-04 13:44:29 +05:30
Jesvin Jose
3c17a14bd3 fix: find linked account from authentication error and link it (#2662)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-10-04 01:08:00 +05:30
Liyas Thomas
3140859993 chore: updated translations
commit 3c15461d82e4b3f9e8fe719ed09d26feddf965df
Author: rivermanbw <35424579+rivermanbw@users.noreply.github.com>
Date:   Mon Oct 3 15:26:37 2022 +0200

    chore: updated translations (#2668)

commit ef68b8a93c
Author: Andrii Bodnar <29282228+andrii-bodnar@users.noreply.github.com>
Date:   Mon Oct 3 16:23:38 2022 +0300

    chore: updated Ukrainian translations (#2732)

commit 96f68e9d70
Author: islamzeki <islamzeki@users.noreply.github.com>
Date:   Mon Oct 3 12:19:39 2022 +0300

    i18n: updated translations (#2731)
2022-10-03 19:03:03 +05:30
Andrew Bastin
3c35bb6091 fix: issue with non-alphanumeric characters within body env variables (fixes #2665) 2022-10-03 14:55:59 +05:30
Liyas Thomas
355e37a27d chore: updated translations
commit c764c3ac6844da28a58ffca3af208b786f9d234e
Author: Aefar <87722826+Marineux@users.noreply.github.com>
Date:   Sun Oct 2 22:03:48 2022 +0700

    feat: add Indonesian translations (#2727)

commit 37d364766a
Author: Alin Pisica <alinp2508@gmail.com>
Date:   Sat Oct 1 19:42:27 2022 +0300

    i18n: update language translations (#2720)

commit 76339b498f
Author: Thomas Bnt <thomasbnt@protonmail.com>
Date:   Sat Oct 1 15:56:13 2022 +0200

    i18n: updated translations
2022-10-02 20:40:47 +05:30
Liyas Thomas
ca71ffe3c2 chore: minor ui improvements 2022-10-02 18:59:23 +05:30
Liyas Thomas
4bcc703444 chore: exclude redirect urls from service worker 2022-10-02 16:59:44 +05:30
Akash K
f01e0b888d chore: exclude robots.txt and sitemap.xml from using fallback navigation (#2706) 2022-10-02 16:39:10 +05:30
Liyas Thomas
5701454c02 ci: fix docker image 2022-10-02 10:31:55 +05:30
Liyas Thomas
0584f781c4 ci: upgrade actions/checkout to v3
Squashed commit of the following:

commit fac38b9293c6963cbc081f05f67b36d652524956
Author: Oscar Dominguez <dominguez.celada@gmail.com>
Date:   Sun Oct 2 02:50:33 2022 +0200

    ci(publish-docker): upgrade actions/checkout to v3 (#2721)

commit f352646887682e80105ca758849415f9628dd336
Author: Oscar Dominguez <dominguez.celada@gmail.com>
Date:   Sun Oct 2 02:50:15 2022 +0200

    ci(deploy-staging-netlify): upgrade actions/checkout to v3 (#2722)

commit 7afc517620a5f649dc08b1bf5afd6af086f20af9
Author: Oscar Dominguez <dominguez.celada@gmail.com>
Date:   Sun Oct 2 02:49:57 2022 +0200

    ci(deploy-prod): upgrade actions/checkout to v3 (#2723)

commit 50f0540f5afcbd835fa6db89e575aead1023c9ff
Author: Oscar Dominguez <dominguez.celada@gmail.com>
Date:   Sun Oct 2 02:49:45 2022 +0200

    ci(codeql-analysis): upgrade actions/checkout to v3 (#2724)

commit fc5d5ea131a553b1f38b272525f40941eea031ee
Author: Oscar Dominguez <dominguez.celada@gmail.com>
Date:   Sun Oct 2 02:49:31 2022 +0200

    ci(deploy-netlify): upgrade actions/checkout to v3 (#2725)

commit 4e16962001800b633309c726f71ac476efba4e13
Author: Oscar Dominguez <dominguez.celada@gmail.com>
Date:   Sun Oct 2 02:47:40 2022 +0200

    chore(PR template): fix typo (#2726)
2022-10-02 06:23:15 +05:30
Raul Piraces Alastuey
fd7d096a77 ci: modify publish-docker workflow to push for ARM platforms (#2719) 2022-10-01 21:13:38 +05:30
Liyas Thomas
6330548cc0 refactor: better shortcut indication 2022-10-01 19:19:43 +05:30
Liyas Thomas
048ac5f7a5 refactor: updated translations 2022-10-01 17:22:01 +05:30
hms5232
bac38688f9 fix: term translattion in tw.json (#2715) 2022-10-01 17:14:10 +05:30
Liyas Thomas
1f29ff24d7 refactor: improved popover actions, key bindings 2022-10-01 12:22:07 +05:30
Andrew Bastin
1006617e99 chore: add additional telemetry for teams errors 2022-10-01 01:53:19 +05:30
Andrew Bastin
99e7e73965 chore: fix eslint issues in windows 2022-09-30 23:54:52 +05:30
Andrew Bastin
a6b91c435c fix: issue with prod-lint in hopp-app on windows 2022-09-30 23:44:50 +05:30
Liyas Thomas
938f940f90 feat: response shortcuts (#2705) 2022-09-30 19:32:09 +05:30
Liyas Thomas
06a8d62dfe fix: resolved #2707 2022-09-30 19:06:44 +05:30
Andrew Bastin
44c439d7a9 chore: add sentry release submission 2022-09-30 16:08:52 +05:30
Andrew Bastin
fd11ea8143 chore: expose release info to sentry 2022-09-30 15:40:55 +05:30
Andrew Bastin
fb65e0e23d feat: introduce extra telemetry info for teams and backend operations 2022-09-30 12:57:15 +05:30
Liyas Thomas
1b23c5ea4a chore: minor ui improvements 2022-09-30 09:47:12 +05:30
Liyas Thomas
045dc10a0d fix: resolved #2704 2022-09-29 14:52:26 +05:30
692 changed files with 31971 additions and 17195 deletions

View File

@@ -0,0 +1,9 @@
{
"name": "Hoppscotch",
"image": "mcr.microsoft.com/devcontainers/typescript-node:18",
"forwardPorts": [3000],
"features": {
"ghcr.io/NicoVIII/devcontainer-features/pnpm:1": {}
},
"postCreateCommand": "mv .env.example .env && pnpm i"
}

View File

@@ -14,13 +14,22 @@ VITE_MESSAGING_SENDER_ID=421993993223
VITE_APP_ID=1:421993993223:web:ec0baa8ee8c02ffa1fc6a2
VITE_MEASUREMENT_ID=G-BBJ3R80PJT
# Base URL
# Base URLs
VITE_BASE_URL=https://hoppscotch.io
VITE_SHORTCODE_BASE_URL=https://hopp.sh
# Backend URLs
VITE_BACKEND_GQL_URL=https://api.hoppscotch.io/graphql
VITE_BACKEND_WS_URL=wss://api.hoppscotch.io/graphql
# Terms Of Service And Privacy Policy Links (Optional)
VITE_APP_TOS_LINK=https://docs.hoppscotch.io/terms
VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/privacy
# Sentry (Optional)
# VITE_SENTRY_DSN: <Sentry DSN here>
# VITE_SENTRY_ENVIRONMENT: <Sentry environment value here>
# VITE_SENTRY_RELEASE_TAG: <Sentry release tag here (for release monitoring)>
# Proxyscotch Access Token (Optional)
# VITE_PROXYSCOTCH_ACCESS_TOKEN: <Token Set In Proxyscotch Server>

View File

@@ -5,6 +5,6 @@ updates:
schedule:
interval: weekly
time: '00:00'
open-pull-requests-limit: 10
open-pull-requests-limit: 0
reviewers:
- liyasthomas

View File

@@ -23,4 +23,4 @@ Closes # <!-- Issue # here -->
- [ ] All the tests have passed
### Additional Information
<!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behavior, etc. -->
<!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behaviour, etc. -->

View File

@@ -1,72 +1,63 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
name: "CodeQL analysis"
on:
push:
branches: [ main ]
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
branches: [main]
schedule:
- cron: '39 7 * * 2'
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# │ │ │ │ │
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: '30 1 * * 0'
jobs:
analyze:
name: Analyze
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
# required for all workflows
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
# only required for workflows in private repositories
actions: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Checkout
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
# Run extended queries including queries using machine learning
queries: security-extended
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
# Run extended queries including queries using machine learning
queries: security-extended
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below).
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following
# three lines and modify them (or add more) to build your code if your
# project uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -1,16 +1,19 @@
name: Deploy to Live Channel
name: Deploy to Firebase (production)
on:
push:
branches:
- main
branches: [main]
jobs:
deploy_live_website:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: FirebaseExtended/action-hosting-deploy@v0
- name: Checkout
uses: actions/checkout@v3
- name: Deploy to Firebase (production)
uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_POSTWOMAN_API }}'

View File

@@ -0,0 +1,54 @@
name: Deploy to Netlify (production)
on:
push:
branches: [main]
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup environment
run: mv .env.example .env
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7
run_install: true
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Build site
env:
VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_SENTRY_ENVIRONMENT: production
VITE_SENTRY_RELEASE_TAG: ${{ github.sha }}
run: pnpm run generate
# Deploy the production site with netlify-cli
- name: Deploy to Netlify (production)
run: npx netlify-cli deploy --dir=packages/hoppscotch-web/dist --prod
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_PRODUCTION_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
- name: Create Sentry release
uses: getsentry/action-release@v1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
with:
environment: production
ignore_missing: true
ignore_empty: true
version: ${{ github.sha }}

View File

@@ -1,25 +1,34 @@
name: Deploy to Staging Netlify
name: Deploy to Netlify (staging)
on:
push:
# TODO: Migrate to staging branch only
branches: [main]
branches: [staging]
pull_request:
branches: [staging]
jobs:
build:
name: Push build files to Netlify
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Checkout
uses: actions/checkout@v3
- name: Setup and run pnpm install
uses: pnpm/action-setup@v2.2.2
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.4
env:
VITE_BACKEND_GQL_URL: ${{ secrets.STAGING_BACKEND_GQL_URL }}
with:
version: 7
run_install: true
- name: Build Site
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Build site
env:
VITE_GA_ID: ${{ secrets.STAGING_GA_ID }}
VITE_GTM_ID: ${{ secrets.STAGING_GTM_ID }}
@@ -34,12 +43,25 @@ jobs:
VITE_BACKEND_GQL_URL: ${{ secrets.STAGING_BACKEND_GQL_URL }}
VITE_BACKEND_WS_URL: ${{ secrets.STAGING_BACKEND_WS_URL }}
VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_SENTRY_RELEASE_TAG: ${{ github.sha }}
VITE_SENTRY_ENVIRONMENT: staging
run: pnpm run generate
# Deploy the staging site with netlify-cli
- name: Deploy to Netlify (staging)
run: npx netlify-cli deploy --dir=packages/hoppscotch-app/dist --prod
run: npx netlify-cli deploy --dir=packages/hoppscotch-web/dist --prod
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_STAGING_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
- name: Create Sentry release
uses: getsentry/action-release@v1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
with:
environment: staging
ignore_missing: true
ignore_empty: true
version: ${{ github.sha }}

41
.github/workflows/deploy-netlify-ui.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Deploy to Netlify (ui)
on:
push:
branches: [main]
# run this workflow only if an update is made to the ui package
paths:
- "packages/hoppscotch-ui/**"
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup environment
run: mv .env.example .env
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7
run_install: true
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Build site
run: pnpm run generate-ui
# Deploy the ui site with netlify-cli
- name: Deploy to Netlify (ui)
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -1,35 +0,0 @@
name: Deploy to Netlify
on:
push:
branches: [main]
jobs:
build:
name: Push build files to Netlify
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup and run pnpm install
uses: pnpm/action-setup@v2.2.2
with:
version: 7
run_install: true
- name: Setup Environment
run: mv packages/hoppscotch-app/.env.example packages/hoppscotch-app/.env
- name: Build Site
env:
VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_SENTRY_ENVIRONMENT: production
run: pnpm run generate
# Deploy the production site with netlify-cli
- name: Deploy to Netlify (production)
run: npx netlify-cli deploy --dir=packages/hoppscotch-app/dist --prod
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_PRODUCTION_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -7,22 +7,37 @@ on:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Maximize build space
uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 8192
swap-size-mb: 18432
remove-dotnet: 'true'
remove-android: 'true'
remove-haskell: 'true'
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
uses: docker/metadata-action@v4
with:
images: hoppscotch/hoppscotch
flavor: |
@@ -31,9 +46,10 @@ jobs:
suffix=
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
uses: docker/build-push-action@v3
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -2,12 +2,13 @@ name: Node.js CI
on:
push:
branches: [main]
branches: [main, staging]
pull_request:
branches: [main]
branches: [main, staging]
jobs:
build:
test:
name: Test
runs-on: ubuntu-latest
strategy:
@@ -15,17 +16,23 @@ jobs:
node-version: ["lts/*"]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Setup and run pnpm install
uses: pnpm/action-setup@v2.2.2
- name: Checkout
uses: actions/checkout@v3
- name: Setup environment
run: mv .env.example .env
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7
run_install: true
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
node-version: ${{ matrix.node }}
cache: pnpm
- name: Run tests
run: pnpm test

82
.gitignore vendored
View File

@@ -1,15 +1,18 @@
# Created by .ignore support plugin (hsz.mobi)
# Firebase
.firebase
### Node template
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
@@ -22,6 +25,7 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
@@ -45,12 +49,27 @@ jspm_packages/
# TypeScript v1 declaration files
typings/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
@@ -60,26 +79,68 @@ typings/
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# next.js build output
# Next.js build output
.next
out
# nuxt.js build output
# Nuxt.js build / generate output
.nuxt
# Nuxt generate
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# SvelteKit build / generate output
.svelte-kit
# IDE / Editor
.idea
@@ -107,3 +168,6 @@ tests/*/videos
# Local Netlify folder
.netlify
# PNPM
.pnpm-store

View File

@@ -6,6 +6,7 @@
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"csstools.postcss",
"folke.vscode-monorepo-workspace"
],
"unwantedRecommendations": [
"octref.vetur"

27
CODEOWNERS Normal file
View File

@@ -0,0 +1,27 @@
# CODEOWNERS is prioritized from bottom to top
# If none of the below matched
* @AndrewBastin @liyasthomas
# Packages
/packages/codemirror-lang-graphql/ @AndrewBastin
/packages/hoppscotch-cli/ @AndrewBastin
/packages/hoppscotch-common/ @amk-dev @AndrewBastin
/packages/hoppscotch-data/ @AndrewBastin
/packages/hoppscotch-js-sandbox/ @AndrewBastin
/packages/hoppscotch-ui/ @anwarulislam
/packages/hoppscotch-web/ @amk-dev
# Sections within Hoppscotch Common
/packages/hoppscotch-common/src/components @anwarulislam
/packages/hoppscotch-common/src/components/collections @nivedin @amk-dev
/packages/hoppscotch-common/src/components/environments @nivedin @amk-dev
/packages/hoppscotch-common/src/composables @amk-dev
/packages/hoppscotch-common/src/modules @AndrewBastin @amk-dev
/packages/hoppscotch-common/src/pages @AndrewBastin @amk-dev
/packages/hoppscotch-common/src/newstore @AndrewBastin @amk-dev
README.md @liyasthomas
# The lockfile has no owner
pnpm-lock.yaml

View File

@@ -17,13 +17,13 @@ COPY . .
RUN npm install -g pnpm
RUN mv .env.example .env
RUN pnpm i --unsafe-perm=true
ENV HOST 0.0.0.0
EXPOSE 3000
RUN mv packages/hoppscotch-app/.env.example packages/hoppscotch-app/.env
RUN pnpm run generate
CMD ["pnpm", "run", "start"]

View File

@@ -36,14 +36,14 @@
<p>
<a href="https://hoppscotch.io/#gh-light-mode-only" target="_blank">
<img
src="./packages/hoppscotch-app/static/images/banner-light.png"
src="./packages/hoppscotch-common/public/images/banner-light.png"
alt="Hoppscotch"
width="100%"
/>
</a>
<a href="https://hoppscotch.io/#gh-dark-mode-only" target="_blank">
<img
src="./packages/hoppscotch-app/static/images/banner-dark.png"
src="./packages/hoppscotch-common/public/images/banner-dark.png"
alt="Hoppscotch"
width="100%"
/>
@@ -275,11 +275,11 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [TypeScript](https://www.typescriptlang.org)
- [Vue](https://vuejs.org)
- [Nuxt](https://nuxtjs.org)
- [Vite](https://vitejs.dev)
## **Developing**
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`.
0. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/.env.example) file found in the root of repository with your own keys and rename it to `.env`.
_Sample keys only work with the [production build](https://hoppscotch.io)._
@@ -315,9 +315,9 @@ docker run --rm --name hoppscotch -p 3000:3000 hoppscotch/hoppscotch:latest
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
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. 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`.
4. Update [`.env.example`](https://github.com/hoppscotch/hoppscotch/blob/main/.env.example) file found in the root of repository with your own keys and rename it to `.env`.
5. Build the release files with `pnpm run generate`.
6. Find the built project in `packages/hoppscotch-app/dist`. Host these files on any [static hosting servers](https://www.pluralsight.com/blog/software-development/where-to-host-your-jamstack-site).
6. Find the built project in `packages/hoppscotch-web/dist`. Host these files on any [static hosting servers](https://www.pluralsight.com/blog/software-development/where-to-host-your-jamstack-site).
## **Contributing**

View File

@@ -11,10 +11,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. **Checkout the `i18n` branch for latest translations.**
3. **Create a new branch for your translation with base branch `i18n`.**
4. **Create target language file in the [`locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-app/locales) directory.**
5. **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.**
4. **Create target language file in the [`/packages/hoppscotch-common/locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-common/locales) directory.**
5. **Copy the contents of the source file [`/packages/hoppscotch-common/locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/locales/en.json) to the target language file.**
6. **Translate the strings in the target language file.**
7. **Add your language entry to [`languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-app/languages.json).**
7. **Add your language entry to [`/packages/hoppscotch-common/languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/languages.json).**
8. **Save & commit changes.**
9. **Send a pull request.**

View File

@@ -20,4 +20,4 @@ services:
- "3000:3000"
environment:
HOST: 0.0.0.0
command: "npm run dev"
command: "pnpm run dev"

View File

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

View File

@@ -4,13 +4,13 @@
[build]
base = "/"
publish = "packages/hoppscotch-app/dist"
publish = "packages/hoppscotch-web/dist"
command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run generate"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Frame-Options = "SAMEORIGIN"
X-XSS-Protection = "1; mode=block"
[[redirects]]

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-app",
"version": "3.0.0",
"version": "3.0.1",
"description": "Open source API development ecosystem",
"author": "Hoppscotch (support@hoppscotch.io)",
"private": true,
@@ -10,12 +10,13 @@
"prepare": "husky install",
"dev": "pnpm -r do-dev",
"generate": "pnpm -r do-build-prod",
"start": "pnpm -r do-prod-start",
"start": "http-server packages/hoppscotch-web/dist -p 3000",
"lint": "pnpm -r do-lint",
"typecheck": "pnpm -r do-typecheck",
"lintfix": "pnpm -r do-lintfix",
"pre-commit": "pnpm -r do-lint && pnpm -r do-typecheck",
"test": "pnpm -r do-test"
"test": "pnpm -r do-test",
"generate-ui": "pnpm -r do-build-ui"
},
"workspaces": [
"./packages/*"
@@ -27,6 +28,7 @@
"devDependencies": {
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@types/node": "^17.0.24"
"@types/node": "^17.0.24",
"http-server": "^14.1.1"
}
}

View File

@@ -1,16 +0,0 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).

View File

@@ -1,13 +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"
>
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 00-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0020 4.77 5.07 5.07 0 0019.91 1S18.73.65 16 2.48a13.38 13.38 0 00-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 005 4.77a5.44 5.44 0 00-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 009 18.13V22" />
</svg>

Before

Width:  |  Height:  |  Size: 504 B

View File

@@ -1,667 +0,0 @@
{
"action": {
"autoscroll": "Autoscroll",
"cancel": "Cancel · lar",
"choose_file": "Trieu un fitxer",
"clear": "Clar",
"clear_all": "Esborra-ho tot",
"close": "Close",
"connect": "Connecteu",
"copy": "Copia",
"delete": "Suprimeix",
"disconnect": "Desconnecta",
"dismiss": "Destitueix",
"dont_save": "Don't save",
"download_file": "Descarrega l'arxiu",
"drag_to_reorder": "Drag to reorder",
"duplicate": "Duplicate",
"edit": "Edita",
"filter_response": "Filter response",
"go_back": "Torna",
"label": "Etiqueta",
"learn_more": "Aprèn més",
"less": "Less",
"more": "Més",
"new": "Novetat",
"no": "No",
"open_workspace": "Open workspace",
"paste": "Paste",
"prettify": "Prettify",
"remove": "Elimina",
"restore": "Restaura",
"save": "Desa",
"scroll_to_bottom": "Scroll to bottom",
"scroll_to_top": "Scroll to top",
"search": "Cerca",
"send": "Envia",
"start": "Començar",
"stop": "Atura",
"to_close": "to close",
"to_navigate": "to navigate",
"to_select": "to select",
"turn_off": "Tanca",
"turn_on": "Encendre",
"undo": "Desfés",
"yes": "Sí"
},
"add": {
"new": "Afegir nou",
"star": "Afegeix una estrella"
},
"app": {
"chat_with_us": "Xateja amb nosaltres",
"contact_us": "Poseu-vos en contacte amb nosaltres",
"copy": "Copia",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
"discord": "Discord",
"documentation": "Documentació",
"github": "GitHub",
"help": "Ajuda, comentaris i documentació",
"home": "Inici",
"invite": "Convidar",
"invite_description": "A Hoppscotch, hem dissenyat una interfície senzilla i intuïtiva per crear i gestionar les vostres API. Hoppscotch és una eina que us ajuda a construir, provar, documentar i compartir les vostres API.",
"invite_your_friends": "Convida els teus amics",
"join_discord_community": "Uniu-vos a la nostra comunitat Discord",
"keyboard_shortcuts": "Dreceres de teclat",
"name": "Hoppscotch",
"new_version_found": "S'ha trobat una nova versió. Actualitzeu per actualitzar.",
"options": "Options",
"proxy_privacy_policy": "Política de privadesa del servidor intermediari",
"reload": "Recarregar",
"search": "Cerca",
"share": "Compartir",
"shortcuts": "Dreceres",
"spotlight": "Destac",
"status": "Estat",
"status_description": "Check the status of the website",
"terms_and_privacy": "Condicions i privadesa",
"twitter": "Twitter",
"type_a_command_search": "Escriviu una ordre o cerqueu ...",
"we_use_cookies": "Utilitzem cookies",
"whats_new": "Que hi ha de nou?",
"wiki": "Wiki"
},
"auth": {
"account_exists": "El compte existeix amb credencials diferents: inicieu la sessió per enllaçar els dos comptes",
"all_sign_in_options": "Totes les opcions d'inici de sessió",
"continue_with_email": "Continueu amb el correu electrònic",
"continue_with_github": "Continueu amb GitHub",
"continue_with_google": "Continueu amb Google",
"continue_with_microsoft": "Continue with Microsoft",
"email": "Correu electrònic",
"logged_out": "Desconnectat",
"login": "iniciar Sessió",
"login_success": "S'ha iniciat la sessió correctament",
"login_to_hoppscotch": "Inicieu la sessió a Hoppscotch",
"logout": "Tancar sessió",
"re_enter_email": "Re-escriu el teu correu",
"send_magic_link": "Envia un enllaç màgic",
"sync": "Sincronitzar",
"we_sent_magic_link": "Us hem enviat un enllaç màgic!",
"we_sent_magic_link_description": "Comproveu la vostra safata d'entrada: hem enviat un correu electrònic a {email}. Conté un enllaç màgic que us permetrà iniciar la sessió."
},
"authorization": {
"generate_token": "Generar testimoni",
"include_in_url": "Inclou a l'URL",
"learn": "Aprèn com",
"pass_key_by": "Pass by",
"password": "Contrasenya",
"token": "Testimoni",
"type": "Tipus d'autorització",
"username": "Nom d'usuari"
},
"collection": {
"created": "S'ha creat la col·lecció",
"edit": "Edita la col·lecció",
"invalid_name": "Proporcioneu un nom vàlid per a la col·lecció",
"my_collections": "Les meves col·leccions",
"name": "La meva nova col·lecció",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Nova col · lecció",
"renamed": "S'ha canviat el nom de la col·lecció",
"request_in_use": "Request in use",
"save_as": "Guardar com",
"select": "Seleccioneu una col·lecció",
"select_location": "Seleccioneu la ubicació",
"select_team": "Selecciona un equip",
"team_collections": "Col·leccions per equips"
},
"confirm": {
"exit_team": "Are you sure you want to leave this team?",
"logout": "Esteu segur que voleu tancar la sessió?",
"remove_collection": "Esteu segur que voleu suprimir permanentment aquesta col·lecció?",
"remove_environment": "Esteu segur que voleu suprimir permanentment aquest entorn?",
"remove_folder": "Esteu segur que voleu suprimir definitivament aquesta carpeta?",
"remove_history": "Esteu segur que voleu suprimir definitivament tot l'historial?",
"remove_request": "Esteu segur que voleu suprimir definitivament aquesta sol·licitud?",
"remove_team": "Esteu segur que voleu suprimir aquest equip?",
"remove_telemetry": "Esteu segur que voleu desactivar Telemetry?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"sync": "Esteu segur que voleu sincronitzar aquest espai de treball?"
},
"count": {
"header": "Capçalera {count}",
"message": "{Compte} de missatges",
"parameter": "Paràmetre {count}",
"protocol": "Protocol {count}",
"value": "Valor {count}",
"variable": "Variable {count}"
},
"documentation": {
"generate": "Generar documentació",
"generate_message": "Importeu qualsevol col·lecció Hoppscotch per generar documentació de l'API sobre la marxa."
},
"empty": {
"authorization": "Aquesta sol·licitud no utilitza cap autorització",
"body": "Aquesta sol·licitud no té cap cos",
"collection": "La col·lecció està buida",
"collections": "Les col·leccions estan buides",
"documentation": "Connect to a GraphQL endpoint to view documentation",
"endpoint": "Endpoint cannot be empty",
"environments": "Els entorns estan buits",
"folder": "La carpeta està buida",
"headers": "Aquesta sol·licitud no té cap capçalera",
"history": "La història és buida",
"invites": "Invite list is empty",
"members": "L'equip està buit",
"parameters": "Aquesta sol·licitud no té cap paràmetre",
"pending_invites": "There are no pending invites for this team",
"profile": "Login in to view your profile",
"protocols": "Els protocols estan buits",
"schema": "Connecteu-vos a un punt final GraphQL",
"shortcodes": "Shortcodes are empty",
"team_name": "El nom de l'equip és buit",
"teams": "Els equips estan buits",
"tests": "No hi ha proves per a aquesta sol·licitud"
},
"environment": {
"add_to_global": "Add to Global",
"added": "Environment addition",
"create_new": "Crea un entorn nou",
"created": "Environment created",
"deleted": "Environment deletion",
"edit": "Edita l'entorn",
"invalid_name": "Proporcioneu un nom vàlid per a l'entorn",
"nested_overflow": "nested environment variables are limited to 10 levels",
"new": "Nou entorn",
"no_environment": "Sense entorn",
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
"select": "Seleccioneu un entorn",
"title": "Entorns",
"updated": "Environment updation",
"variable_list": "Llista de variables"
},
"error": {
"browser_support_sse": "Sembla que aquest navegador no és compatible amb els esdeveniments enviats per servidor.",
"check_console_details": "Consulteu el registre de la consola per obtenir més informació.",
"curl_invalid_format": "cURL no està formatat correctament",
"empty_req_name": "Nom de la sol·licitud buida",
"f12_details": "(F12 per obtenir més informació)",
"gql_prettify_invalid_query": "No s'ha pogut definir una consulta no vàlida, resoldre els errors de sintaxi de la consulta i tornar-ho a provar",
"incomplete_config_urls": "Incomplete configuration URLs",
"incorrect_email": "Incorrect email",
"invalid_link": "Invalid link",
"invalid_link_description": "The link you clicked is invalid or expired.",
"json_parsing_failed": "Invalid JSON",
"json_prettify_invalid_body": "No s'ha pogut personalitzar un cos no vàlid, resoldre errors de sintaxi json i tornar-ho a provar",
"network_error": "There seems to be a network error. Please try again.",
"network_fail": "No s'ha pogut enviar la sol·licitud",
"no_duration": "Sense durada",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"script_fail": "No s'ha pogut executar l'script de sol·licitud prèvia",
"something_went_wrong": "Alguna cosa ha anat malament",
"test_script_fail": "Could not execute post-request script"
},
"export": {
"as_json": "Exporta com a JSON",
"create_secret_gist": "Crea un resum secret",
"gist_created": "Gist creat",
"require_github": "Inicieu la sessió amb GitHub per crear un resum secret",
"title": "Export"
},
"folder": {
"created": "S'ha creat la carpeta",
"edit": "Edita la carpeta",
"invalid_name": "Proporcioneu un nom per a la carpeta",
"name_length_insufficient": "Folder name should be at least 3 characters long",
"new": "Carpeta nova",
"renamed": "S'ha canviat el nom de la carpeta"
},
"graphql": {
"mutations": "Mutacions",
"schema": "Esquema",
"subscriptions": "Subscripcions"
},
"header": {
"install_pwa": "Instal·la l'aplicació",
"login": "iniciar Sessió",
"save_workspace": "Desa el meu espai de treball"
},
"helpers": {
"authorization": "La capçalera de l'autorització es generarà automàticament quan envieu la sol·licitud.",
"generate_documentation_first": "Genereu documentació primer",
"network_fail": "No es pot arribar al punt final de l'API. Comproveu la connexió de xarxa i torneu-ho a provar.",
"offline": "Sembla que estàs fora de línia. És possible que les dades d'aquest espai de treball no estiguin actualitzades.",
"offline_short": "Sembla que estàs fora de línia.",
"post_request_tests": "Els scripts de prova s'escriuen en JavaScript i s'executen després de rebre la resposta.",
"pre_request_script": "Els scripts de sol·licitud prèvia s'escriuen en JavaScript i s'executen abans que s'enviï la sol·licitud.",
"script_fail": "Sembla que hi ha un error a l'script de sol·licitud prèvia. Comproveu l'error a continuació i solucioneu l'script en conseqüència.",
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
"tests": "Escriviu un script de prova per automatitzar la depuració."
},
"hide": {
"collection": "Collapse Collection Panel",
"more": "Amaga més",
"preview": "Amaga la previsualització",
"sidebar": "Amaga la barra lateral"
},
"import": {
"collections": "Importar col·leccions",
"curl": "Importa cURL",
"failed": "La importació ha fallat",
"from_gist": "Importa de Gist",
"from_gist_description": "Import from Gist URL",
"from_insomnia": "Import from Insomnia",
"from_insomnia_description": "Import from Insomnia collection",
"from_json": "Import from Hoppscotch",
"from_json_description": "Import from Hoppscotch collection file",
"from_my_collections": "Importa de Les meves col·leccions",
"from_my_collections_description": "Import from My Collections file",
"from_openapi": "Import from OpenAPI",
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection",
"from_url": "Import from URL",
"gist_url": "Introduïu l'URL Gist",
"import_from_url_invalid_fetch": "Couldn't get data from the url",
"import_from_url_invalid_file_format": "Error while importing collections",
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
"import_from_url_success": "Collections Imported",
"json_description": "Import collections from a Hoppscotch Collections JSON file",
"title": "Importació"
},
"layout": {
"collapse_collection": "Collapse or Expand Collections",
"collapse_sidebar": "Collapse or Expand the sidebar",
"column": "Vertical layout",
"name": "Layout",
"row": "Horizontal layout",
"zen_mode": "Mode Zen"
},
"modal": {
"collections": "Col·leccions",
"confirm": "Confirmeu",
"edit_request": "Sol·licitud d'edició",
"import_export": "Importació-exportació"
},
"mqtt": {
"communication": "Comunicació",
"log": "Registre",
"message": "Missatge",
"publish": "Publica",
"subscribe": "Subscriu-te",
"topic": "Tema",
"topic_name": "Nom del tema",
"topic_title": "Publicar / subscriure el tema",
"unsubscribe": "Cancel·lar la subscripció",
"url": "URL"
},
"navigation": {
"doc": "Documents",
"graphql": "GraphQL",
"profile": "Profile",
"realtime": "Temps real",
"rest": "REST",
"settings": "Configuració"
},
"preRequest": {
"javascript_code": "Codi JavaScript",
"learn": "Llegiu la documentació",
"script": "Script de sol·licitud prèvia",
"snippets": "Fragments"
},
"profile": {
"app_settings": "App Settings",
"editor": "Editor",
"editor_description": "Editors can add, edit, and delete requests.",
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
"no_permission": "You do not have permission to perform this action.",
"owner": "Owner",
"owner_description": "Owners can add, edit, and delete requests, collections and team members.",
"roles": "Roles",
"roles_description": "Roles are used to control access to the shared collections.",
"updated": "Profile updated",
"viewer": "Viewer",
"viewer_description": "Viewers can only view and use requests."
},
"remove": {
"star": "Elimina l'estrella"
},
"request": {
"added": "S'ha afegit la sol·licitud",
"authorization": "Autorització",
"body": "Organisme de sol·licitud",
"choose_language": "Tria l'idioma",
"content_type": "Tipus de contingut",
"content_type_titles": {
"others": "Others",
"structured": "Structured",
"text": "Text"
},
"copy_link": "Copia l'enllaç",
"duration": "Durada",
"enter_curl": "Introduïu cURL",
"generate_code": "Generar codi",
"generated_code": "Codi generat",
"header_list": "Llista de capçaleres",
"invalid_name": "Proporcioneu un nom per a la sol·licitud",
"method": "Mètode",
"name": "Sol·licita el nom",
"new": "New Request",
"override": "Override",
"override_help": "Set <xmp>Content-Type</xmp> in Headers",
"overriden": "Overridden",
"parameter_list": "Paràmetres de consulta",
"parameters": "Paràmetres",
"path": "Camí",
"payload": "Càrrega útil",
"query": "Consulta",
"raw_body": "Cos de sol·licitud en brut",
"renamed": "S'ha canviat el nom de la sol·licitud",
"run": "Correr",
"save": "Desa",
"save_as": "Guardar com",
"saved": "S'ha desat la sol·licitud",
"share": "Compartir",
"share_description": "Share Hoppscotch with your friends",
"title": "Sol·licitud",
"type": "Tipus de sol·licitud",
"url": "URL",
"variables": "Les variables",
"view_my_links": "View my links"
},
"response": {
"body": "Cos de resposta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Capçaleres",
"html": "HTML",
"image": "Imatge",
"json": "JSON",
"pdf": "PDF",
"preview_html": "Previsualitza HTML",
"raw": "Raw",
"size": "Mida",
"status": "Estat",
"time": "Temps",
"title": "Resposta",
"waiting_for_connection": "esperant la connexió",
"xml": "XML"
},
"settings": {
"accent_color": "Color d'accent",
"account": "Compte",
"account_description": "Personalitzeu la configuració del compte.",
"account_email_description": "La vostra adreça de correu electrònic principal.",
"account_name_description": "Aquest és el vostre nom visible.",
"background": "Antecedents",
"black_mode": "Negre",
"change_font_size": "Canvia la mida de la lletra",
"choose_language": "Tria l'idioma",
"dark_mode": "Fosc",
"expand_navigation": "Expand navigation",
"experiments": "Experiments",
"experiments_notice": "Es tracta d'una col·lecció d'experiments en què estem treballant que poden resultar útils, divertits, o ambdós. No són finals i potser no són estables, de manera que si passa alguna cosa massa estrany, no us espanteu. Només cal que apagueu la cosa dang. Bromes a part,",
"extension_ver_not_reported": "No s'ha informat",
"extension_version": "Versió d'extensió",
"extensions": "Extensions",
"extensions_use_toggle": "Utilitzeu l'extensió del navegador per enviar sol·licituds (si n'hi ha)",
"follow": "Follow Us",
"font_size": "Mida de la font",
"font_size_large": "Gran",
"font_size_medium": "Mitjà",
"font_size_small": "Petit",
"interceptor": "Interceptor",
"interceptor_description": "Middleware entre aplicació i API.",
"language": "Llenguatge",
"light_mode": "Llum",
"official_proxy_hosting": "El servidor intermediari oficial està allotjat per Hoppscotch.",
"profile": "Profile",
"profile_description": "Update your profile details",
"profile_email": "Email address",
"profile_name": "Profile name",
"proxy": "Servidor intermediari",
"proxy_url": "URL del servidor intermediari",
"proxy_use_toggle": "Utilitzeu el middleware del servidor intermediari per enviar sol·licituds",
"read_the": "Llegir el",
"reset_default": "Restableix el valor predeterminat",
"short_codes": "Short codes",
"short_codes_description": "Short codes which were created by you.",
"sidebar_on_left": "Sidebar on left",
"sync": "Sincronitza",
"sync_collections": "Col·leccions",
"sync_description": "Aquesta configuració se sincronitza amb el núvol.",
"sync_environments": "Entorns",
"sync_history": "Història",
"system_mode": "Sistema",
"telemetry": "Telemetria",
"telemetry_helps_us": "La telemetria ens ajuda a personalitzar les nostres operacions i oferir-vos la millor experiència.",
"theme": "Tema",
"theme_description": "Personalitzeu el tema de l'aplicació.",
"use_experimental_url_bar": "Utilitzeu la barra d'URL experimental amb ressaltat de l'entorn",
"user": "Usuari",
"verified_email": "Verified email",
"verify_email": "Verify email"
},
"shortcodes": {
"actions": "Actions",
"created_on": "Created on",
"deleted": "Shortcode deleted",
"method": "Method",
"not_found": "Shortcode not found",
"short_code": "Short code",
"url": "URL"
},
"shortcut": {
"general": {
"close_current_menu": "Tanca el menú actual",
"command_menu": "Menú de cerca i ordres",
"help_menu": "Menú d'ajuda",
"show_all": "Dreceres de teclat",
"title": "General"
},
"miscellaneous": {
"invite": "Convidar gent a Hoppscotch",
"title": "Divers"
},
"navigation": {
"back": "Torneu a la pàgina anterior",
"documentation": "Aneu a la pàgina Documentació",
"forward": "Aneu a la pàgina següent",
"graphql": "Aneu a la pàgina GraphQL",
"profile": "Go to Profile page",
"realtime": "Aneu a la pàgina en temps real",
"rest": "Aneu a la pàgina REST",
"settings": "Aneu a la pàgina Configuració",
"title": "Navegació"
},
"request": {
"copy_request_link": "Copia l'enllaç de la sol·licitud",
"delete_method": "Seleccioneu el mètode DELETE",
"get_method": "Seleccioneu el mètode GET",
"head_method": "Seleccioneu el mètode HEAD",
"method": "Mètode",
"next_method": "Seleccioneu Mètode següent",
"post_method": "Seleccioneu el mètode POST",
"previous_method": "Seleccioneu el mètode anterior",
"put_method": "Seleccioneu el mètode PUT",
"reset_request": "Sol·licitud de restabliment",
"save_to_collections": "Desa a les col·leccions",
"send_request": "Enviar sol.licitud",
"title": "Sol·licitud"
},
"theme": {
"black": "Switch theme to black mode",
"dark": "Switch theme to dark mode",
"light": "Switch theme to light mode",
"system": "Switch theme to system mode",
"title": "Theme"
}
},
"show": {
"code": "Mostra el codi",
"collection": "Expand Collection Panel",
"more": "Mostra més",
"sidebar": "Mostra la barra lateral"
},
"socketio": {
"communication": "Comunicació",
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
"event_name": "Nom de l'esdeveniment",
"events": "Esdeveniments",
"log": "Registre",
"url": "URL"
},
"sse": {
"event_type": "Tipus d'esdeveniment",
"log": "Registre",
"url": "URL"
},
"state": {
"bulk_mode": "Edició massiva",
"bulk_mode_placeholder": "Les entrades estan separades per una nova línia\nLes claus i els valors estan separats per:\nPrepara # a qualsevol fila que vulguis afegir, però continua desactivat",
"cleared": "Esborrat",
"connected": "Connectat",
"connected_to": "Connectat a {name}",
"connecting_to": "S'està connectant a {name} ...",
"connection_error": "Failed to connect",
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"copied_to_clipboard": "Copiat al porta-retalls",
"deleted": "Suprimit",
"deprecated": "DEPRECAT",
"disabled": "Desactivat",
"disconnected": "Desconnectat",
"disconnected_from": "Desconnectat de {name}",
"docs_generated": "Documentació generada",
"download_started": "S'ha iniciat la baixada",
"enabled": "Activat",
"file_imported": "Fitxer importat",
"finished_in": "Acabat en {duration} ms",
"history_deleted": "S'ha suprimit l'historial",
"linewrap": "Línies d'embolcall",
"loading": "S'està carregant ...",
"message_received": "Message: {message} arrived on topic: {topic}",
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
"none": "Cap",
"nothing_found": "No s'ha trobat res",
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
"published_message": "Published message: {message} to topic: {topic}",
"reconnection_error": "Failed to reconnect",
"subscribed_failed": "Failed to subscribe to topic: {topic}",
"subscribed_success": "Successfully subscribed to topic: {topic}",
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
"waiting_send_request": "S'està esperant l'enviament de la sol·licitud"
},
"support": {
"changelog": "Llegiu més sobre les darreres versions",
"chat": "Tens preguntes? Xateja amb nosaltres!",
"community": "Feu preguntes i ajudeu els altres",
"documentation": "Llegiu més sobre Hoppscotch",
"forum": "Feu preguntes i obteniu respostes",
"github": "Follow us on Github",
"shortcuts": "Navega per l'aplicació més ràpidament",
"team": "Poseu-vos en contacte amb l'equip",
"title": "Suport",
"twitter": "segueix-nos a Twitter"
},
"tab": {
"authorization": "Autorització",
"body": "Cos",
"collections": "Col·leccions",
"documentation": "Documentació",
"headers": "Capçaleres",
"history": "Història",
"mqtt": "MQTT",
"parameters": "Paràmetres",
"pre_request_script": "Script de sol·licitud prèvia",
"queries": "Consultes",
"query": "Consulta",
"schema": "Schema",
"socketio": "Socket.IO",
"sse": "SSE",
"tests": "Proves",
"types": "Tipus",
"variables": "Les variables",
"websocket": "WebSocket"
},
"team": {
"already_member": "You are already a member of this team. Contact your team owner.",
"create_new": "Crea un equip nou",
"deleted": "L'equip s'ha suprimit",
"edit": "Edita l'equip",
"email": "Correu electrònic",
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Sortiu de l'equip",
"exit_disabled": "L'únic propietari no pot sortir de l'equip",
"invalid_email_format": "El format del correu electrònic no és vàlid",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
"invalid_invite_link_description": "The link you followed is invalid. Contact your team owner.",
"invalid_member_permission": "Proporcioneu un permís vàlid al membre de l'equip",
"invite": "Invite",
"invite_more": "Invite more",
"invite_tooltip": "Invite people to this workspace",
"invited_to_team": "{owner} invited you to join {team}",
"join": "Invitation accepted",
"join_beta": "Uniu-vos al programa beta per accedir als equips.",
"join_team": "Join {team}",
"joined_team": "You have joined {team}",
"joined_team_description": "You are now a member of this team",
"left": "Vas deixar l'equip",
"login_to_continue": "Login to continue",
"login_to_continue_description": "You need to be logged in to join a team.",
"logout_and_try_again": "Logout and sign in with another account",
"member_has_invite": "This email ID already has an invite. Contact your team owner.",
"member_not_found": "Member not found. Contact your team owner.",
"member_removed": "S'ha eliminat l'usuari",
"member_role_updated": "Rols d'usuari actualitzats",
"members": "Membres",
"name_length_insufficient": "El nom de l'equip ha de tenir com a mínim 6 caràcters",
"name_updated": "Team name updated",
"new": "Nou equip",
"new_created": "S'ha creat un nou equip",
"new_name": "El meu nou equip",
"no_access": "No teniu accés d'edició a aquestes col·leccions",
"no_invite_found": "Invitation not found. Contact your team owner.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"pending_invites": "Pending invites",
"permissions": "Permisos",
"saved": "L'equip s'ha desat",
"select_a_team": "Select a team",
"title": "Equips",
"we_sent_invite_link": "We sent an invite link to all invitees!",
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
},
"test": {
"failed": "test failed",
"javascript_code": "Codi JavaScript",
"learn": "Llegiu la documentació",
"passed": "test passed",
"report": "Informe de prova",
"results": "Resultats de l'exàmen",
"script": "Guió",
"snippets": "Fragments"
},
"websocket": {
"communication": "Comunicació",
"log": "Registre",
"message": "Missatge",
"protocols": "Protocols",
"url": "URL"
}
}

View File

@@ -1,667 +0,0 @@
{
"action": {
"autoscroll": "Autoscroll",
"cancel": "Cancel",
"choose_file": "Choose a file",
"clear": "Clear",
"clear_all": "Clear all",
"close": "Close",
"connect": "Connect",
"copy": "Copy",
"delete": "Delete",
"disconnect": "Disconnect",
"dismiss": "Dismiss",
"dont_save": "Don't save",
"download_file": "Download file",
"drag_to_reorder": "Drag to reorder",
"duplicate": "Duplicate",
"edit": "Edit",
"filter_response": "Filter response",
"go_back": "Go back",
"label": "Label",
"learn_more": "Learn more",
"less": "Less",
"more": "More",
"new": "New",
"no": "No",
"open_workspace": "Open workspace",
"paste": "Paste",
"prettify": "Prettify",
"remove": "Remove",
"restore": "Restore",
"save": "Save",
"scroll_to_bottom": "Scroll to bottom",
"scroll_to_top": "Scroll to top",
"search": "Search",
"send": "Send",
"start": "Start",
"stop": "Stop",
"to_close": "to close",
"to_navigate": "to navigate",
"to_select": "to select",
"turn_off": "Turn off",
"turn_on": "Turn on",
"undo": "Undo",
"yes": "Yes"
},
"add": {
"new": "Add new",
"star": "Add star"
},
"app": {
"chat_with_us": "Chat with us",
"contact_us": "Contact us",
"copy": "Copy",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
"discord": "Discord",
"documentation": "Documentation",
"github": "GitHub",
"help": "Help & feedback",
"home": "Home",
"invite": "Invite",
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
"invite_your_friends": "Invite your friends",
"join_discord_community": "Join our Discord community",
"keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch",
"new_version_found": "New version found. Refresh to update.",
"options": "Options",
"proxy_privacy_policy": "Proxy privacy policy",
"reload": "Reload",
"search": "Search",
"share": "Share",
"shortcuts": "Shortcuts",
"spotlight": "Spotlight",
"status": "Status",
"status_description": "Check the status of the website",
"terms_and_privacy": "Terms and privacy",
"twitter": "Twitter",
"type_a_command_search": "Type a command or search…",
"we_use_cookies": "We use cookies",
"whats_new": "What's new?",
"wiki": "Wiki"
},
"auth": {
"account_exists": "Account exists with different credential - Login to link both accounts",
"all_sign_in_options": "All sign in options",
"continue_with_email": "Continue with Email",
"continue_with_github": "Continue with GitHub",
"continue_with_google": "Continue with Google",
"continue_with_microsoft": "Continue with Microsoft",
"email": "Email",
"logged_out": "Logged out",
"login": "Login",
"login_success": "Successfully logged in",
"login_to_hoppscotch": "Login to Hoppscotch",
"logout": "Logout",
"re_enter_email": "Re-enter email",
"send_magic_link": "Send a magic link",
"sync": "Sync",
"we_sent_magic_link": "We sent you a magic link!",
"we_sent_magic_link_description": "Check your inbox - we sent an email to {email}. It contains a magic link that will log you in."
},
"authorization": {
"generate_token": "Generate Token",
"include_in_url": "Include in URL",
"learn": "Learn how",
"pass_key_by": "Pass by",
"password": "Password",
"token": "Token",
"type": "Authorization Type",
"username": "Username"
},
"collection": {
"created": "Collection created",
"edit": "Edit Collection",
"invalid_name": "Please provide a name for the collection",
"my_collections": "My Collections",
"name": "My New Collection",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "New Collection",
"renamed": "Collection renamed",
"request_in_use": "Request in use",
"save_as": "Save as",
"select": "Select a Collection",
"select_location": "Select location",
"select_team": "Select a team",
"team_collections": "Team Collections"
},
"confirm": {
"exit_team": "Are you sure you want to leave this team?",
"logout": "Are you sure you want to logout?",
"remove_collection": "Are you sure you want to permanently delete this collection?",
"remove_environment": "Are you sure you want to permanently delete this environment?",
"remove_folder": "Are you sure you want to permanently delete this folder?",
"remove_history": "Are you sure you want to permanently delete all history?",
"remove_request": "Are you sure you want to permanently delete this request?",
"remove_team": "Are you sure you want to delete this team?",
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
},
"count": {
"header": "Header {count}",
"message": "Message {count}",
"parameter": "Parameter {count}",
"protocol": "Protocol {count}",
"value": "Value {count}",
"variable": "Variable {count}"
},
"documentation": {
"generate": "Generate documentation",
"generate_message": "Import any Hoppscotch collection to generate API documentation on-the-go."
},
"empty": {
"authorization": "This request does not use any authorization",
"body": "This request does not have a body",
"collection": "Collection is empty",
"collections": "Collections are empty",
"documentation": "Connect to a GraphQL endpoint to view documentation",
"endpoint": "Endpoint cannot be empty",
"environments": "Environments are empty",
"folder": "Folder is empty",
"headers": "This request does not have any headers",
"history": "History is empty",
"invites": "Invite list is empty",
"members": "Team is empty",
"parameters": "This request does not have any parameters",
"pending_invites": "There are no pending invites for this team",
"profile": "Login in to view your profile",
"protocols": "Protocols are empty",
"schema": "Connect to a GraphQL endpoint to view schema",
"shortcodes": "Shortcodes are empty",
"team_name": "Team name empty",
"teams": "You don't belong to any teams",
"tests": "There are no tests for this request"
},
"environment": {
"add_to_global": "Add to Global",
"added": "Environment addition",
"create_new": "Create new environment",
"created": "Environment created",
"deleted": "Environment deletion",
"edit": "Edit Environment",
"invalid_name": "Please provide a name for the environment",
"nested_overflow": "nested environment variables are limited to 10 levels",
"new": "New Environment",
"no_environment": "No environment",
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
"select": "Select environment",
"title": "Environments",
"updated": "Environment updated",
"variable_list": "Variable List"
},
"error": {
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
"check_console_details": "Check console log for details.",
"curl_invalid_format": "cURL is not formatted properly",
"empty_req_name": "Empty Request Name",
"f12_details": "(F12 for details)",
"gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
"incomplete_config_urls": "Incomplete configuration URLs",
"incorrect_email": "Incorrect email",
"invalid_link": "Invalid link",
"invalid_link_description": "The link you clicked is invalid or expired.",
"json_parsing_failed": "Invalid JSON",
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
"network_error": "There seems to be a network error. Please try again.",
"network_fail": "Could not send request",
"no_duration": "No duration",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script"
},
"export": {
"as_json": "Export as JSON",
"create_secret_gist": "Create secret Gist",
"gist_created": "Gist created",
"require_github": "Login with GitHub to create secret gist",
"title": "Export"
},
"folder": {
"created": "Folder created",
"edit": "Edit Folder",
"invalid_name": "Please provide a name for the folder",
"name_length_insufficient": "Folder name should be at least 3 characters long",
"new": "New Folder",
"renamed": "Folder renamed"
},
"graphql": {
"mutations": "Mutations",
"schema": "Schema",
"subscriptions": "Subscriptions"
},
"header": {
"install_pwa": "Install app",
"login": "Login",
"save_workspace": "Save My Workspace"
},
"helpers": {
"authorization": "The authorization header will be automatically generated when you send the request.",
"generate_documentation_first": "Generate documentation first",
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
"offline": "You seem to be offline. Data in this workspace might not be up to date.",
"offline_short": "You seem to be offline.",
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
"tests": "Write a test script to automate debugging."
},
"hide": {
"collection": "Collapse Collection Panel",
"more": "Hide more",
"preview": "Hide Preview",
"sidebar": "Collapse sidebar"
},
"import": {
"collections": "Import collections",
"curl": "Import cURL",
"failed": "Error while importing: format not recognized",
"from_gist": "Import from Gist",
"from_gist_description": "Import from Gist URL",
"from_insomnia": "Import from Insomnia",
"from_insomnia_description": "Import from Insomnia collection",
"from_json": "Import from Hoppscotch",
"from_json_description": "Import from Hoppscotch collection file",
"from_my_collections": "Import from My Collections",
"from_my_collections_description": "Import from My Collections file",
"from_openapi": "Import from OpenAPI",
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection",
"from_url": "Import from URL",
"gist_url": "Enter Gist URL",
"import_from_url_invalid_fetch": "Couldn't get data from the url",
"import_from_url_invalid_file_format": "Error while importing collections",
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
"import_from_url_success": "Collections Imported",
"json_description": "Import collections from a Hoppscotch Collections JSON file",
"title": "Import"
},
"layout": {
"collapse_collection": "Collapse or Expand Collections",
"collapse_sidebar": "Collapse or Expand the sidebar",
"column": "Vertical layout",
"name": "Layout",
"row": "Horizontal layout",
"zen_mode": "Zen mode"
},
"modal": {
"collections": "Collections",
"confirm": "Confirm",
"edit_request": "Edit Request",
"import_export": "Import / Export"
},
"mqtt": {
"communication": "Communication",
"log": "Log",
"message": "Message",
"publish": "Publish",
"subscribe": "Subscribe",
"topic": "Topic",
"topic_name": "Topic Name",
"topic_title": "Publish / Subscribe topic",
"unsubscribe": "Unsubscribe",
"url": "URL"
},
"navigation": {
"doc": "Docs",
"graphql": "GraphQL",
"profile": "Profile",
"realtime": "Realtime",
"rest": "REST",
"settings": "Settings"
},
"preRequest": {
"javascript_code": "JavaScript Code",
"learn": "Read documentation",
"script": "Pre-Request Script",
"snippets": "Snippets"
},
"profile": {
"app_settings": "App Settings",
"editor": "Editor",
"editor_description": "Editors can add, edit, and delete requests.",
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
"no_permission": "You do not have permission to perform this action.",
"owner": "Owner",
"owner_description": "Owners can add, edit, and delete requests, collections and team members.",
"roles": "Roles",
"roles_description": "Roles are used to control access to the shared collections.",
"updated": "Profile updated",
"viewer": "Viewer",
"viewer_description": "Viewers can only view and use requests."
},
"remove": {
"star": "Remove star"
},
"request": {
"added": "Request added",
"authorization": "Authorization",
"body": "Request Body",
"choose_language": "Choose language",
"content_type": "Content Type",
"content_type_titles": {
"others": "Others",
"structured": "Structured",
"text": "Text"
},
"copy_link": "Copy link",
"duration": "Duration",
"enter_curl": "Enter cURL",
"generate_code": "Generate code",
"generated_code": "Generated code",
"header_list": "Header List",
"invalid_name": "Please provide a name for the request",
"method": "Method",
"name": "Request name",
"new": "New Request",
"override": "Override",
"override_help": "Set <xmp>Content-Type</xmp> in Headers",
"overriden": "Overridden",
"parameter_list": "Query Parameters",
"parameters": "Parameters",
"path": "Path",
"payload": "Payload",
"query": "Query",
"raw_body": "Raw Request Body",
"renamed": "Request renamed",
"run": "Run",
"save": "Save",
"save_as": "Save as",
"saved": "Request saved",
"share": "Share",
"share_description": "Share Hoppscotch with your friends",
"title": "Request",
"type": "Request type",
"url": "URL",
"variables": "Variables",
"view_my_links": "View my links"
},
"response": {
"body": "Response Body",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Headers",
"html": "HTML",
"image": "Image",
"json": "JSON",
"pdf": "PDF",
"preview_html": "Preview HTML",
"raw": "Raw",
"size": "Size",
"status": "Status",
"time": "Time",
"title": "Response",
"waiting_for_connection": "waiting for connection",
"xml": "XML"
},
"settings": {
"accent_color": "Accent color",
"account": "アカウント",
"account_description": "Customize your account settings.",
"account_email_description": "Your primary email address.",
"account_name_description": "This is your display name.",
"background": "Background",
"black_mode": "Black",
"change_font_size": "Change font size",
"choose_language": "Choose language",
"dark_mode": "Dark",
"expand_navigation": "Expand navigation",
"experiments": "Experiments",
"experiments_notice": "This is a collection of experiments we're working on that might turn out to be useful, fun, both, or neither. They're not final and may not be stable, so if something overly weird happens, don't panic. Just turn the dang thing off. Jokes aside, ",
"extension_ver_not_reported": "Not Reported",
"extension_version": "Extension Version",
"extensions": "Browser extension",
"extensions_use_toggle": "Use the browser extension to send requests (if present)",
"follow": "Follow Us",
"font_size": "Font size",
"font_size_large": "Large",
"font_size_medium": "Medium",
"font_size_small": "Small",
"interceptor": "Interceptor",
"interceptor_description": "Middleware between application and APIs.",
"language": "Language",
"light_mode": "Light",
"official_proxy_hosting": "Official Proxy is hosted by Hoppscotch.",
"profile": "Profile",
"profile_description": "Update your profile details",
"profile_email": "Email address",
"profile_name": "Profile name",
"proxy": "Proxy",
"proxy_url": "Proxy URL",
"proxy_use_toggle": "Use the proxy middleware to send requests",
"read_the": "Read the",
"reset_default": "Reset to default",
"short_codes": "Short codes",
"short_codes_description": "Short codes which were created by you.",
"sidebar_on_left": "Sidebar on left",
"sync": "Synchronise",
"sync_collections": "Collections",
"sync_description": "These settings are synced to cloud.",
"sync_environments": "Environments",
"sync_history": "History",
"system_mode": "System",
"telemetry": "Telemetry",
"telemetry_helps_us": "Telemetry helps us to personalize our operations and deliver the best experience to you.",
"theme": "Theme",
"theme_description": "Customize your application theme.",
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting",
"user": "User",
"verified_email": "Verified email",
"verify_email": "Verify email"
},
"shortcodes": {
"actions": "Actions",
"created_on": "Created on",
"deleted": "Shortcode deleted",
"method": "Method",
"not_found": "Shortcode not found",
"short_code": "Short code",
"url": "URL"
},
"shortcut": {
"general": {
"close_current_menu": "Close current menu",
"command_menu": "Search & command menu",
"help_menu": "Help menu",
"show_all": "Keyboard shortcuts",
"title": "General"
},
"miscellaneous": {
"invite": "Invite people to Hoppscotch",
"title": "Miscellaneous"
},
"navigation": {
"back": "Go back to previous page",
"documentation": "Go to Documentation page",
"forward": "Go forward to next page",
"graphql": "Go to GraphQL page",
"profile": "Go to Profile page",
"realtime": "Go to Realtime page",
"rest": "Go to REST page",
"settings": "Go to Settings page",
"title": "Navigation"
},
"request": {
"copy_request_link": "Copy Request Link",
"delete_method": "Select DELETE method",
"get_method": "Select GET method",
"head_method": "Select HEAD method",
"method": "Method",
"next_method": "Select Next method",
"post_method": "Select POST method",
"previous_method": "Select Previous method",
"put_method": "Select PUT method",
"reset_request": "Reset Request",
"save_to_collections": "Save to Collections",
"send_request": "Send Request",
"title": "Request"
},
"theme": {
"black": "Switch theme to black mode",
"dark": "Switch theme to dark mode",
"light": "Switch theme to light mode",
"system": "Switch theme to system mode",
"title": "Theme"
}
},
"show": {
"code": "Show code",
"collection": "Expand Collection Panel",
"more": "Show more",
"sidebar": "Expand sidebar"
},
"socketio": {
"communication": "Communication",
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
"event_name": "Event Name",
"events": "Events",
"log": "Log",
"url": "URL"
},
"sse": {
"event_type": "Event type",
"log": "Log",
"url": "URL"
},
"state": {
"bulk_mode": "Bulk edit",
"bulk_mode_placeholder": "Entries are separated by newline\nKeys and values are separated by :\nPrepend # to any row you want to add but keep disabled",
"cleared": "Cleared",
"connected": "Connected",
"connected_to": "Connected to {name}",
"connecting_to": "Connecting to {name}...",
"connection_error": "Failed to connect",
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"copied_to_clipboard": "Copied to clipboard",
"deleted": "Deleted",
"deprecated": "DEPRECATED",
"disabled": "Disabled",
"disconnected": "Disconnected",
"disconnected_from": "Disconnected from {name}",
"docs_generated": "Documentation generated",
"download_started": "Download started",
"enabled": "Enabled",
"file_imported": "File imported",
"finished_in": "Finished in {duration} ms",
"history_deleted": "History deleted",
"linewrap": "Wrap lines",
"loading": "Loading...",
"message_received": "Message: {message} arrived on topic: {topic}",
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
"none": "None",
"nothing_found": "Nothing found for",
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
"published_message": "Published message: {message} to topic: {topic}",
"reconnection_error": "Failed to reconnect",
"subscribed_failed": "Failed to subscribe to topic: {topic}",
"subscribed_success": "Successfully subscribed to topic: {topic}",
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
"waiting_send_request": "Waiting to send request"
},
"support": {
"changelog": "Read more about latest releases",
"chat": "Questions? Chat with us!",
"community": "Ask questions and help others",
"documentation": "Read more about Hoppscotch",
"forum": "Ask questions and get answers",
"github": "Follow us on Github",
"shortcuts": "Browse app faster",
"team": "Get in touch with the team",
"title": "Support",
"twitter": "Follow us on Twitter"
},
"tab": {
"authorization": "Authorization",
"body": "Body",
"collections": "Collections",
"documentation": "Documentation",
"headers": "Headers",
"history": "History",
"mqtt": "MQTT",
"parameters": "Parameters",
"pre_request_script": "Pre-request Script",
"queries": "Queries",
"query": "Query",
"schema": "Schema",
"socketio": "Socket.IO",
"sse": "SSE",
"tests": "Tests",
"types": "Types",
"variables": "Variables",
"websocket": "WebSocket"
},
"team": {
"already_member": "You are already a member of this team. Contact your team owner.",
"create_new": "Create new team",
"deleted": "Team deleted",
"edit": "Edit Team",
"email": "E-mail",
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Exit Team",
"exit_disabled": "Only owner cannot exit the team",
"invalid_email_format": "Email format is invalid",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
"invalid_invite_link_description": "The link you followed is invalid. Contact your team owner.",
"invalid_member_permission": "Please provide a valid permission to the team member",
"invite": "Invite",
"invite_more": "Invite more",
"invite_tooltip": "Invite people to this workspace",
"invited_to_team": "{owner} invited you to join {team}",
"join": "Invitation accepted",
"join_beta": "Join the beta program to access teams.",
"join_team": "Join {team}",
"joined_team": "You have joined {team}",
"joined_team_description": "You are now a member of this team",
"left": "You left the team",
"login_to_continue": "Login to continue",
"login_to_continue_description": "You need to be logged in to join a team.",
"logout_and_try_again": "Logout and sign in with another account",
"member_has_invite": "This email ID already has an invite. Contact your team owner.",
"member_not_found": "Member not found. Contact your team owner.",
"member_removed": "User removed",
"member_role_updated": "User roles updated",
"members": "Members",
"name_length_insufficient": "Team name should be at least 6 characters long",
"name_updated": "Team name updated",
"new": "New Team",
"new_created": "New team created",
"new_name": "My New Team",
"no_access": "You do not have edit access to these collections",
"no_invite_found": "Invitation not found. Contact your team owner.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"pending_invites": "Pending invites",
"permissions": "Permissions",
"saved": "Team saved",
"select_a_team": "Select a team",
"title": "Teams",
"we_sent_invite_link": "We sent an invite link to all invitees!",
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
},
"test": {
"failed": "test failed",
"javascript_code": "JavaScript Code",
"learn": "Read documentation",
"passed": "test passed",
"report": "Test Report",
"results": "Test Results",
"script": "Script",
"snippets": "Snippets"
},
"websocket": {
"communication": "Communication",
"log": "Log",
"message": "Message",
"protocols": "Protocols",
"url": "URL"
}
}

View File

@@ -1,272 +0,0 @@
import AppAnnouncement from "./components/app/Announcement.vue";
import AppDeveloperOptions from "./components/app/DeveloperOptions.vue";
import AppFooter from "./components/app/Footer.vue";
import AppFuse from "./components/app/Fuse.vue";
import AppGitHubStarButton from "./components/app/GitHubStarButton.vue";
import AppHeader from "./components/app/Header.vue";
import AppInterceptor from "./components/app/Interceptor.vue";
import AppLogo from "./components/app/Logo.vue";
import AppOptions from "./components/app/Options.vue";
import AppPaneLayout from "./components/app/PaneLayout.vue";
import AppPowerSearch from "./components/app/PowerSearch.vue";
import AppPowerSearchEntry from "./components/app/PowerSearchEntry.vue";
import AppShare from "./components/app/Share.vue";
import AppShortcuts from "./components/app/Shortcuts.vue";
import AppShortcutsEntry from "./components/app/ShortcutsEntry.vue";
import AppSidenav from "./components/app/Sidenav.vue";
import AppSlideOver from "./components/app/SlideOver.vue";
import AppSupport from "./components/app/Support.vue";
import ButtonPrimary from "./components/button/Primary.vue";
import ButtonSecondary from "./components/button/Secondary.vue";
import CollectionsAdd from "./components/collections/Add.vue";
import CollectionsAddFolder from "./components/collections/AddFolder.vue";
import CollectionsAddRequest from "./components/collections/AddRequest.vue";
import CollectionsChooseType from "./components/collections/ChooseType.vue";
import CollectionsEdit from "./components/collections/Edit.vue";
import CollectionsEditFolder from "./components/collections/EditFolder.vue";
import CollectionsEditRequest from "./components/collections/EditRequest.vue";
import CollectionsImportExport from "./components/collections/ImportExport.vue";
import CollectionsSaveRequest from "./components/collections/SaveRequest.vue";
import CollectionsGraphqlAdd from "./components/collections/graphql/Add.vue";
import CollectionsGraphqlAddFolder from "./components/collections/graphql/AddFolder.vue";
import CollectionsGraphqlAddRequest from "./components/collections/graphql/AddRequest.vue";
import CollectionsGraphqlCollection from "./components/collections/graphql/Collection.vue";
import CollectionsGraphqlEdit from "./components/collections/graphql/Edit.vue";
import CollectionsGraphqlEditFolder from "./components/collections/graphql/EditFolder.vue";
import CollectionsGraphqlEditRequest from "./components/collections/graphql/EditRequest.vue";
import CollectionsGraphqlFolder from "./components/collections/graphql/Folder.vue";
import CollectionsGraphqlImportExport from "./components/collections/graphql/ImportExport.vue";
import CollectionsGraphqlRequest from "./components/collections/graphql/Request.vue";
import CollectionsGraphql from "./components/collections/graphql/index.vue";
import Collections from "./components/collections/index.vue";
import CollectionsMyCollection from "./components/collections/my/Collection.vue";
import CollectionsMyFolder from "./components/collections/my/Folder.vue";
import CollectionsMyRequest from "./components/collections/my/Request.vue";
import CollectionsTeamsCollection from "./components/collections/teams/Collection.vue";
import CollectionsTeamsFolder from "./components/collections/teams/Folder.vue";
import CollectionsTeamsRequest from "./components/collections/teams/Request.vue";
import DocsCollection from "./components/docs/Collection.vue";
import DocsFolder from "./components/docs/Folder.vue";
import DocsRequest from "./components/docs/Request.vue";
import EnvironmentsDetails from "./components/environments/Details.vue";
import EnvironmentsEnvironment from "./components/environments/Environment.vue";
import EnvironmentsImportExport from "./components/environments/ImportExport.vue";
import Environments from "./components/environments/index.vue";
import FirebaseLogin from "./components/firebase/Login.vue";
import FirebaseLogout from "./components/firebase/Logout.vue";
import GraphqlAuthorization from "./components/graphql/Authorization.vue";
import GraphqlField from "./components/graphql/Field.vue";
import GraphqlRequest from "./components/graphql/Request.vue";
import GraphqlRequestOptions from "./components/graphql/RequestOptions.vue";
import GraphqlResponse from "./components/graphql/Response.vue";
import GraphqlSidebar from "./components/graphql/Sidebar.vue";
import GraphqlType from "./components/graphql/Type.vue";
import GraphqlTypeLink from "./components/graphql/TypeLink.vue";
import HistoryGraphqlCard from "./components/history/graphql/Card.vue";
import History from "./components/history/index.vue";
import HistoryRestCard from "./components/history/rest/Card.vue";
import HttpAuthorization from "./components/http/Authorization.vue";
import HttpBody from "./components/http/Body.vue";
import HttpBodyParameters from "./components/http/BodyParameters.vue";
import HttpCodegenModal from "./components/http/CodegenModal.vue";
import HttpHeaders from "./components/http/Headers.vue";
import HttpImportCurl from "./components/http/ImportCurl.vue";
import HttpOAuth2Authorization from "./components/http/OAuth2Authorization.vue";
import HttpParameters from "./components/http/Parameters.vue";
import HttpPreRequestScript from "./components/http/PreRequestScript.vue";
import HttpRawBody from "./components/http/RawBody.vue";
import HttpReqChangeConfirmModal from "./components/http/ReqChangeConfirmModal.vue";
import HttpRequest from "./components/http/Request.vue";
import HttpRequestOptions from "./components/http/RequestOptions.vue";
import HttpResponse from "./components/http/Response.vue";
import HttpResponseMeta from "./components/http/ResponseMeta.vue";
import HttpSidebar from "./components/http/Sidebar.vue";
import HttpTestResult from "./components/http/TestResult.vue";
import HttpTestResultEntry from "./components/http/TestResultEntry.vue";
import HttpTestResultEnv from "./components/http/TestResultEnv.vue";
import HttpTestResultReport from "./components/http/TestResultReport.vue";
import HttpTests from "./components/http/Tests.vue";
import HttpURLEncodedParams from "./components/http/URLEncodedParams.vue";
import LensesHeadersRenderer from "./components/lenses/HeadersRenderer.vue";
import LensesHeadersRendererEntry from "./components/lenses/HeadersRendererEntry.vue";
import LensesResponseBodyRenderer from "./components/lenses/ResponseBodyRenderer.vue";
import LensesRenderersHTMLLensRenderer from "./components/lenses/renderers/HTMLLensRenderer.vue";
import LensesRenderersImageLensRenderer from "./components/lenses/renderers/ImageLensRenderer.vue";
import LensesRenderersJSONLensRenderer from "./components/lenses/renderers/JSONLensRenderer.vue";
import LensesRenderersPDFLensRenderer from "./components/lenses/renderers/PDFLensRenderer.vue";
import LensesRenderersRawLensRenderer from "./components/lenses/renderers/RawLensRenderer.vue";
import LensesRenderersXMLLensRenderer from "./components/lenses/renderers/XMLLensRenderer.vue";
import ProfilePicture from "./components/profile/Picture.vue";
import ProfileShortcode from "./components/profile/Shortcode.vue";
import RealtimeCommunication from "./components/realtime/Communication.vue";
import RealtimeLog from "./components/realtime/Log.vue";
import RealtimeLogEntry from "./components/realtime/LogEntry.vue";
import SmartAccentModePicker from "./components/smart/AccentModePicker.vue";
import SmartAnchor from "./components/smart/Anchor.vue";
import SmartAutoComplete from "./components/smart/AutoComplete.vue";
import SmartChangeLanguage from "./components/smart/ChangeLanguage.vue";
import SmartCheckbox from "./components/smart/Checkbox.vue";
import SmartColorModePicker from "./components/smart/ColorModePicker.vue";
import SmartConfirmModal from "./components/smart/ConfirmModal.vue";
import SmartEnvInput from "./components/smart/EnvInput.vue";
import SmartExpand from "./components/smart/Expand.vue";
import SmartFileChip from "./components/smart/FileChip.vue";
import SmartFontSizePicker from "./components/smart/FontSizePicker.vue";
import SmartIcon from "./components/smart/Icon.vue";
import SmartIntersection from "./components/smart/Intersection.vue";
import SmartItem from "./components/smart/Item.vue";
import SmartLoadingIndicator from "./components/smart/LoadingIndicator.vue";
import SmartModal from "./components/smart/Modal.vue";
import SmartProgressRing from "./components/smart/ProgressRing.vue";
import SmartRadio from "./components/smart/Radio.vue";
import SmartRadioGroup from "./components/smart/RadioGroup.vue";
import SmartSpinner from "./components/smart/Spinner.vue";
import SmartTab from "./components/smart/Tab.vue";
import SmartTabs from "./components/smart/Tabs.vue";
import SmartToggle from "./components/smart/Toggle.vue";
import TabPrimary from "./components/tab/Primary.vue";
import TabSecondary from "./components/tab/Secondary.vue";
import TeamsAdd from "./components/teams/Add.vue";
import TeamsEdit from "./components/teams/Edit.vue";
import TeamsInvite from "./components/teams/Invite.vue";
import TeamsModal from "./components/teams/Modal.vue";
import TeamsTeam from "./components/teams/Team.vue";
import Teams from "./components/teams/index.vue";
declare global {
interface __VLS_GlobalComponents {
AppAnnouncement: typeof AppAnnouncement;
AppDeveloperOptions: typeof AppDeveloperOptions;
AppFooter: typeof AppFooter;
AppFuse: typeof AppFuse;
AppGitHubStarButton: typeof AppGitHubStarButton;
AppHeader: typeof AppHeader;
AppInterceptor: typeof AppInterceptor;
AppLogo: typeof AppLogo;
AppOptions: typeof AppOptions;
AppPaneLayout: typeof AppPaneLayout;
AppPowerSearch: typeof AppPowerSearch;
AppPowerSearchEntry: typeof AppPowerSearchEntry;
AppShare: typeof AppShare;
AppShortcuts: typeof AppShortcuts;
AppShortcutsEntry: typeof AppShortcutsEntry;
AppSidenav: typeof AppSidenav;
AppSlideOver: typeof AppSlideOver;
AppSupport: typeof AppSupport;
ButtonPrimary: typeof ButtonPrimary;
ButtonSecondary: typeof ButtonSecondary;
CollectionsAdd: typeof CollectionsAdd;
CollectionsAddFolder: typeof CollectionsAddFolder;
CollectionsAddRequest: typeof CollectionsAddRequest;
CollectionsChooseType: typeof CollectionsChooseType;
CollectionsEdit: typeof CollectionsEdit;
CollectionsEditFolder: typeof CollectionsEditFolder;
CollectionsEditRequest: typeof CollectionsEditRequest;
CollectionsImportExport: typeof CollectionsImportExport;
CollectionsSaveRequest: typeof CollectionsSaveRequest;
CollectionsGraphqlAdd: typeof CollectionsGraphqlAdd;
CollectionsGraphqlAddFolder: typeof CollectionsGraphqlAddFolder;
CollectionsGraphqlAddRequest: typeof CollectionsGraphqlAddRequest;
CollectionsGraphqlCollection: typeof CollectionsGraphqlCollection;
CollectionsGraphqlEdit: typeof CollectionsGraphqlEdit;
CollectionsGraphqlEditFolder: typeof CollectionsGraphqlEditFolder;
CollectionsGraphqlEditRequest: typeof CollectionsGraphqlEditRequest;
CollectionsGraphqlFolder: typeof CollectionsGraphqlFolder;
CollectionsGraphqlImportExport: typeof CollectionsGraphqlImportExport;
CollectionsGraphqlRequest: typeof CollectionsGraphqlRequest;
CollectionsGraphql: typeof CollectionsGraphql;
Collections: typeof Collections;
CollectionsMyCollection: typeof CollectionsMyCollection;
CollectionsMyFolder: typeof CollectionsMyFolder;
CollectionsMyRequest: typeof CollectionsMyRequest;
CollectionsTeamsCollection: typeof CollectionsTeamsCollection;
CollectionsTeamsFolder: typeof CollectionsTeamsFolder;
CollectionsTeamsRequest: typeof CollectionsTeamsRequest;
DocsCollection: typeof DocsCollection;
DocsFolder: typeof DocsFolder;
DocsRequest: typeof DocsRequest;
EnvironmentsDetails: typeof EnvironmentsDetails;
EnvironmentsEnvironment: typeof EnvironmentsEnvironment;
EnvironmentsImportExport: typeof EnvironmentsImportExport;
Environments: typeof Environments;
FirebaseLogin: typeof FirebaseLogin;
FirebaseLogout: typeof FirebaseLogout;
GraphqlAuthorization: typeof GraphqlAuthorization;
GraphqlField: typeof GraphqlField;
GraphqlRequest: typeof GraphqlRequest;
GraphqlRequestOptions: typeof GraphqlRequestOptions;
GraphqlResponse: typeof GraphqlResponse;
GraphqlSidebar: typeof GraphqlSidebar;
GraphqlType: typeof GraphqlType;
GraphqlTypeLink: typeof GraphqlTypeLink;
HistoryGraphqlCard: typeof HistoryGraphqlCard;
History: typeof History;
HistoryRestCard: typeof HistoryRestCard;
HttpAuthorization: typeof HttpAuthorization;
HttpBody: typeof HttpBody;
HttpBodyParameters: typeof HttpBodyParameters;
HttpCodegenModal: typeof HttpCodegenModal;
HttpHeaders: typeof HttpHeaders;
HttpImportCurl: typeof HttpImportCurl;
HttpOAuth2Authorization: typeof HttpOAuth2Authorization;
HttpParameters: typeof HttpParameters;
HttpPreRequestScript: typeof HttpPreRequestScript;
HttpRawBody: typeof HttpRawBody;
HttpReqChangeConfirmModal: typeof HttpReqChangeConfirmModal;
HttpRequest: typeof HttpRequest;
HttpRequestOptions: typeof HttpRequestOptions;
HttpResponse: typeof HttpResponse;
HttpResponseMeta: typeof HttpResponseMeta;
HttpSidebar: typeof HttpSidebar;
HttpTestResult: typeof HttpTestResult;
HttpTestResultEntry: typeof HttpTestResultEntry;
HttpTestResultEnv: typeof HttpTestResultEnv;
HttpTestResultReport: typeof HttpTestResultReport;
HttpTests: typeof HttpTests;
HttpURLEncodedParams: typeof HttpURLEncodedParams;
LensesHeadersRenderer: typeof LensesHeadersRenderer;
LensesHeadersRendererEntry: typeof LensesHeadersRendererEntry;
LensesResponseBodyRenderer: typeof LensesResponseBodyRenderer;
LensesRenderersHTMLLensRenderer: typeof LensesRenderersHTMLLensRenderer;
LensesRenderersImageLensRenderer: typeof LensesRenderersImageLensRenderer;
LensesRenderersJSONLensRenderer: typeof LensesRenderersJSONLensRenderer;
LensesRenderersPDFLensRenderer: typeof LensesRenderersPDFLensRenderer;
LensesRenderersRawLensRenderer: typeof LensesRenderersRawLensRenderer;
LensesRenderersXMLLensRenderer: typeof LensesRenderersXMLLensRenderer;
ProfilePicture: typeof ProfilePicture;
ProfileShortcode: typeof ProfileShortcode;
RealtimeCommunication: typeof RealtimeCommunication;
RealtimeLog: typeof RealtimeLog;
RealtimeLogEntry: typeof RealtimeLogEntry;
SmartAccentModePicker: typeof SmartAccentModePicker;
SmartAnchor: typeof SmartAnchor;
SmartAutoComplete: typeof SmartAutoComplete;
SmartChangeLanguage: typeof SmartChangeLanguage;
SmartCheckbox: typeof SmartCheckbox;
SmartColorModePicker: typeof SmartColorModePicker;
SmartConfirmModal: typeof SmartConfirmModal;
SmartEnvInput: typeof SmartEnvInput;
SmartExpand: typeof SmartExpand;
SmartFileChip: typeof SmartFileChip;
SmartFontSizePicker: typeof SmartFontSizePicker;
SmartIcon: typeof SmartIcon;
SmartIntersection: typeof SmartIntersection;
SmartItem: typeof SmartItem;
SmartLoadingIndicator: typeof SmartLoadingIndicator;
SmartModal: typeof SmartModal;
SmartProgressRing: typeof SmartProgressRing;
SmartRadio: typeof SmartRadio;
SmartRadioGroup: typeof SmartRadioGroup;
SmartSpinner: typeof SmartSpinner;
SmartTab: typeof SmartTab;
SmartTabs: typeof SmartTabs;
SmartToggle: typeof SmartToggle;
TabPrimary: typeof TabPrimary;
TabSecondary: typeof TabSecondary;
TeamsAdd: typeof TeamsAdd;
TeamsEdit: typeof TeamsEdit;
TeamsInvite: typeof TeamsInvite;
TeamsModal: typeof TeamsModal;
TeamsTeam: typeof TeamsTeam;
Teams: typeof Teams;
}
}

View File

@@ -1,217 +0,0 @@
<template>
<div>
<header
class="flex items-center justify-between flex-1 px-2 py-2 overflow-x-auto overflow-y-hidden space-x-2"
>
<div class="inline-flex items-center space-x-2">
<ButtonSecondary
class="tracking-wide !font-bold !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark uppercase"
:label="t('app.name')"
to="/"
/>
<AppGitHubStarButton class="mt-1.5 transition <sm:hidden" />
</div>
<div class="inline-flex items-center space-x-2">
<ButtonSecondary
v-if="showInstallButton"
v-tippy="{ theme: 'tooltip' }"
:title="t('header.install_pwa')"
:icon="IconDownload"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="installPWA()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t('app.search')} <xmp>/</xmp>`"
:icon="IconSearch"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="invokeAction('modals.search.toggle')"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${
mdAndLarger ? t('support.title') : t('app.options')
} <xmp>?</xmp>`"
:icon="IconLifeBuoy"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="invokeAction('modals.support.toggle')"
/>
<ButtonSecondary
v-if="currentUser === null"
:icon="IconUploadCloud"
:label="t('header.save_workspace')"
filled
class="hidden md:flex"
@click="showLogin = true"
/>
<ButtonPrimary
v-if="currentUser === null"
:label="t('header.login')"
@click="showLogin = true"
/>
<div v-else class="inline-flex items-center space-x-2">
<ButtonPrimary
v-tippy="{ theme: 'tooltip' }"
:title="t('team.invite_tooltip')"
:label="t('team.invite')"
:icon="IconUserPlus"
class="!bg-green-500 !bg-opacity-15 !text-green-500 !hover:bg-opacity-10 !hover:bg-green-400 !hover:text-green-600"
@click="showTeamsModal = true"
/>
<span class="px-2">
<tippy
interactive
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<ProfilePicture
v-if="currentUser.photoURL"
v-tippy="{
theme: 'tooltip',
}"
:url="currentUser.photoURL"
:alt="currentUser.displayName"
:title="currentUser.displayName"
indicator
:indicator-styles="
network.isOnline ? 'bg-green-500' : 'bg-red-500'
"
/>
<ProfilePicture
v-else
v-tippy="{ theme: 'tooltip' }"
:title="currentUser.displayName"
:initial="currentUser.displayName"
indicator
:indicator-styles="
network.isOnline ? 'bg-green-500' : 'bg-red-500'
"
/>
<template #content="{ hide }">
<div class="flex flex-col px-2 text-tiny">
<span class="inline-flex font-semibold truncate">
{{ currentUser.displayName }}
</span>
<span class="inline-flex truncate text-secondaryLight">
{{ currentUser.email }}
</span>
</div>
<hr />
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.enter="profile.$el.click()"
@keyup.s="settings.$el.click()"
@keyup.l="logout.$el.click()"
@keyup.escape="hide()"
>
<SmartItem
ref="profile"
to="/profile"
:icon="IconUser"
:label="t('navigation.profile')"
:shortcut="['↩']"
@click="hide()"
/>
<SmartItem
ref="settings"
to="/settings"
:icon="IconSettings"
:label="t('navigation.settings')"
:shortcut="['S']"
@click="hide()"
/>
<FirebaseLogout
ref="logout"
:shortcut="['L']"
@confirm-logout="hide()"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
</header>
<AppAnnouncement v-if="!network.isOnline" />
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue"
import IconUser from "~icons/lucide/user"
import IconSettings from "~icons/lucide/settings"
import IconDownload from "~icons/lucide/download"
import IconSearch from "~icons/lucide/search"
import IconLifeBuoy from "~icons/lucide/life-buoy"
import IconUploadCloud from "~icons/lucide/upload-cloud"
import IconUserPlus from "~icons/lucide/user-plus"
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
import { probableUser$ } from "@helpers/fb/auth"
import { getLocalConfig, setLocalConfig } from "~/newstore/localpersistence"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useReadonlyStream } from "@composables/stream"
import { invokeAction } from "@helpers/actions"
const t = useI18n()
const toast = useToast()
/**
* Once the PWA code is initialized, this holds a method
* that can be called to show the user the installation
* prompt.
*/
const showInstallButton = computed(() => !!pwaDefferedPrompt.value)
const showLogin = ref(false)
const showTeamsModal = ref(false)
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
const network = reactive(useNetwork())
const currentUser = useReadonlyStream(probableUser$, null)
onMounted(() => {
const cookiesAllowed = getLocalConfig("cookiesAllowed") === "yes"
if (!cookiesAllowed) {
toast.show(`${t("app.we_use_cookies")}`, {
duration: 0,
action: [
{
text: `${t("action.learn_more")}`,
onClick: (_, toastObject) => {
setLocalConfig("cookiesAllowed", "yes")
toastObject.goAway(0)
window.open("https://docs.hoppscotch.io/privacy", "_blank")?.focus()
},
},
{
text: `${t("action.dismiss")}`,
onClick: (_, toastObject) => {
setLocalConfig("cookiesAllowed", "yes")
toastObject.goAway(0)
},
},
],
})
}
})
// Template refs
const tippyActions = ref<any | null>(null)
const profile = ref<any | null>(null)
const settings = ref<any | null>(null)
const logout = ref<any | null>(null)
</script>

View File

@@ -1,97 +0,0 @@
<template>
<AppSlideOver :show="show" @close="close()">
<template #content>
<div class="sticky top-0 z-10 flex flex-col bg-primary">
<div
class="flex items-center justify-between p-2 border-b border-dividerLight"
>
<h3 class="ml-4 heading">{{ t("app.shortcuts") }}</h3>
<ButtonSecondary :icon="IconX" @click="close()" />
</div>
<div class="flex flex-col px-6 py-4 border-b border-dividerLight">
<input
v-model="filterText"
type="search"
autocomplete="off"
class="flex px-4 py-2 border rounded bg-primaryLight border-dividerLight focus-visible:border-divider"
:placeholder="`${t('action.search')}`"
/>
</div>
</div>
<div v-if="filterText" class="flex flex-col divide-y divide-dividerLight">
<div
v-for="(map, mapIndex) in searchResults"
:key="`map-${mapIndex}`"
class="px-6 py-4 space-y-4"
>
<h1 class="font-semibold text-secondaryDark">
{{ t(map.item.section) }}
</h1>
<AppShortcutsEntry
v-for="(shortcut, index) in map.item.shortcuts"
:key="`shortcut-${index}`"
:shortcut="shortcut"
/>
</div>
<div
v-if="searchResults.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
<span class="my-2 text-center">
{{ t("state.nothing_found") }} "{{ filterText }}"
</span>
</div>
</div>
<div v-else class="flex flex-col divide-y divide-dividerLight">
<div
v-for="(map, mapIndex) in mappings"
:key="`map-${mapIndex}`"
class="px-6 py-4 space-y-4"
>
<h1 class="font-semibold text-secondaryDark">
{{ t(map.section) }}
</h1>
<AppShortcutsEntry
v-for="(shortcut, shortcutIndex) in map.shortcuts"
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
:shortcut="shortcut"
/>
</div>
</div>
</template>
</AppSlideOver>
</template>
<script setup lang="ts">
import IconX from "~icons/lucide/x"
import { computed, ref } from "vue"
import Fuse from "fuse.js"
import mappings from "~/helpers/shortcuts"
import { useI18n } from "@composables/i18n"
const t = useI18n()
defineProps<{
show: boolean
}>()
const options = {
keys: ["shortcuts.label"],
}
const fuse = new Fuse(mappings, options)
const filterText = ref("")
const searchResults = computed(() => fuse.search(filterText.value))
const emit = defineEmits<{
(e: "close"): void
}>()
const close = () => {
filterText.value = ""
emit("close")
}
</script>

View File

@@ -1,81 +0,0 @@
<template>
<SmartModal
v-if="show"
dialog
:title="t('collection.new')"
@close="hideModal"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelAdd"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addNewCollection"
/>
<label for="selectLabelAdd">
{{ t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span class="flex">
<ButtonPrimary
:label="t('action.save')"
:loading="loadingState"
@click="addNewCollection"
/>
<ButtonSecondary :label="t('action.cancel')" @click="hideModal" />
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
export default defineComponent({
props: {
show: Boolean,
loadingState: Boolean,
},
emits: ["submit", "hide-modal"],
setup() {
return {
toast: useToast(),
t: useI18n(),
}
},
data() {
return {
name: null,
}
},
watch: {
show(isShowing: boolean) {
if (!isShowing) {
this.name = null
}
},
},
methods: {
addNewCollection() {
if (!this.name) {
this.toast.error(this.t("collection.invalid_name"))
return
}
this.$emit("submit", this.name)
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -1,86 +0,0 @@
<template>
<SmartModal
v-if="show"
dialog
:title="t('folder.new')"
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelAddFolder"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addFolder"
/>
<label for="selectLabelAddFolder">
{{ t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span class="flex">
<ButtonPrimary
:label="t('action.save')"
:loading="loadingState"
@click="addFolder"
/>
<ButtonSecondary :label="t('action.cancel')" @click="hideModal" />
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
export default defineComponent({
props: {
show: Boolean,
folder: { type: Object, default: () => ({}) },
folderPath: { type: String, default: null },
collectionIndex: { type: Number, default: null },
loadingState: Boolean,
},
emits: ["hide-modal", "add-folder"],
setup() {
return {
toast: useToast(),
t: useI18n(),
}
},
data() {
return {
name: null,
}
},
watch: {
show(isShowing: boolean) {
if (!isShowing) this.name = null
},
},
methods: {
addFolder() {
if (!this.name) {
this.toast.error(this.t("folder.invalid_name"))
return
}
this.$emit("add-folder", {
name: this.name,
folder: this.folder,
path: this.folderPath || `${this.collectionIndex}`,
})
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -1,149 +0,0 @@
<template>
<div v-show="show">
<SmartTabs
:id="'collections_tab'"
v-model="selectedCollectionTab"
render-inactive-tabs
>
<SmartTab
:id="'my-collections'"
:label="`${t('collection.my_collections')}`"
/>
<SmartTab
v-if="currentUser"
:id="'team-collections'"
:label="`${t('collection.team_collections')}`"
>
<SmartIntersection @intersecting="onTeamSelectIntersect">
<tippy
interactive
trigger="click"
theme="popover"
arrow
placement="bottom"
>
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('collection.select_team')}`"
class="bg-transparent border-b border-dividerLight select-wrapper"
>
<ButtonSecondary
v-if="collectionsType.selectedTeam"
:icon="IconUsers"
:label="collectionsType.selectedTeam.name"
class="flex-1 !justify-start pr-8 rounded-none"
/>
<ButtonSecondary
v-else
:label="`${t('collection.select_team')}`"
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
class="flex flex-col"
tabindex="0"
role="menu"
@keyup.escape="hide()"
>
<SmartItem
v-for="(team, index) in myTeams"
:key="`team-${index}`"
:label="team.name"
:info-icon="
team.id === collectionsType.selectedTeam?.id
? IconDone
: null
"
:active-info-icon="
team.id === collectionsType.selectedTeam?.id
"
:icon="IconUsers"
@click="
() => {
updateSelectedTeam(team)
hide()
}
"
/>
</div>
</template>
</tippy>
</SmartIntersection>
</SmartTab>
</SmartTabs>
</div>
</template>
<script setup lang="ts">
import IconUsers from "~icons/lucide/users"
import IconDone from "~icons/lucide/check"
import { ref, watch } from "vue"
import { GetMyTeamsQuery, Team } from "~/helpers/backend/graphql"
import { currentUserInfo$ } from "~/helpers/teams/BackendUserInfo"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useReadonlyStream } from "@composables/stream"
import { onLoggedIn } from "@composables/auth"
import { useI18n } from "@composables/i18n"
import { useLocalState } from "~/newstore/localstate"
type TeamData = GetMyTeamsQuery["myTeams"][number]
type CollectionTabs = "my-collections" | "team-collections"
const t = useI18n()
const selectedCollectionTab = ref<CollectionTabs>("my-collections")
defineProps<{
show: boolean
collectionsType: {
type: "my-collections" | "team-collections"
selectedTeam: Team | undefined
}
}>()
const emit = defineEmits<{
(e: "update-collection-type", tabID: string): void
(e: "update-selected-team", team: TeamData | undefined): void
}>()
const currentUser = useReadonlyStream(currentUserInfo$, null)
const adapter = new TeamListAdapter(true)
const myTeams = useReadonlyStream(adapter.teamList$, null)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
let teamListFetched = false
watch(myTeams, (teams) => {
if (teams && !teamListFetched) {
teamListFetched = true
if (REMEMBERED_TEAM_ID.value && currentUser) {
const team = teams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) updateSelectedTeam(team)
}
}
})
onLoggedIn(() => {
adapter.initialize()
})
const onTeamSelectIntersect = () => {
// Load team data as soon as intersection
adapter.fetchList()
}
const updateCollectionsType = (tabID: string) => {
emit("update-collection-type", tabID)
}
const updateSelectedTeam = (team: TeamData | undefined) => {
REMEMBERED_TEAM_ID.value = team?.id
emit("update-selected-team", team)
}
watch(selectedCollectionTab, (newValue: string) => {
updateCollectionsType(newValue)
})
</script>

View File

@@ -1,80 +0,0 @@
<template>
<SmartModal
v-if="show"
dialog
:title="t('collection.edit')"
@close="hideModal"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelEdit"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveCollection"
/>
<label for="selectLabelEdit">
{{ t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span class="flex">
<ButtonPrimary
:label="t('action.save')"
:loading="loadingState"
@click="saveCollection"
/>
<ButtonSecondary :label="t('action.cancel')" @click="hideModal" />
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
export default defineComponent({
props: {
show: Boolean,
editingCollectionName: { type: String, default: null },
loadingState: Boolean,
},
emits: ["submit", "hide-modal"],
setup() {
return {
toast: useToast(),
t: useI18n(),
}
},
data() {
return {
name: null,
}
},
watch: {
editingCollectionName(val) {
this.name = val
},
},
methods: {
saveCollection() {
if (!this.name) {
this.toast.error(this.t("collection.invalid_name"))
return
}
this.$emit("submit", this.name)
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -1,80 +0,0 @@
<template>
<SmartModal
v-if="show"
dialog
:title="t('folder.edit')"
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelEditFolder"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="editFolder"
/>
<label for="selectLabelEditFolder">
{{ t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span class="flex">
<ButtonPrimary
:label="t('action.save')"
:loading="loadingState"
@click="editFolder"
/>
<ButtonSecondary :label="t('action.cancel')" @click="hideModal" />
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
export default defineComponent({
props: {
show: Boolean,
editingFolderName: { type: String, default: null },
loadingState: Boolean,
},
emits: ["submit", "hide-modal"],
setup() {
return {
t: useI18n(),
toast: useToast(),
}
},
data() {
return {
name: null,
}
},
watch: {
editingFolderName(val) {
this.name = val
},
},
methods: {
editFolder() {
if (!this.name) {
this.toast.error(this.t("folder.invalid_name"))
return
}
this.$emit("submit", this.name)
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -1,82 +0,0 @@
<template>
<SmartModal
v-if="show"
dialog
:title="t('modal.edit_request')"
@close="hideModal"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelEditReq"
v-model="requestUpdateData.name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveRequest"
/>
<label for="selectLabelEditReq">
{{ t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span class="flex">
<ButtonPrimary
:label="t('action.save')"
:loading="loadingState"
@click="saveRequest"
/>
<ButtonSecondary :label="t('action.cancel')" @click="hideModal" />
</span>
</template>
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
export default defineComponent({
props: {
show: Boolean,
editingRequestName: { type: String, default: null },
loadingState: Boolean,
},
emits: ["submit", "hide-modal"],
setup() {
return {
t: useI18n(),
toast: useToast(),
}
},
data() {
return {
requestUpdateData: {
name: null,
},
}
},
watch: {
editingRequestName(val) {
this.requestUpdateData.name = val
},
},
methods: {
saveRequest() {
if (!this.requestUpdateData.name) {
this.toast.error(this.t("request.invalid_name"))
return
}
this.$emit("submit", this.requestUpdateData)
},
hideModal() {
this.requestUpdateData = { name: null }
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -1,391 +0,0 @@
<template>
<SmartModal
v-if="show"
dialog
:title="`${t('collection.save_as')}`"
@close="hideModal"
>
<template #body>
<div class="flex flex-col">
<div class="relative flex">
<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="p-4">
{{ t("collection.select_location") }}
</label>
<CollectionsGraphql
v-if="mode === 'graphql'"
:show-coll-actions="false"
:picked="picked"
:saving-mode="true"
@select="onSelect"
/>
<Collections
v-else
:picked="picked"
:save-request="true"
@select="onSelect"
@update-collection="updateColl"
@update-coll-type="onUpdateCollType"
/>
</div>
</template>
<template #footer>
<span class="flex">
<ButtonPrimary :label="`${t('action.save')}`" @click="saveRequestAs" />
<ButtonSecondary :label="`${t('action.cancel')}`" @click="hideModal" />
</span>
</template>
</SmartModal>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from "vue"
import * as E from "fp-ts/Either"
import { HoppGQLRequest, isHoppRESTRequest } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es"
import {
editGraphqlRequest,
editRESTRequest,
saveGraphqlRequestAs,
saveRESTRequestAs,
} from "~/newstore/collections"
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
import {
getRESTRequest,
setRESTSaveContext,
useRESTRequestName,
} from "~/newstore/RESTSession"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { runMutation } from "~/helpers/backend/GQLClient"
import {
CreateRequestInCollectionDocument,
UpdateRequestDocument,
} from "~/helpers/backend/graphql"
const t = useI18n()
type CollectionType =
| {
type: "my-collections"
}
| {
type: "team-collections"
// TODO: Figure this type out
selectedTeam: {
id: string
}
}
type Picked =
| {
pickedType: "my-request"
folderPath: string
requestIndex: number
}
| {
pickedType: "my-folder"
folderPath: string
}
| {
pickedType: "my-collection"
collectionIndex: number
}
| {
pickedType: "teams-request"
requestID: string
}
| {
pickedType: "teams-folder"
folderID: string
}
| {
pickedType: "teams-collection"
collectionID: string
}
| {
pickedType: "gql-my-request"
folderPath: string
requestIndex: number
}
| {
pickedType: "gql-my-folder"
folderPath: string
}
| {
pickedType: "gql-my-collection"
collectionIndex: number
}
const props = defineProps<{
mode: "rest" | "graphql"
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const toast = useToast()
// TODO: Use a better implementation with computed ?
// This implementation can't work across updates to mode prop (which won't happen tho)
const requestName =
props.mode === "rest" ? useRESTRequestName() : useGQLRequestName()
const requestData = reactive({
name: requestName,
collectionIndex: undefined as number | undefined,
folderName: undefined as number | undefined,
requestIndex: undefined as number | undefined,
})
const collectionsType = ref<CollectionType>({
type: "my-collections",
})
// TODO: Figure this type out
const picked = ref<Picked | null>(null)
// Resets
watch(
() => requestData.collectionIndex,
() => {
requestData.folderName = undefined
requestData.requestIndex = undefined
}
)
watch(
() => requestData.folderName,
() => {
requestData.requestIndex = undefined
}
)
// All the methods
const onUpdateCollType = (newCollType: CollectionType) => {
collectionsType.value = newCollType
}
const onSelect = ({ picked: pickedVal }: { picked: Picked | null }) => {
picked.value = pickedVal
}
const hideModal = () => {
picked.value = null
emit("hide-modal")
}
const saveRequestAs = async () => {
if (!requestName.value) {
toast.error(`${t("error.empty_req_name")}`)
return
}
if (picked.value === null) {
toast.error(`${t("collection.select")}`)
return
}
// Clone Deep because objects are shared by reference so updating
// just one bit will update other referenced shared instances
const requestUpdated =
props.mode === "rest"
? cloneDeep(getRESTRequest())
: cloneDeep(getGQLSession().request)
// // 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 (picked.value.pickedType === "my-request") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
editRESTRequest(
picked.value.folderPath,
picked.value.requestIndex,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: picked.value.folderPath,
requestIndex: picked.value.requestIndex,
req: cloneDeep(requestUpdated),
})
requestSaved()
} else if (picked.value.pickedType === "my-folder") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
const insertionIndex = saveRESTRequestAs(
picked.value.folderPath,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: picked.value.folderPath,
requestIndex: insertionIndex,
req: cloneDeep(requestUpdated),
})
requestSaved()
} else if (picked.value.pickedType === "my-collection") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
const insertionIndex = saveRESTRequestAs(
`${picked.value.collectionIndex}`,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: `${picked.value.collectionIndex}`,
requestIndex: insertionIndex,
req: cloneDeep(requestUpdated),
})
requestSaved()
} else if (picked.value.pickedType === "teams-request") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
if (collectionsType.value.type !== "team-collections")
throw new Error("Collections Type mismatch")
runMutation(UpdateRequestDocument, {
requestID: picked.value.requestID,
data: {
request: JSON.stringify(requestUpdated),
title: requestUpdated.name,
},
})().then((result) => {
if (E.isLeft(result)) {
toast.error(`${t("profile.no_permission")}`)
throw new Error(`${result.left}`)
} else {
requestSaved()
}
})
setRESTSaveContext({
originLocation: "team-collection",
requestID: picked.value.requestID,
req: cloneDeep(requestUpdated),
})
} else if (picked.value.pickedType === "teams-folder") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
if (collectionsType.value.type !== "team-collections")
throw new Error("Collections Type mismatch")
const result = await runMutation(CreateRequestInCollectionDocument, {
collectionID: picked.value.folderID,
data: {
request: JSON.stringify(requestUpdated),
teamID: collectionsType.value.selectedTeam.id,
title: requestUpdated.name,
},
})()
if (E.isLeft(result)) {
toast.error(`${t("profile.no_permission")}`)
console.error(result.left)
} else {
setRESTSaveContext({
originLocation: "team-collection",
requestID: result.right.createRequestInCollection.id,
teamID: collectionsType.value.selectedTeam.id,
collectionID: picked.value.folderID,
req: cloneDeep(requestUpdated),
})
requestSaved()
}
} else if (picked.value.pickedType === "teams-collection") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
if (collectionsType.value.type !== "team-collections")
throw new Error("Collections Type mismatch")
const result = await runMutation(CreateRequestInCollectionDocument, {
collectionID: picked.value.collectionID,
data: {
title: requestUpdated.name,
request: JSON.stringify(requestUpdated),
teamID: collectionsType.value.selectedTeam.id,
},
})()
if (E.isLeft(result)) {
toast.error(`${t("profile.no_permission")}`)
console.error(result.left)
} else {
setRESTSaveContext({
originLocation: "team-collection",
requestID: result.right.createRequestInCollection.id,
teamID: collectionsType.value.selectedTeam.id,
collectionID: picked.value.collectionID,
req: cloneDeep(requestUpdated),
})
requestSaved()
}
} else if (picked.value.pickedType === "gql-my-request") {
// TODO: Check for GQL request ?
editGraphqlRequest(
picked.value.folderPath,
picked.value.requestIndex,
requestUpdated as HoppGQLRequest
)
requestSaved()
} else if (picked.value.pickedType === "gql-my-folder") {
// TODO: Check for GQL request ?
saveGraphqlRequestAs(
picked.value.folderPath,
requestUpdated as HoppGQLRequest
)
requestSaved()
} else if (picked.value.pickedType === "gql-my-collection") {
// TODO: Check for GQL request ?
saveGraphqlRequestAs(
`${picked.value.collectionIndex}`,
requestUpdated as HoppGQLRequest
)
requestSaved()
}
}
const requestSaved = () => {
toast.success(`${t("request.added")}`)
hideModal()
}
const updateColl = (ev: CollectionType["type"]) => {
collectionsType.value.type = ev
}
</script>

View File

@@ -1,959 +0,0 @@
<template>
<div :class="{ 'rounded border border-divider': saveRequest }">
<div
class="sticky z-10 flex flex-col border-b rounded-t bg-primary border-dividerLight"
:style="
saveRequest ? 'top: calc(-1.35 * var(--font-size-body))' : 'top: 0'
"
>
<div class="flex flex-col border-b border-dividerLight">
<input
v-model="filterText"
type="search"
autocomplete="off"
:placeholder="t('action.search')"
class="py-2 pl-4 pr-2 bg-transparent"
:disabled="collectionsType.type == 'team-collections'"
/>
</div>
<CollectionsChooseType
:collections-type="collectionsType"
:show="showTeamCollections"
@update-collection-type="updateCollectionType"
@update-selected-team="updateSelectedTeam"
/>
<div class="flex justify-between flex-1">
<ButtonSecondary
v-if="
collectionsType.type == 'team-collections' &&
(collectionsType.selectedTeam == undefined ||
collectionsType.selectedTeam.myRole == 'VIEWER')
"
v-tippy="{ theme: 'tooltip' }"
disabled
class="!rounded-none"
:icon="IconPlus"
:title="t('team.no_access')"
:label="t('action.new')"
/>
<ButtonSecondary
v-else
:icon="IconPlus"
:label="t('action.new')"
class="!rounded-none"
@click="displayModalAdd(true)"
/>
<span class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/collections"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<ButtonSecondary
v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }"
:disabled="
collectionsType.type == 'team-collections' &&
collectionsType.selectedTeam == undefined
"
:icon="IconArchive"
:title="t('modal.import_export')"
@click="displayModalImportExport(true)"
/>
</span>
</div>
</div>
<div class="flex flex-col flex-1">
<component
:is="
collectionsType.type == 'my-collections'
? 'CollectionsMyCollection'
: 'CollectionsTeamsCollection'
"
v-for="(collection, index) in filteredCollections"
:key="`collection-${index}`"
:collection-index="parseInt(index)"
:collection="collection"
:is-filtered="filterText.length > 0"
:save-request="saveRequest"
:collections-type="collectionsType"
:picked="picked"
:loading-collection-i-ds="loadingCollectionIDs"
@edit-collection="editCollection(collection, index)"
@add-request="addRequest($event)"
@add-folder="addFolder($event)"
@edit-folder="editFolder($event)"
@edit-request="editRequest($event)"
@duplicate-request="duplicateRequest($event)"
@update-team-collections="updateTeamCollections"
@select-collection="$emit('use-collection', collection)"
@unselect-collection="$emit('remove-collection', collection)"
@select="$emit('select', $event)"
@expand-collection="expandCollection"
@remove-collection="removeCollection"
@remove-request="removeRequest"
@remove-folder="removeFolder"
/>
</div>
<div
v-if="loadingCollectionIDs.includes('root')"
class="flex flex-col items-center justify-center p-4"
>
<SmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div
v-else-if="filteredCollections.length === 0 && filterText.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="t('empty.collections')"
/>
<span class="pb-4 text-center">
{{ t("empty.collections") }}
</span>
<ButtonSecondary
v-if="
collectionsType.type == 'team-collections' &&
(collectionsType.selectedTeam == undefined ||
collectionsType.selectedTeam.myRole == 'VIEWER')
"
v-tippy="{ theme: 'tooltip' }"
:title="t('team.no_access')"
:label="t('add.new')"
class="mb-4"
filled
/>
<ButtonSecondary
v-else
:label="t('add.new')"
filled
class="mb-4"
@click="displayModalAdd(true)"
/>
</div>
<div
v-if="filterText.length !== 0 && filteredCollections.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
<span class="my-2 text-center">
{{ t("state.nothing_found") }} "{{ filterText }}"
</span>
</div>
<CollectionsAdd
:show="showModalAdd"
:loading-state="modalLoadingState"
@submit="addNewRootCollection"
@hide-modal="displayModalAdd(false)"
/>
<CollectionsEdit
:show="showModalEdit"
:editing-collection-name="
editingCollection
? editingCollection.name || editingCollection.title
: ''
"
:loading-state="modalLoadingState"
@hide-modal="displayModalEdit(false)"
@submit="updateEditingCollection"
/>
<CollectionsAddRequest
:show="showModalAddRequest"
:folder="editingFolder"
:folder-path="editingFolderPath"
:loading-state="modalLoadingState"
@add-request="onAddRequest($event)"
@hide-modal="displayModalAddRequest(false)"
/>
<CollectionsAddFolder
:show="showModalAddFolder"
:folder="editingFolder"
:folder-path="editingFolderPath"
:loading-state="modalLoadingState"
@add-folder="onAddFolder($event)"
@hide-modal="displayModalAddFolder(false)"
/>
<CollectionsEditFolder
:show="showModalEditFolder"
:editing-folder-name="
editingFolder ? editingFolder.name || editingFolder.title : ''
"
:loading-state="modalLoadingState"
@submit="updateEditingFolder"
@hide-modal="displayModalEditFolder(false)"
/>
<CollectionsEditRequest
:show="showModalEditRequest"
:editing-request-name="editingRequest ? editingRequest.name : ''"
:loading-state="modalLoadingState"
@submit="updateEditingRequest"
@hide-modal="displayModalEditRequest(false)"
/>
<CollectionsImportExport
:show="showModalImportExport"
:collections-type="collectionsType"
@hide-modal="displayModalImportExport(false)"
@update-team-collections="updateTeamCollections"
/>
<SmartConfirmModal
:show="showConfirmModal"
:title="confirmModalTitle"
:loading-state="modalLoadingState"
@hide-modal="showConfirmModal = false"
@resolve="resolveConfirmModal"
/>
</div>
</template>
<script>
import IconArchive from "~icons/lucide/archive"
import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle"
import { cloneDeep } from "lodash-es"
import { defineComponent, markRaw } from "vue"
import { makeCollection } from "@hoppscotch/data"
import { useColorMode } from "@composables/theming"
import * as E from "fp-ts/Either"
import CollectionsMyCollection from "./my/Collection.vue"
import CollectionsTeamsCollection from "./teams/Collection.vue"
import { currentUser$ } from "~/helpers/fb/auth"
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
import {
restCollections$,
addRESTCollection,
editRESTCollection,
addRESTFolder,
removeRESTCollection,
removeRESTFolder,
editRESTFolder,
removeRESTRequest,
editRESTRequest,
saveRESTRequestAs,
} from "~/newstore/collections"
import {
setRESTRequest,
getRESTRequest,
getRESTSaveContext,
} from "~/newstore/RESTSession"
import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
import { runMutation } from "~/helpers/backend/GQLClient"
import {
CreateChildCollectionDocument,
CreateNewRootCollectionDocument,
CreateRequestInCollectionDocument,
DeleteCollectionDocument,
DeleteRequestDocument,
RenameCollectionDocument,
UpdateRequestDocument,
} from "~/helpers/backend/graphql"
import { useToast } from "@composables/toast"
import { useI18n } from "~/composables/i18n"
export default defineComponent({
components: {
CollectionsMyCollection,
CollectionsTeamsCollection,
},
props: {
saveRequest: Boolean,
picked: { type: Object, default: () => ({}) },
},
emits: [
"update-collection",
"update-coll-type",
"update-team-collections",
"select-request",
"select",
"use-collection",
"remove-collection",
],
setup() {
const { subscribeToStream } = useStreamSubscriber()
return {
subscribeTo: subscribeToStream,
collections: useReadonlyStream(restCollections$, [], "deep"),
currentUser: useReadonlyStream(currentUser$, null),
colorMode: useColorMode(),
toast: useToast(),
t: useI18n(),
}
},
data() {
return {
IconArchive: markRaw(IconArchive),
IconHelpCircle: markRaw(IconHelpCircle),
IconPlus: markRaw(IconPlus),
showModalAdd: false,
showModalEdit: false,
showModalImportExport: false,
showModalAddRequest: false,
showModalAddFolder: false,
showModalEditFolder: false,
showModalEditRequest: false,
showConfirmModal: false,
modalLoadingState: false,
editingCollection: undefined,
editingCollectionIndex: undefined,
editingCollectionID: undefined,
editingFolder: undefined,
editingFolderName: undefined,
editingFolderIndex: undefined,
editingFolderPath: undefined,
editingRequest: undefined,
editingRequestIndex: undefined,
confirmModalTitle: undefined,
filterText: "",
collectionsType: {
type: "my-collections",
selectedTeam: undefined,
},
teamCollectionAdapter: new TeamCollectionAdapter(null),
teamCollectionsNew: [],
loadingCollectionIDs: [],
}
},
computed: {
showTeamCollections() {
if (this.currentUser == null) {
return false
}
return true
},
filteredCollections() {
const collections =
this.collectionsType.type === "my-collections"
? this.collections
: this.teamCollectionsNew
if (!this.filterText) {
return collections
}
if (this.collectionsType.type === "team-collections") {
return []
}
const filterText = this.filterText.toLowerCase()
const filteredCollections = []
for (const collection of collections) {
const filteredRequests = []
const filteredFolders = []
for (const request of collection.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredRequests.push(request)
}
for (const folder of this.collectionsType.type === "team-collections"
? collection.children
: collection.folders) {
const filteredFolderRequests = []
for (const request of folder.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredFolderRequests.push(request)
}
if (filteredFolderRequests.length > 0) {
const filteredFolder = Object.assign({}, folder)
filteredFolder.requests = filteredFolderRequests
filteredFolders.push(filteredFolder)
}
}
if (
filteredRequests.length + filteredFolders.length > 0 ||
collection.name.toLowerCase().includes(filterText)
) {
const filteredCollection = Object.assign({}, collection)
filteredCollection.requests = filteredRequests
filteredCollection.folders = filteredFolders
filteredCollections.push(filteredCollection)
}
}
return filteredCollections
},
},
watch: {
"collectionsType.type": function emitstuff() {
this.$emit("update-collection", this.$data.collectionsType.type)
},
"collectionsType.selectedTeam"(value) {
if (value?.id) this.teamCollectionAdapter.changeTeamID(value.id)
},
currentUser(newValue) {
if (!newValue) this.updateCollectionType("my-collections")
},
},
beforeUnmount() {
this.teamCollectionAdapter.unsubscribeSubscriptions()
},
mounted() {
this.subscribeTo(this.teamCollectionAdapter.collections$, (colls) => {
this.teamCollectionsNew = cloneDeep(colls)
})
this.subscribeTo(
this.teamCollectionAdapter.loadingCollections$,
(collectionsIDs) => {
this.loadingCollectionIDs = collectionsIDs
}
)
},
methods: {
updateTeamCollections() {
// TODO: Remove this at some point
},
updateSelectedTeam(newSelectedTeam) {
this.collectionsType.selectedTeam = newSelectedTeam
this.$emit("update-coll-type", this.collectionsType)
},
updateCollectionType(newCollectionType) {
this.collectionsType.type = newCollectionType
this.$emit("update-coll-type", this.collectionsType)
},
// Intented to be called by the CollectionAdd modal submit event
addNewRootCollection(name) {
if (this.collectionsType.type === "my-collections") {
addRESTCollection(
makeCollection({
name,
folders: [],
requests: [],
})
)
this.displayModalAdd(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
runMutation(CreateNewRootCollectionDocument, {
title: name,
teamID: this.collectionsType.selectedTeam.id,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
if (result.left.error === "team_coll/short_title")
this.toast.error(this.t("collection.name_length_insufficient"))
else this.toast.error(this.t("error.something_went_wrong"))
console.error(result.left.error)
} else {
this.toast.success(this.t("collection.created"))
this.displayModalAdd(false)
}
})
}
},
// Intented to be called by CollectionEdit modal submit event
updateEditingCollection(newName) {
if (!newName) {
this.toast.error(this.t("collection.invalid_name"))
return
}
if (this.collectionsType.type === "my-collections") {
const collectionUpdated = {
...this.editingCollection,
name: newName,
}
editRESTCollection(this.editingCollectionIndex, collectionUpdated)
this.displayModalEdit(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
runMutation(RenameCollectionDocument, {
collectionID: this.editingCollection.id,
newTitle: newName,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.toast.error(this.t("error.something_went_wrong"))
console.error(result.left.error)
} else {
this.toast.success(this.t("collection.renamed"))
this.displayModalEdit(false)
}
})
}
},
// Intended to be called by CollectionEditFolder modal submit event
updateEditingFolder(name) {
if (this.collectionsType.type === "my-collections") {
editRESTFolder(this.editingFolderPath, { ...this.editingFolder, name })
this.displayModalEditFolder(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
runMutation(RenameCollectionDocument, {
collectionID: this.editingFolder.id,
newTitle: name,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
if (result.left.error === "team_coll/short_title")
this.toast.error(this.t("folder.name_length_insufficient"))
else this.toast.error(this.t("error.something_went_wrong"))
console.error(result.left.error)
} else {
this.toast.success(this.t("folder.renamed"))
this.displayModalEditFolder(false)
}
})
}
},
// Intented to by called by CollectionsEditRequest modal submit event
updateEditingRequest(requestUpdateData) {
const saveCtx = getRESTSaveContext()
const requestUpdated = {
...this.editingRequest,
name: requestUpdateData.name || this.editingRequest.name,
}
if (this.collectionsType.type === "my-collections") {
// Update REST Session with the updated state
if (
saveCtx &&
saveCtx.originLocation === "user-collection" &&
saveCtx.requestIndex === this.editingRequestIndex &&
saveCtx.folderPath === this.editingFolderPath
) {
setRESTRequest({
...getRESTRequest(),
name: requestUpdateData.name,
})
}
editRESTRequest(
this.editingFolderPath,
this.editingRequestIndex,
requestUpdated
)
this.displayModalEditRequest(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
const requestName = requestUpdateData.name || this.editingRequest.name
// Update REST Session with the updated state
if (
saveCtx &&
saveCtx.originLocation === "team-collection" &&
saveCtx.requestID === this.editingRequestIndex
) {
setRESTRequest({
...getRESTRequest(),
name: requestUpdateData.name,
})
}
runMutation(UpdateRequestDocument, {
data: {
request: JSON.stringify(requestUpdated),
title: requestName,
},
requestID: this.editingRequestIndex,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.toast.error(this.t("error.something_went_wrong"))
console.error(result.left.error)
} else {
this.toast.success(this.t("request.renamed"))
this.$emit("update-team-collections")
this.displayModalEditRequest(false)
}
})
}
},
displayModalAdd(shouldDisplay) {
this.showModalAdd = shouldDisplay
},
displayModalEdit(shouldDisplay) {
this.showModalEdit = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalImportExport(shouldDisplay) {
this.showModalImportExport = shouldDisplay
},
displayModalAddRequest(shouldDisplay) {
this.showModalAddRequest = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalAddFolder(shouldDisplay) {
this.showModalAddFolder = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalEditFolder(shouldDisplay) {
this.showModalEditFolder = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalEditRequest(shouldDisplay) {
this.showModalEditRequest = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayConfirmModal(shouldDisplay) {
this.showConfirmModal = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
editCollection(collection, collectionIndex) {
this.$data.editingCollection = collection
this.$data.editingCollectionIndex = collectionIndex
this.displayModalEdit(true)
},
onAddFolder({ name, folder, path }) {
if (this.collectionsType.type === "my-collections") {
addRESTFolder(name, path)
this.displayModalAddFolder(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
runMutation(CreateChildCollectionDocument, {
childTitle: name,
collectionID: folder.id,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
if (result.left.error === "team_coll/short_title")
this.toast.error(this.t("folder.name_length_insufficient"))
else this.toast.error(this.t("error.something_went_wrong"))
console.error(result.left.error)
} else {
this.toast.success(this.t("folder.created"))
this.displayModalAddFolder(false)
this.$emit("update-team-collections")
}
})
}
},
addFolder(payload) {
const { folder, path } = payload
this.$data.editingFolder = folder
this.$data.editingFolderPath = path
this.displayModalAddFolder(true)
},
editFolder(payload) {
const { collectionIndex, folder, folderIndex, folderPath } = payload
this.$data.editingCollectionIndex = collectionIndex
this.$data.editingFolder = folder
this.$data.editingFolderIndex = folderIndex
this.$data.editingFolderPath = folderPath
this.$data.collectionsType = this.collectionsType
this.displayModalEditFolder(true)
},
editRequest(payload) {
const {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
} = payload
this.$data.editingCollectionIndex = collectionIndex
this.$data.editingFolderIndex = folderIndex
this.$data.editingFolderName = folderName
this.$data.editingRequest = request
this.$data.editingRequestIndex = requestIndex
this.editingFolderPath = folderPath
this.$emit("select-request", requestIndex)
this.displayModalEditRequest(true)
},
resetSelectedData() {
this.$data.editingCollection = undefined
this.$data.editingCollectionIndex = undefined
this.$data.editingCollectionID = undefined
this.$data.editingFolder = undefined
this.$data.editingFolderPath = undefined
this.$data.editingFolderIndex = undefined
this.$data.editingRequest = undefined
this.$data.editingRequestIndex = undefined
this.$data.confirmModalTitle = undefined
},
expandCollection(collectionID) {
this.teamCollectionAdapter.expandCollection(collectionID)
},
removeCollection({ collectionIndex, collectionID }) {
this.$data.editingCollectionIndex = collectionIndex
this.$data.editingCollectionID = collectionID
this.confirmModalTitle = `${this.t("confirm.remove_collection")}`
this.displayConfirmModal(true)
},
onRemoveCollection() {
const collectionIndex = this.$data.editingCollectionIndex
const collectionID = this.$data.editingCollectionID
if (this.collectionsType.type === "my-collections") {
// Cancel pick if picked collection is deleted
if (
this.picked &&
this.picked.pickedType === "my-collection" &&
this.picked.collectionIndex === collectionIndex
) {
this.$emit("select", { picked: null })
}
removeRESTCollection(collectionIndex)
this.toast.success(this.t("state.deleted"))
this.displayConfirmModal(false)
} else if (this.collectionsType.type === "team-collections") {
this.modalLoadingState = true
// Cancel pick if picked collection is deleted
if (
this.picked &&
this.picked.pickedType === "teams-collection" &&
this.picked.collectionID === collectionID
) {
this.$emit("select", { picked: null })
}
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
runMutation(DeleteCollectionDocument, {
collectionID,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.toast.error(this.t("error.something_went_wrong"))
console.error(result.left.error)
} else {
this.toast.success(this.t("state.deleted"))
this.displayConfirmModal(false)
}
})
}
}
},
removeFolder({ collectionID, folder, folderPath }) {
this.$data.editingCollectionID = collectionID
this.$data.editingFolder = folder
this.$data.editingFolderPath = folderPath
this.confirmModalTitle = `${this.t("confirm.remove_folder")}`
this.displayConfirmModal(true)
},
onRemoveFolder() {
const folder = this.$data.editingFolder
const folderPath = this.$data.editingFolderPath
if (this.collectionsType.type === "my-collections") {
// Cancel pick if picked folder was deleted
if (
this.picked &&
this.picked.pickedType === "my-folder" &&
this.picked.folderPath === folderPath
) {
this.$emit("select", { picked: null })
}
removeRESTFolder(folderPath)
this.toast.success(this.t("state.deleted"))
this.displayConfirmModal(false)
} else if (this.collectionsType.type === "team-collections") {
this.modalLoadingState = true
// Cancel pick if picked collection folder was deleted
if (
this.picked &&
this.picked.pickedType === "teams-folder" &&
this.picked.folderID === folder.id
) {
this.$emit("select", { picked: null })
}
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
runMutation(DeleteCollectionDocument, {
collectionID: folder.id,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.toast.error(`${this.t("error.something_went_wrong")}`)
console.error(result.left.error)
} else {
this.toast.success(`${this.t("state.deleted")}`)
this.displayConfirmModal(false)
this.updateTeamCollections()
}
})
}
}
},
removeRequest({ requestIndex, folderPath }) {
this.$data.editingRequestIndex = requestIndex
this.$data.editingFolderPath = folderPath
this.confirmModalTitle = `${this.t("confirm.remove_request")}`
this.displayConfirmModal(true)
},
onRemoveRequest() {
const requestIndex = this.$data.editingRequestIndex
const folderPath = this.$data.editingFolderPath
if (this.collectionsType.type === "my-collections") {
// Cancel pick if the picked item is being deleted
if (
this.picked &&
this.picked.pickedType === "my-request" &&
this.picked.folderPath === folderPath &&
this.picked.requestIndex === requestIndex
) {
this.$emit("select", { picked: null })
}
removeRESTRequest(folderPath, requestIndex)
this.toast.success(this.t("state.deleted"))
this.displayConfirmModal(false)
} else if (this.collectionsType.type === "team-collections") {
this.modalLoadingState = true
// Cancel pick if the picked item is being deleted
if (
this.picked &&
this.picked.pickedType === "teams-request" &&
this.picked.requestID === requestIndex
) {
this.$emit("select", { picked: null })
}
runMutation(DeleteRequestDocument, {
requestID: requestIndex,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.toast.error(this.t("error.something_went_wrong"))
console.error(result.left.error)
} else {
this.toast.success(this.t("state.deleted"))
this.displayConfirmModal(false)
}
})
}
},
addRequest(payload) {
// TODO: check if the request being worked on
// is being overwritten (selected or not)
const { folder, path } = payload
this.$data.editingFolder = folder
this.$data.editingFolderPath = path
this.displayModalAddRequest(true)
},
onAddRequest({ name, folder, path }) {
const newRequest = {
...cloneDeep(getRESTRequest()),
name,
}
if (this.collectionsType.type === "my-collections") {
const insertionIndex = saveRESTRequestAs(path, newRequest)
// point to it
setRESTRequest(newRequest, {
originLocation: "user-collection",
folderPath: path,
requestIndex: insertionIndex,
})
this.displayModalAddRequest(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
runMutation(CreateRequestInCollectionDocument, {
collectionID: folder.id,
data: {
request: JSON.stringify(newRequest),
teamID: this.collectionsType.selectedTeam.id,
title: name,
},
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.toast.error(this.t("error.something_went_wrong"))
console.error(result.left.error)
} else {
const { createRequestInCollection } = result.right
// point to it
setRESTRequest(newRequest, {
originLocation: "team-collection",
requestID: createRequestInCollection.id,
collectionID: createRequestInCollection.collection.id,
teamID: createRequestInCollection.collection.team.id,
})
this.displayModalAddRequest(false)
}
})
}
},
duplicateRequest({ folderPath, request, collectionID }) {
if (this.collectionsType.type === "team-collections") {
const newReq = {
...cloneDeep(request),
name: `${request.name} - ${this.t("action.duplicate")}`,
}
// Error handling ?
runMutation(CreateRequestInCollectionDocument, {
collectionID,
data: {
request: JSON.stringify(newReq),
teamID: this.collectionsType.selectedTeam.id,
title: `${request.name} - ${this.t("action.duplicate")}`,
},
})()
} else if (this.collectionsType.type === "my-collections") {
saveRESTRequestAs(folderPath, {
...cloneDeep(request),
name: `${request.name} - ${this.t("action.duplicate")}`,
})
}
},
resolveConfirmModal(title) {
if (title === `${this.t("confirm.remove_collection")}`)
this.onRemoveCollection()
else if (title === `${this.t("confirm.remove_request")}`)
this.onRemoveRequest()
else if (title === `${this.t("confirm.remove_folder")}`)
this.onRemoveFolder()
else {
console.error(
`Confirm modal title ${title} is not handled by the component`
)
this.toast.error(this.t("error.something_went_wrong"))
this.displayConfirmModal(false)
}
},
},
})
// request inside folder is not being deleted, you dumb fuck
</script>

View File

@@ -1,355 +0,0 @@
<template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div
class="flex items-stretch group"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()"
>
<span
class="flex items-center justify-center px-4 cursor-pointer"
@click="toggleShowChildren()"
>
<component
:is="getCollectionIcon"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
</span>
<span
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ collection.name }}
</span>
</span>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconFilePlus"
:title="t('request.new')"
class="hidden group-hover:inline-flex"
@click="
$emit('add-request', {
path: `${collectionIndex}`,
})
"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconFolderPlus"
:title="t('folder.new')"
class="hidden group-hover:inline-flex"
@click="
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
"
/>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.x="exportAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="requestAction"
:icon="IconFilePlus"
:label="t('request.new')"
:shortcut="['R']"
@click="
() => {
$emit('add-request', {
path: `${collectionIndex}`,
})
hide()
}
"
/>
<SmartItem
ref="folderAction"
:icon="IconFolderPlus"
:label="t('folder.new')"
:shortcut="['N']"
@click="
() => {
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
hide()
}
"
/>
<SmartItem
ref="edit"
:icon="IconEdit"
:label="t('action.edit')"
:shortcut="['E']"
@click="
() => {
$emit('edit-collection')
hide()
}
"
/>
<SmartItem
ref="exportAction"
:icon="IconDownload"
:label="t('export.title')"
:shortcut="['X']"
@click="
() => {
exportCollection()
hide()
}
"
/>
<SmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
removeCollection()
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
<div v-if="showChildren || isFiltered" class="flex">
<div
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
@click="toggleShowChildren()"
></div>
<div class="flex flex-col flex-1 truncate">
<CollectionsMyFolder
v-for="(folder, index) in collection.folders"
:key="`folder-${index}`"
:folder="folder"
:folder-index="index"
:folder-path="`${collectionIndex}/${index}`"
:collection-index="collectionIndex"
:save-request="saveRequest"
:collections-type="collectionsType"
:is-filtered="isFiltered"
:picked="picked"
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
@remove-folder="$emit('remove-folder', $event)"
/>
<CollectionsMyRequest
v-for="(request, index) in collection.requests"
:key="`request-${index}`"
:request="request"
:collection-index="collectionIndex"
:folder-index="-1"
:folder-name="collection.name"
:folder-path="`${collectionIndex}`"
:request-index="index"
:save-request="saveRequest"
:collections-type="collectionsType"
:picked="picked"
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
/>
<div
v-if="
(collection.folders == undefined ||
collection.folders.length === 0) &&
(collection.requests == undefined ||
collection.requests.length === 0)
"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.collection')}`"
/>
<span class="text-center">
{{ t("empty.collection") }}
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import IconCircle from "~icons/lucide/circle"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconFilePlus from "~icons/lucide/file-plus"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconDownload from "~icons/lucide/download"
import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit"
import IconFolder from "~icons/lucide/folder"
import IconFolderOpen from "~icons/lucide/folder-open"
import { useColorMode } from "@composables/theming"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { defineComponent, ref, markRaw } from "vue"
import { moveRESTRequest } from "~/newstore/collections"
export default defineComponent({
props: {
collectionIndex: { type: Number, default: null },
collection: { type: Object, default: () => ({}) },
isFiltered: Boolean,
saveRequest: Boolean,
collectionsType: { type: Object, default: () => ({}) },
picked: { type: Object, default: () => ({}) },
},
emits: [
"select",
"expand-collection",
"add-collection",
"remove-collection",
"add-folder",
"add-request",
"edit-folder",
"edit-request",
"duplicate-request",
"remove-folder",
"remove-request",
"select-collection",
"unselect-collection",
"edit-collection",
],
setup() {
return {
colorMode: useColorMode(),
toast: useToast(),
t: useI18n(),
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
requestAction: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
exportAction: ref<any | null>(null),
}
},
data() {
return {
IconCircle: markRaw(IconCircle),
IconCheckCircle: markRaw(IconCheckCircle),
IconFilePlus: markRaw(IconFilePlus),
IconFolderPlus: markRaw(IconFolderPlus),
IconMoreVertical: markRaw(IconMoreVertical),
IconEdit: markRaw(IconEdit),
IconDownload: markRaw(IconDownload),
IconTrash2: markRaw(IconTrash2),
showChildren: false,
dragging: false,
selectedFolder: {},
prevCursor: "",
cursor: "",
pageNo: 0,
}
},
computed: {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "my-collection" &&
this.picked.collectionIndex === this.collectionIndex
)
},
getCollectionIcon() {
if (this.isSelected) return IconCheckCircle
else if (!this.showChildren && !this.isFiltered) return IconFolder
else if (this.showChildren || this.isFiltered) return IconFolderOpen
else return IconFolder
},
},
methods: {
exportCollection() {
const collectionJSON = JSON.stringify(this.collection)
const file = new Blob([collectionJSON], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${this.collection.name}.json`
document.body.appendChild(a)
a.click()
this.toast.success(this.t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
},
toggleShowChildren() {
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "my-collection",
collectionIndex: this.collectionIndex,
},
})
this.$emit("expand-collection", this.collection.id)
this.showChildren = !this.showChildren
},
removeCollection() {
this.$emit("remove-collection", {
collectionIndex: this.collectionIndex,
collectionID: this.collection.id,
})
},
dropEvent({ dataTransfer }: any) {
this.dragging = !this.dragging
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
moveRESTRequest(folderPath, requestIndex, `${this.collectionIndex}`)
},
},
})
</script>

View File

@@ -1,341 +0,0 @@
<template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div
class="flex items-stretch group"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()"
>
<span
class="flex items-center justify-center px-4 cursor-pointer"
@click="toggleShowChildren()"
>
<component
:is="getCollectionIcon"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
</span>
<span
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ folder.name ? folder.name : folder.title }}
</span>
</span>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconFilePlus"
:title="t('request.new')"
class="hidden group-hover:inline-flex"
@click="$emit('add-request', { path: folderPath })"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconFolderPlus"
:title="t('folder.new')"
class="hidden group-hover:inline-flex"
@click="$emit('add-folder', { folder, path: folderPath })"
/>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.x="exportAction.$el.click()"
@keyup.escape="hide()"
>
<SmartItem
ref="requestAction"
:icon="IconFilePlus"
:label="t('request.new')"
:shortcut="['R']"
@click="
() => {
$emit('add-request', { path: folderPath })
hide()
}
"
/>
<SmartItem
ref="folderAction"
:icon="IconFolderPlus"
:label="t('folder.new')"
:shortcut="['N']"
@click="
() => {
$emit('add-folder', { folder, path: folderPath })
hide()
}
"
/>
<SmartItem
ref="edit"
:icon="IconEdit"
:label="t('action.edit')"
:shortcut="['E']"
@click="
() => {
$emit('edit-folder', {
folder,
folderIndex,
collectionIndex,
folderPath,
})
hide()
}
"
/>
<SmartItem
ref="exportAction"
:icon="IconDownload"
:label="t('export.title')"
:shortcut="['X']"
@click="
() => {
exportFolder()
hide()
}
"
/>
<SmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
removeFolder()
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
<div v-if="showChildren || isFiltered" class="flex">
<div
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
@click="toggleShowChildren()"
></div>
<div class="flex flex-col flex-1 truncate">
<!-- Referring to this component only (this is recursive) -->
<Folder
v-for="(subFolder, subFolderIndex) in folder.folders"
:key="`subFolder-${subFolderIndex}`"
:folder="subFolder"
:folder-index="subFolderIndex"
:collection-index="collectionIndex"
:save-request="saveRequest"
:collections-type="collectionsType"
:folder-path="`${folderPath}/${subFolderIndex}`"
:picked="picked"
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@update-team-collections="$emit('update-team-collections')"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
@remove-folder="$emit('remove-folder', $event)"
/>
<CollectionsMyRequest
v-for="(request, index) in folder.requests"
:key="`request-${index}`"
:request="request"
:collection-index="collectionIndex"
:folder-index="folderIndex"
:folder-name="folder.name"
:folder-path="folderPath"
:request-index="index"
:picked="picked"
:save-request="saveRequest"
:collections-type="collectionsType"
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
/>
<div
v-if="
folder.folders &&
folder.folders.length === 0 &&
folder.requests &&
folder.requests.length === 0
"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.folder')}`"
/>
<span class="text-center">
{{ t("empty.folder") }}
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import IconFilePlus from "~icons/lucide/file-plus"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconEdit from "~icons/lucide/edit"
import IconDownload from "~icons/lucide/download"
import IconTrash2 from "~icons/lucide/trash-2"
import IconFolder from "~icons/lucide/folder"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconFolderOpen from "~icons/lucide/folder-open"
import { defineComponent, ref } from "vue"
import { useColorMode } from "@composables/theming"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { moveRESTRequest } from "~/newstore/collections"
export default defineComponent({
name: "Folder",
props: {
folder: { type: Object, default: () => ({}) },
folderIndex: { type: Number, default: null },
collectionIndex: { type: Number, default: null },
folderPath: { type: String, default: null },
saveRequest: Boolean,
isFiltered: Boolean,
collectionsType: { type: Object, default: () => ({}) },
picked: { type: Object, default: () => ({}) },
},
emits: [
"add-request",
"add-folder",
"edit-folder",
"update-team",
"remove-folder",
"edit-request",
"duplicate-request",
"select",
"remove-request",
"update-team-collections",
],
setup() {
const t = useI18n()
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
requestAction: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
exportAction: ref<any | null>(null),
t,
toast: useToast(),
colorMode: useColorMode(),
IconFilePlus,
IconFolderPlus,
IconMoreVertical,
IconEdit,
IconDownload,
IconTrash2,
}
},
data() {
return {
showChildren: false,
dragging: false,
prevCursor: "",
cursor: "",
}
},
computed: {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "my-folder" &&
this.picked.folderPath === this.folderPath
)
},
getCollectionIcon() {
if (this.isSelected) return IconCheckCircle
else if (!this.showChildren && !this.isFiltered) return IconFolder
else if (this.showChildren || this.isFiltered) return IconFolderOpen
else return IconFolder
},
},
methods: {
exportFolder() {
const folderJSON = JSON.stringify(this.folder)
const file = new Blob([folderJSON], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${this.folder.name}.json`
document.body.appendChild(a)
a.click()
this.toast.success(this.t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
},
toggleShowChildren() {
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "my-folder",
collectionIndex: this.collectionIndex,
folderName: this.folder.name,
folderPath: this.folderPath,
},
})
this.showChildren = !this.showChildren
},
removeFolder() {
this.$emit("remove-folder", {
folder: this.folder,
folderPath: this.folderPath,
})
},
dropEvent({ dataTransfer }) {
this.dragging = !this.dragging
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
moveRESTRequest(folderPath, requestIndex, this.folderPath)
},
},
})
</script>

View File

@@ -1,435 +0,0 @@
<template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div
class="flex items-stretch group"
draggable="true"
@dragstart="dragStart"
@dragover.stop
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()"
>
<span
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
:class="getRequestLabelColor(request.method)"
@click="selectRequest()"
>
<component
:is="IconCheckCircle"
v-if="isSelected"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
<span v-else class="font-semibold truncate text-tiny">
{{ request.method }}
</span>
</span>
<span
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="selectRequest()"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }}
</span>
<span
v-if="isActive"
v-tippy="{ theme: 'tooltip' }"
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
:title="`${t('collection.request_in_use')}`"
>
<span
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
>
</span>
<span
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
></span>
</span>
</span>
<div class="flex">
<ButtonSecondary
v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }"
:icon="IconRotateCCW"
:title="t('action.restore')"
class="hidden group-hover:inline-flex"
@click="selectRequest()"
/>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.e="edit.$el.click()"
@keyup.d="duplicate.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="hide()"
>
<SmartItem
ref="edit"
:icon="IconEdit"
:label="t('action.edit')"
:shortcut="['E']"
@click="
() => {
emit('edit-request', {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
})
hide()
}
"
/>
<SmartItem
ref="duplicate"
:icon="IconCopy"
:label="t('action.duplicate')"
:shortcut="['D']"
@click="
() => {
emit('duplicate-request', {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
})
hide()
}
"
/>
<SmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
removeRequest()
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
<HttpReqChangeConfirmModal
:show="confirmChange"
@hide-modal="confirmChange = false"
@save-change="saveRequestChange"
@discard-change="discardRequestChange"
/>
<CollectionsSaveRequest
mode="rest"
:show="showSaveRequestModal"
@hide-modal="showSaveRequestModal = false"
/>
</div>
</template>
<script setup lang="ts">
import IconCheckCircle from "~icons/lucide/check-circle"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconEdit from "~icons/lucide/edit"
import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import { ref, computed } from "vue"
import {
HoppRESTRequest,
safelyExtractRESTRequest,
translateToNewRequest,
isEqualHoppRESTRequest,
} from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import { cloneDeep } from "lodash-es"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useReadonlyStream } from "@composables/stream"
import {
getDefaultRESTRequest,
getRESTRequest,
restSaveContext$,
setRESTRequest,
setRESTSaveContext,
getRESTSaveContext,
} from "~/newstore/RESTSession"
import { editRESTRequest } from "~/newstore/collections"
import { runMutation } from "~/helpers/backend/GQLClient"
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
const props = defineProps<{
request: HoppRESTRequest
collectionIndex: number
folderIndex: number
folderName: string
requestIndex: number
saveRequest: boolean
collectionsType: object
folderPath: string
picked?: {
pickedType: string
collectionIndex: number
folderPath: string
folderName: string
requestIndex: number
}
}>()
const emit = defineEmits<{
(
e: "select",
data:
| {
picked: {
pickedType: string
collectionIndex: number
folderPath: string
folderName: string
requestIndex: number
}
}
| undefined
): void
(
e: "remove-request",
data: {
folderPath: string
requestIndex: number
}
): void
(
e: "duplicate-request",
data: {
collectionIndex: number
folderIndex: number
folderName: string
request: HoppRESTRequest
folderPath: string
requestIndex: number
}
): void
(
e: "edit-request",
data: {
collectionIndex: number
folderIndex: number
folderName: string
request: HoppRESTRequest
folderPath: string
requestIndex: number
}
): void
}>()
const t = useI18n()
const toast = useToast()
const dragging = ref(false)
const requestMethodLabels = {
get: "text-green-500",
post: "text-yellow-500",
put: "text-blue-500",
delete: "text-red-500",
default: "text-gray-500",
}
const confirmChange = ref(false)
const showSaveRequestModal = ref(false)
// Template refs
const tippyActions = ref<any | null>(null)
const options = ref<any | null>(null)
const edit = ref<any | null>(null)
const duplicate = ref<any | null>(null)
const deleteAction = ref<any | null>(null)
const active = useReadonlyStream(restSaveContext$, null)
const isSelected = computed(
() =>
props.picked &&
props.picked.pickedType === "my-request" &&
props.picked.folderPath === props.folderPath &&
props.picked.requestIndex === props.requestIndex
)
const isActive = computed(
() =>
active.value &&
active.value.originLocation === "user-collection" &&
active.value.folderPath === props.folderPath &&
active.value.requestIndex === props.requestIndex
)
const dragStart = ({ dataTransfer }: DragEvent) => {
if (dataTransfer) {
dragging.value = !dragging.value
dataTransfer.setData("folderPath", props.folderPath)
dataTransfer.setData("requestIndex", props.requestIndex.toString())
}
}
const removeRequest = () => {
emit("remove-request", {
folderPath: props.folderPath,
requestIndex: props.requestIndex,
})
}
const getRequestLabelColor = (method: string) =>
requestMethodLabels[
method.toLowerCase() as keyof typeof requestMethodLabels
] || requestMethodLabels.default
const setRestReq = (request: any) => {
setRESTRequest(
cloneDeep(
safelyExtractRESTRequest(
translateToNewRequest(request),
getDefaultRESTRequest()
)
),
{
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
req: cloneDeep(request),
}
)
}
/** Loads request from the save once, checks for unsaved changes, but ignores default values */
const selectRequest = () => {
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
if (props.saveRequest) {
emit("select", {
picked: {
pickedType: "my-request",
collectionIndex: props.collectionIndex,
folderPath: props.folderPath,
folderName: props.folderName,
requestIndex: props.requestIndex,
},
})
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
confirmChange.value = false
setRestReq(props.request)
} else if (!active.value) {
// If the current request is the same as the request to be loaded in, there is no data loss
const currentReq = getRESTRequest()
if (isEqualHoppRESTRequest(currentReq, props.request)) {
setRestReq(props.request)
} else {
confirmChange.value = true
}
} else {
const currentReqWithNoChange = active.value.req
const currentFullReq = getRESTRequest()
// Check if whether user clicked the same request or not
if (!isActive.value && currentReqWithNoChange !== undefined) {
// Check if there is any changes done on the current request
if (isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)) {
setRestReq(props.request)
} else {
confirmChange.value = true
}
} else {
setRESTSaveContext(null)
}
}
}
/** Save current request to the collection */
const saveRequestChange = () => {
const saveCtx = getRESTSaveContext()
saveCurrentRequest(saveCtx)
confirmChange.value = false
}
/** Discard changes and change the current request and context */
const discardRequestChange = () => {
setRestReq(props.request)
if (!isActive.value) {
setRESTSaveContext({
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
req: cloneDeep(props.request),
})
}
confirmChange.value = false
}
const saveCurrentRequest = (saveCtx: HoppRequestSaveContext | null) => {
if (!saveCtx) {
showSaveRequestModal.value = true
return
}
if (saveCtx.originLocation === "user-collection") {
try {
editRESTRequest(
saveCtx.folderPath,
saveCtx.requestIndex,
getRESTRequest()
)
setRestReq(props.request)
toast.success(`${t("request.saved")}`)
} catch (e) {
setRESTSaveContext(null)
saveCurrentRequest(saveCtx)
}
} else if (saveCtx.originLocation === "team-collection") {
const req = getRESTRequest()
try {
runMutation(UpdateRequestDocument, {
requestID: saveCtx.requestID,
data: {
title: req.name,
request: JSON.stringify(req),
},
})().then((result) => {
if (E.isLeft(result)) {
toast.error(`${t("profile.no_permission")}`)
} else {
toast.success(`${t("request.saved")}`)
}
})
setRestReq(props.request)
} catch (error) {
showSaveRequestModal.value = true
toast.error(`${t("error.something_went_wrong")}`)
console.error(error)
}
}
}
</script>

View File

@@ -1,407 +0,0 @@
<template>
<div class="flex flex-col">
<div
class="flex items-stretch group"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()"
>
<span
class="flex items-center justify-center px-4 cursor-pointer"
@click="toggleShowChildren()"
>
<component
:is="getCollectionIcon"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
</span>
<span
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ collection.title }}
</span>
</span>
<div class="flex">
<ButtonSecondary
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
:icon="IconFilePlus"
:title="t('request.new')"
class="hidden group-hover:inline-flex"
@click="
$emit('add-request', {
folder: collection,
path: `${collectionIndex}`,
})
"
/>
<ButtonSecondary
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
:icon="IconFolderPlus"
:title="t('folder.new')"
class="hidden group-hover:inline-flex"
@click="
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
"
/>
<span>
<tippy
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
ref="options"
interactive
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.x="exportAction.$el.click()"
@keyup.escape="hide()"
>
<SmartItem
ref="requestAction"
:icon="IconFilePlus"
:label="t('request.new')"
:shortcut="['R']"
@click="
() => {
$emit('add-request', {
folder: collection,
path: `${collectionIndex}`,
})
hide()
}
"
/>
<SmartItem
ref="folderAction"
:icon="IconFolderPlus"
:label="t('folder.new')"
:shortcut="['N']"
@click="
() => {
$emit('add-folder', {
folder: collection,
path: `${collectionIndex}`,
})
hide()
}
"
/>
<SmartItem
ref="edit"
:icon="IconEdit"
:label="t('action.edit')"
:shortcut="['E']"
@click="
() => {
$emit('edit-collection')
hide()
}
"
/>
<SmartItem
ref="exportAction"
:icon="IconDownload"
:label="t('export.title')"
:shortcut="['X']"
:loading="exportLoading"
@click="exportCollection"
/>
<SmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
removeCollection()
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
<div v-if="showChildren || isFiltered" class="flex">
<div
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
@click="toggleShowChildren()"
></div>
<div class="flex flex-col flex-1 truncate">
<CollectionsTeamsFolder
v-for="(folder, index) in collection.children"
:key="`folder-${index}`"
:folder="folder"
:folder-index="index"
:folder-path="`${collectionIndex}/${index}`"
:collection-index="collectionIndex"
:save-request="saveRequest"
:collections-type="collectionsType"
:is-filtered="isFiltered"
:picked="picked"
:loading-collection-i-ds="loadingCollectionIDs"
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
@expand-collection="expandCollection"
@remove-request="$emit('remove-request', $event)"
@remove-folder="$emit('remove-folder', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<CollectionsTeamsRequest
v-for="(request, index) in collection.requests"
:key="`request-${index}`"
:request="request.request"
:collection-index="collectionIndex"
:folder-index="-1"
:folder-name="collection.name"
:request-index="request.id"
:save-request="saveRequest"
:collection-i-d="collection.id"
:collections-type="collectionsType"
:picked="picked"
@edit-request="editRequest($event)"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<div
v-if="loadingCollectionIDs.includes(collection.id)"
class="flex flex-col items-center justify-center p-4"
>
<SmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div
v-else-if="
(collection.children == undefined ||
collection.children.length === 0) &&
(collection.requests == undefined ||
collection.requests.length === 0)
"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.collection')}`"
/>
<span class="text-center">
{{ t("empty.collection") }}
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconTrash2 from "~icons/lucide/trash-2"
import IconDownload from "~icons/lucide/download"
import IconEdit from "~icons/lucide/edit"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconFilePlus from "~icons/lucide/file-plus"
import IconCircle from "~icons/lucide/circle"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconFolder from "~icons/lucide/folder"
import IconFolderOpen from "~icons/lucide/folder-open"
import { defineComponent, ref } from "vue"
import * as E from "fp-ts/Either"
import {
getCompleteCollectionTree,
teamCollToHoppRESTColl,
} from "~/helpers/backend/helpers"
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
import { useColorMode } from "@composables/theming"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
export default defineComponent({
props: {
collectionIndex: { type: Number, default: null },
collection: { type: Object, default: () => ({}) },
isFiltered: Boolean,
saveRequest: Boolean,
collectionsType: { type: Object, default: () => ({}) },
picked: { type: Object, default: () => ({}) },
loadingCollectionIDs: { type: Array, default: () => [] },
},
emits: [
"edit-collection",
"add-request",
"add-folder",
"edit-folder",
"edit-request",
"remove-folder",
"select",
"remove-request",
"duplicate-request",
"expand-collection",
"remove-collection",
],
setup() {
const t = useI18n()
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
requestAction: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
exportAction: ref<any | null>(null),
exportLoading: ref<boolean>(false),
t,
toast: useToast(),
colorMode: useColorMode(),
IconCheckCircle,
IconCircle,
IconFilePlus,
IconFolderPlus,
IconEdit,
IconDownload,
IconTrash2,
IconMoreVertical,
}
},
data() {
return {
showChildren: false,
dragging: false,
selectedFolder: {},
prevCursor: "",
cursor: "",
pageNo: 0,
}
},
computed: {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "teams-collection" &&
this.picked.collectionID === this.collection.id
)
},
getCollectionIcon() {
if (this.isSelected) return IconCheckCircle
else if (!this.showChildren && !this.isFiltered) return IconFolder
else if (this.showChildren || this.isFiltered) return IconFolderOpen
else return IconFolder
},
},
methods: {
async exportCollection() {
this.exportLoading = true
const result = await getCompleteCollectionTree(this.collection.id)()
if (E.isLeft(result)) {
this.toast.error(this.t("error.something_went_wrong").toString())
console.log(result.left)
this.exportLoading = false
this.options.tippy().hide()
return
}
const hoppColl = teamCollToHoppRESTColl(result.right)
const collectionJSON = JSON.stringify(hoppColl)
const file = new Blob([collectionJSON], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${hoppColl.name}.json`
document.body.appendChild(a)
a.click()
this.toast.success(this.t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
this.exportLoading = false
this.options.tippy().hide()
},
editRequest(event: any) {
this.$emit("edit-request", event)
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "teams-collection",
collectionID: this.collection.id,
},
})
},
toggleShowChildren() {
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "teams-collection",
collectionID: this.collection.id,
},
})
this.$emit("expand-collection", this.collection.id)
this.showChildren = !this.showChildren
},
removeCollection() {
this.$emit("remove-collection", {
collectionIndex: this.collectionIndex,
collectionID: this.collection.id,
})
},
expandCollection(collectionID: string) {
this.$emit("expand-collection", collectionID)
},
async dropEvent({ dataTransfer }: any) {
this.dragging = !this.dragging
const requestIndex = dataTransfer.getData("requestIndex")
const moveRequestResult = await moveRESTTeamRequest(
requestIndex,
this.collection.id
)()
if (E.isLeft(moveRequestResult))
this.toast.error(`${this.t("error.something_went_wrong")}`)
},
},
})
</script>

View File

@@ -1,382 +0,0 @@
<template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div
class="flex items-stretch group"
@dragover.prevent
@drop.prevent="dropEvent"
@dragover="dragging = true"
@drop="dragging = false"
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()"
>
<span
class="flex items-center justify-center px-4 cursor-pointer"
@click="toggleShowChildren()"
>
<component
:is="getCollectionIcon"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
</span>
<span
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ folder.name ? folder.name : folder.title }}
</span>
</span>
<div class="flex">
<ButtonSecondary
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
:icon="IconFilePlus"
:title="t('request.new')"
class="hidden group-hover:inline-flex"
@click="$emit('add-request', { folder, path: folderPath })"
/>
<ButtonSecondary
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
:icon="IconFolderPlus"
:title="t('folder.new')"
class="hidden group-hover:inline-flex"
@click="$emit('add-folder', { folder, path: folderPath })"
/>
<span>
<tippy
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
ref="options"
interactive
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.x="exportAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="requestAction"
:icon="IconFilePlus"
:label="t('request.new')"
:shortcut="['R']"
@click="
() => {
$emit('add-request', { folder, path: folderPath })
hide()
}
"
/>
<SmartItem
ref="folderAction"
:icon="IconFolderPlus"
:label="t('folder.new')"
:shortcut="['N']"
@click="
() => {
$emit('add-folder', { folder, path: folderPath })
hide()
}
"
/>
<SmartItem
ref="edit"
:icon="IconEdit"
:label="t('action.edit')"
:shortcut="['E']"
@click="
() => {
$emit('edit-folder', {
folder,
folderIndex,
collectionIndex,
folderPath: '',
})
hide()
}
"
/>
<SmartItem
ref="exportAction"
:icon="IconDownload"
:label="t('export.title')"
:shortcut="['X']"
:loading="exportLoading"
@click="exportFolder"
/>
<SmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
removeFolder()
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
<div v-if="showChildren || isFiltered" class="flex">
<div
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-1 hover:bg-dividerDark hover:scale-x-125"
@click="toggleShowChildren()"
></div>
<div class="flex flex-col flex-1 truncate">
<!-- Referring to this component only (this is recursive) -->
<Folder
v-for="(subFolder, subFolderIndex) in folder.children"
:key="`subFolder-${subFolderIndex}`"
:folder="subFolder"
:folder-index="subFolderIndex"
:collection-index="collectionIndex"
:save-request="saveRequest"
:collections-type="collectionsType"
:folder-path="`${folderPath}/${subFolderIndex}`"
:picked="picked"
:loading-collection-i-ds="loadingCollectionIDs"
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@update-team-collections="$emit('update-team-collections')"
@select="$emit('select', $event)"
@expand-collection="expandCollection"
@remove-request="$emit('remove-request', $event)"
@remove-folder="$emit('remove-folder', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<CollectionsTeamsRequest
v-for="(request, index) in folder.requests"
:key="`request-${index}`"
:request="request.request"
:collection-index="collectionIndex"
:folder-index="folderIndex"
:folder-name="folder.name"
:request-index="request.id"
:save-request="saveRequest"
:collections-type="collectionsType"
:picked="picked"
:collection-i-d="folder.id"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<div
v-if="loadingCollectionIDs.includes(folder.id)"
class="flex flex-col items-center justify-center p-4"
>
<SmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div
v-else-if="
(folder.children == undefined || folder.children.length === 0) &&
(folder.requests == undefined || folder.requests.length === 0)
"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.folder')}`"
/>
<span class="text-center">
{{ t("empty.folder") }}
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconEdit from "~icons/lucide/edit"
import IconDownload from "~icons/lucide/download"
import IconTrash2 from "~icons/lucide/trash-2"
import IconFilePlus from "~icons/lucide/file-plus"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconFolder from "~icons/lucide/folder"
import IconFolderOpen from "~icons/lucide/folder-open"
import { defineComponent, ref } from "vue"
import * as E from "fp-ts/Either"
import {
getCompleteCollectionTree,
teamCollToHoppRESTColl,
} from "~/helpers/backend/helpers"
import { moveRESTTeamRequest } from "~/helpers/backend/mutations/TeamRequest"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useColorMode } from "@composables/theming"
export default defineComponent({
name: "Folder",
props: {
folder: { type: Object, default: () => ({}) },
folderIndex: { type: Number, default: null },
collectionIndex: { type: Number, default: null },
folderPath: { type: String, default: null },
saveRequest: Boolean,
isFiltered: Boolean,
collectionsType: { type: Object, default: () => ({}) },
picked: { type: Object, default: () => ({}) },
loadingCollectionIDs: { type: Array, default: () => [] },
},
emits: [
"add-request",
"add-folder",
"edit-folder",
"update-team-collections",
"edit-request",
"remove-request",
"duplicate-request",
"select",
"remove-folder",
"expand-collection",
],
setup() {
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
requestAction: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
exportAction: ref<any | null>(null),
exportLoading: ref<boolean>(false),
toast: useToast(),
t: useI18n(),
colorMode: useColorMode(),
IconFilePlus,
IconFolderPlus,
IconCheckCircle,
IconFolder,
IconFolderOpen,
IconMoreVertical,
IconEdit,
IconDownload,
IconTrash2,
}
},
data() {
return {
showChildren: false,
dragging: false,
prevCursor: "",
cursor: "",
}
},
computed: {
isSelected(): boolean {
return (
this.picked &&
this.picked.pickedType === "teams-folder" &&
this.picked.folderID === this.folder.id
)
},
getCollectionIcon() {
if (this.isSelected) return IconCheckCircle
else if (!this.showChildren && !this.isFiltered) return IconFolder
else if (this.showChildren || this.isFiltered) return IconFolderOpen
else return IconFolder
},
},
methods: {
async exportFolder() {
this.exportLoading = true
const result = await getCompleteCollectionTree(this.folder.id)()
if (E.isLeft(result)) {
this.toast.error(this.t("error.something_went_wrong").toString())
console.log(result.left)
this.exportLoading = false
this.options.tippy().hide()
return
}
const hoppColl = teamCollToHoppRESTColl(result.right)
const collectionJSON = JSON.stringify(hoppColl)
const file = new Blob([collectionJSON], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${hoppColl.name}.json`
document.body.appendChild(a)
a.click()
this.toast.success(this.t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
this.exportLoading = false
this.options.tippy().hide()
},
toggleShowChildren() {
if (this.$props.saveRequest)
this.$emit("select", {
picked: {
pickedType: "teams-folder",
folderID: this.folder.id,
},
})
this.$emit("expand-collection", this.$props.folder.id)
this.showChildren = !this.showChildren
},
removeFolder() {
this.$emit("remove-folder", {
collectionsType: this.collectionsType,
folder: this.folder,
})
},
expandCollection(collectionID: number) {
this.$emit("expand-collection", collectionID)
},
async dropEvent({ dataTransfer }: any) {
this.dragging = !this.dragging
const requestIndex = dataTransfer.getData("requestIndex")
const moveRequestResult = await moveRESTTeamRequest(
requestIndex,
this.folder.id
)()
if (E.isLeft(moveRequestResult))
this.toast.error(`${this.t("error.something_went_wrong")}`)
},
},
})
</script>

View File

@@ -1,407 +0,0 @@
<template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div
class="flex items-stretch group"
draggable="true"
@dragstart="dragStart"
@dragover.stop
@dragleave="dragging = false"
@dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()"
>
<span
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
:class="getRequestLabelColor(request.method)"
@click="selectRequest()"
>
<component
:is="IconCheckCircle"
v-if="isSelected"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
<span v-else class="font-semibold truncate text-tiny">
{{ request.method }}
</span>
</span>
<span
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="selectRequest()"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }}
</span>
<span
v-if="isActive"
v-tippy="{ theme: 'tooltip' }"
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
:title="`${t('collection.request_in_use')}`"
>
<span
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
>
</span>
<span
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
></span>
</span>
</span>
<div class="flex">
<ButtonSecondary
v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }"
:icon="IconRotateCCW"
:title="t('action.restore')"
class="hidden group-hover:inline-flex"
@click="selectRequest()"
/>
<span>
<tippy
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
ref="options"
interactive
trigger="click"
theme="popover"
arrow
:on-shown="() => tippyActions.focus()"
>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.e="edit.$el.click()"
@keyup.d="duplicate.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="hide()"
>
<SmartItem
ref="edit"
:icon="IconEdit"
:label="t('action.edit')"
:shortcut="['E']"
@click="
() => {
emit('edit-request', {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
})
hide()
}
"
/>
<SmartItem
ref="duplicate"
:icon="IconCopy"
:label="t('action.duplicate')"
:shortcut="['D']"
@click="
() => {
emit('duplicate-request', {
request,
requestIndex,
collectionID,
})
hide()
}
"
/>
<SmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
removeRequest()
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
<HttpReqChangeConfirmModal
:show="confirmChange"
@hide-modal="confirmChange = false"
@save-change="saveRequestChange"
@discard-change="discardRequestChange"
/>
<CollectionsSaveRequest
mode="rest"
:show="showSaveRequestModal"
@hide-modal="showSaveRequestModal = false"
/>
</div>
</template>
<script setup lang="ts">
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconEdit from "~icons/lucide/edit"
import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2"
import { ref, computed } from "vue"
import {
HoppRESTRequest,
isEqualHoppRESTRequest,
safelyExtractRESTRequest,
translateToNewRequest,
} from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useReadonlyStream } from "@composables/stream"
import {
getDefaultRESTRequest,
restSaveContext$,
setRESTRequest,
setRESTSaveContext,
getRESTSaveContext,
getRESTRequest,
} from "~/newstore/RESTSession"
import { editRESTRequest } from "~/newstore/collections"
import { runMutation } from "~/helpers/backend/GQLClient"
import { Team, UpdateRequestDocument } from "~/helpers/backend/graphql"
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
const props = defineProps<{
request: HoppRESTRequest
collectionIndex: number
folderIndex: number
folderName?: string
requestIndex: string
saveRequest: boolean
collectionsType: {
type: "my-collections" | "team-collections"
selectedTeam: Team | undefined
}
collectionID: string
picked?: {
pickedType: string
requestID: string
}
}>()
const emit = defineEmits<{
(
e: "select",
data:
| {
picked: {
pickedType: string
requestID: string
}
}
| undefined
): void
(
e: "remove-request",
data: {
folderPath: string | undefined
requestIndex: string
}
): void
(
e: "edit-request",
data: {
collectionIndex: number
folderIndex: number
folderName: string | undefined
requestIndex: string
request: HoppRESTRequest
}
): void
(
e: "duplicate-request",
data: {
collectionID: number | string
requestIndex: string
request: HoppRESTRequest
}
): void
}>()
const t = useI18n()
const toast = useToast()
const dragging = ref(false)
const requestMethodLabels = {
get: "text-green-500",
post: "text-yellow-500",
put: "text-blue-500",
delete: "text-red-500",
default: "text-gray-500",
}
const confirmChange = ref(false)
const showSaveRequestModal = ref(false)
// Template refs
const tippyActions = ref<any | null>(null)
const options = ref<any | null>(null)
const edit = ref<any | null>(null)
const duplicate = ref<any | null>(null)
const deleteAction = ref<any | null>(null)
const active = useReadonlyStream(restSaveContext$, null)
const isSelected = computed(
() =>
props.picked &&
props.picked.pickedType === "teams-request" &&
props.picked.requestID === props.requestIndex
)
const isActive = computed(
() =>
active.value &&
active.value.originLocation === "team-collection" &&
active.value.requestID === props.requestIndex
)
const dragStart = ({ dataTransfer }: DragEvent) => {
if (dataTransfer) {
dragging.value = !dragging.value
dataTransfer.setData("requestIndex", props.requestIndex)
}
}
const removeRequest = () => {
emit("remove-request", {
folderPath: props.folderName,
requestIndex: props.requestIndex,
})
}
const getRequestLabelColor = (method: string): string => {
return (
(requestMethodLabels as any)[method.toLowerCase()] ||
requestMethodLabels.default
)
}
const setRestReq = (request: HoppRESTRequest) => {
setRESTRequest(
safelyExtractRESTRequest(
translateToNewRequest(request),
getDefaultRESTRequest()
),
{
originLocation: "team-collection",
requestID: props.requestIndex,
req: request,
}
)
}
const selectRequest = () => {
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
if (props.saveRequest) {
emit("select", {
picked: {
pickedType: "teams-request",
requestID: props.requestIndex,
},
})
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
confirmChange.value = false
setRestReq(props.request)
} else if (!active.value) {
confirmChange.value = true
} else {
const currentReqWithNoChange = active.value.req
const currentFullReq = getRESTRequest()
// Check if whether user clicked the same request or not
if (!isActive.value && currentReqWithNoChange) {
// Check if there is any changes done on the current request
if (isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)) {
setRestReq(props.request)
} else {
confirmChange.value = true
}
} else {
setRESTSaveContext(null)
}
}
}
/** Save current request to the collection */
const saveRequestChange = () => {
const saveCtx = getRESTSaveContext()
saveCurrentRequest(saveCtx)
confirmChange.value = false
}
/** Discard changes and change the current request and context */
const discardRequestChange = () => {
setRestReq(props.request)
if (!isActive.value) {
setRESTSaveContext({
originLocation: "team-collection",
requestID: props.requestIndex,
req: props.request,
})
}
confirmChange.value = false
}
const saveCurrentRequest = (saveCtx: HoppRequestSaveContext | null) => {
if (!saveCtx) {
showSaveRequestModal.value = true
return
}
if (saveCtx.originLocation === "team-collection") {
const req = getRESTRequest()
try {
runMutation(UpdateRequestDocument, {
requestID: saveCtx.requestID,
data: {
title: req.name,
request: JSON.stringify(req),
},
})().then((result) => {
if (E.isLeft(result)) {
toast.error(`${t("profile.no_permission")}`)
} else {
toast.success(`${t("request.saved")}`)
}
})
setRestReq(props.request)
} catch (error) {
showSaveRequestModal.value = true
toast.error(`${t("error.something_went_wrong")}`)
console.error(error)
}
} else if (saveCtx.originLocation === "user-collection") {
try {
editRESTRequest(
saveCtx.folderPath,
saveCtx.requestIndex,
getRESTRequest()
)
setRestReq(props.request)
toast.success(`${t("request.saved")}`)
} catch (e) {
setRESTSaveContext(null)
saveCurrentRequest(null)
}
}
}
</script>

View File

@@ -1,191 +0,0 @@
<template>
<div>
<div class="sticky top-0 z-10 flex flex-col rounded-t bg-primary">
<tippy ref="options" interactive trigger="click" theme="popover" arrow>
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`"
class="flex-1 bg-transparent border-b border-dividerLight select-wrapper"
>
<ButtonSecondary
v-if="selectedEnvironmentIndex !== -1"
:label="environments[selectedEnvironmentIndex].name"
class="flex-1 !justify-start pr-8 rounded-none"
/>
<ButtonSecondary
v-else
:label="`${t('environment.select')}`"
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
class="flex flex-col"
tabindex="0"
role="menu"
@keyup.escape="hide()"
>
<SmartItem
:label="`${t('environment.no_environment')}`"
:info-icon="selectedEnvironmentIndex === -1 ? IconDone : null"
:active-info-icon="selectedEnvironmentIndex === -1"
@click="
() => {
selectedEnvironmentIndex = -1
hide()
}
"
/>
<hr v-if="environments.length > 0" />
<SmartItem
v-for="(gen, index) in environments"
:key="`gen-${index}`"
:label="gen.name"
:info-icon="index === selectedEnvironmentIndex ? IconDone : null"
:active-info-icon="index === selectedEnvironmentIndex"
@click="
() => {
selectedEnvironmentIndex = index
hide()
}
"
/>
</div>
</template>
</tippy>
<div class="flex justify-between flex-1 border-b border-dividerLight">
<ButtonSecondary
:icon="IconPlus"
:label="`${t('action.new')}`"
class="!rounded-none"
@click="displayModalAdd(true)"
/>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/environments"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconArchive"
:title="t('modal.import_export')"
@click="displayModalImportExport(true)"
/>
</div>
</div>
</div>
<div class="flex flex-col">
<EnvironmentsEnvironment
environment-index="Global"
:environment="globalEnvironment"
class="border-b border-dashed border-dividerLight"
@edit-environment="editEnvironment('Global')"
/>
<EnvironmentsEnvironment
v-for="(environment, index) in environments"
:key="`environment-${index}`"
:environment-index="index"
:environment="environment"
@edit-environment="editEnvironment(index)"
/>
</div>
<div
v-if="environments.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.environments") }}
</span>
<ButtonSecondary
:label="`${t('add.new')}`"
filled
class="mb-4"
@click="displayModalAdd(true)"
/>
</div>
<EnvironmentsDetails
:show="showModalDetails"
:action="action"
:editing-environment-index="editingEnvironmentIndex"
@hide-modal="displayModalEdit(false)"
/>
<EnvironmentsImportExport
:show="showModalImportExport"
@hide-modal="displayModalImportExport(false)"
/>
</div>
</template>
<script setup lang="ts">
import IconDone from "~icons/lucide/check"
import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconArchive from "~icons/lucide/archive"
import { computed, ref } from "vue"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import {
environments$,
setCurrentEnvironment,
selectedEnvIndex$,
globalEnv$,
} from "~/newstore/environments"
const t = useI18n()
const options = ref<any | null>(null)
const colorMode = useColorMode()
const globalEnv = useReadonlyStream(globalEnv$, [])
const globalEnvironment = computed(() => ({
name: "Global",
variables: globalEnv.value,
}))
const environments = useReadonlyStream(environments$, [])
const selectedEnvironmentIndex = useStream(
selectedEnvIndex$,
-1,
setCurrentEnvironment
)
const showModalImportExport = ref(false)
const showModalDetails = ref(false)
const action = ref<"new" | "edit">("edit")
const editingEnvironmentIndex = ref<number | "Global" | null>(null)
const displayModalAdd = (shouldDisplay: boolean) => {
action.value = "new"
showModalDetails.value = shouldDisplay
}
const displayModalEdit = (shouldDisplay: boolean) => {
action.value = "edit"
showModalDetails.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalImportExport = (shouldDisplay: boolean) => {
showModalImportExport.value = shouldDisplay
}
const editEnvironment = (environmentIndex: number | "Global") => {
editingEnvironmentIndex.value = environmentIndex
action.value = "edit"
displayModalEdit(true)
}
const resetSelectedData = () => {
editingEnvironmentIndex.value = null
}
</script>

View File

@@ -1,302 +0,0 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold text-secondaryLight">
{{ t("authorization.type") }}
</label>
<tippy
ref="authTypeOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<span class="select-wrapper">
<ButtonSecondary class="pr-8 ml-2 rounded-none" :label="authName" />
</span>
<template #content="{ hide }">
<div
class="flex flex-col"
tabindex="0"
role="menu"
@keyup.escape="hide()"
>
<SmartItem
label="None"
:icon="authName === 'None' ? IconCircleDot : IconCircle"
:active="authName === 'None'"
@click="
() => {
authType = 'none'
hide()
}
"
/>
<SmartItem
label="Basic Auth"
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
:active="authName === 'Basic Auth'"
@click="
() => {
authType = 'basic'
hide()
}
"
/>
<SmartItem
label="Bearer Token"
:icon="authName === 'Bearer' ? IconCircleDot : IconCircle"
:active="authName === 'Bearer'"
@click="
() => {
authType = 'bearer'
hide()
}
"
/>
<SmartItem
label="OAuth 2.0"
:icon="authName === 'OAuth 2.0' ? IconCircleDot : IconCircle"
:active="authName === 'OAuth 2.0'"
@click="
() => {
authType = 'oauth-2'
hide()
}
"
/>
<SmartItem
label="API key"
:icon="authName === 'API key' ? IconCircleDot : IconCircle"
:active="authName === 'API key'"
@click="
() => {
authType = 'api-key'
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
<div class="flex">
<!-- <SmartCheckbox
:on="!URLExcludes.auth"
@change="setExclude('auth', !$event)"
>
{{ $t("authorization.include_in_url") }}
</SmartCheckbox>-->
<SmartCheckbox
:on="authActive"
class="px-2"
@change="authActive = !authActive"
>{{ t("state.enabled") }}</SmartCheckbox
>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/authorization"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear')"
:icon="IconTrash2"
@click="clearContent"
/>
</div>
</div>
<div
v-if="authType === 'none'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/login.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.authorization')}`"
/>
<span class="pb-4 text-center">{{ t("empty.authorization") }}</span>
<ButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/features/authorization"
blank
:icon="IconExternalLink"
reverse
class="mb-4"
/>
</div>
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">
<div v-if="authType === 'basic'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="basicUsername"
:placeholder="t('authorization.username')"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="basicPassword"
:placeholder="t('authorization.password')"
/>
</div>
</div>
<div v-if="authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="bearerToken" placeholder="Token" />
</div>
</div>
<div v-if="authType === 'oauth-2'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="oauth2Token" placeholder="Token" />
</div>
<HttpOAuth2Authorization />
</div>
<div v-if="authType === 'api-key'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="apiKey" placeholder="Key" />
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="apiValue" placeholder="Value" />
</div>
<div class="flex items-center border-b border-dividerLight">
<span class="flex items-center">
<label class="ml-4 text-secondaryLight">
{{ t("authorization.pass_key_by") }}
</label>
<tippy
ref="addToOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<span class="select-wrapper">
<ButtonSecondary
:label="addTo || t('state.none')"
class="pr-8 ml-2 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
class="flex flex-col"
tabindex="0"
role="menu"
@keyup.escape="hide()"
>
<SmartItem
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
:active="addTo === 'Headers'"
:label="'Headers'"
@click="
() => {
addTo = 'Headers'
hide()
}
"
/>
<SmartItem
:icon="
addTo === 'Query params' ? IconCircleDot : IconCircle
"
:active="addTo === 'Query params'"
:label="'Query params'"
@click="
() => {
addTo = 'Query params'
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
</div>
<div
class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
>
<div class="pb-2 text-secondaryLight">
{{ t("helpers.authorization") }}
</div>
<SmartAnchor
class="link"
:label="t('authorization.learn')"
:icon="IconExternalLink"
to="https://docs.hoppscotch.io/features/authorization"
blank
reverse
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconExternalLink from "~icons/lucide/external-link"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle"
import { computed, ref, Ref } from "vue"
import {
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey,
} from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
const t = useI18n()
const colorMode = useColorMode()
const auth = useStream(
restAuth$,
{ authType: "none", authActive: true },
setRESTAuth
)
const authType = pluckRef(auth, "authType")
const authName = computed(() => {
if (authType.value === "basic") return "Basic Auth"
else if (authType.value === "bearer") return "Bearer"
else if (authType.value === "oauth-2") return "OAuth 2.0"
else if (authType.value === "api-key") return "API key"
else return "None"
})
const authActive = pluckRef(auth, "authActive")
const basicUsername = pluckRef(auth as Ref<HoppRESTAuthBasic>, "username")
const basicPassword = pluckRef(auth as Ref<HoppRESTAuthBasic>, "password")
const bearerToken = pluckRef(auth as Ref<HoppRESTAuthBearer>, "token")
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
const apiKey = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "key")
const apiValue = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "value")
const addTo = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "addTo")
if (typeof addTo.value === "undefined") {
addTo.value = "Headers"
apiKey.value = ""
apiValue.value = ""
}
const clearContent = () => {
auth.value = {
authType: "none",
authActive: true,
}
}
const authTypeOptions = ref<any | null>(null)
const addToOptions = ref<any | null>(null)
</script>

View File

@@ -1,117 +0,0 @@
<template>
<SmartModal
v-if="show"
dialog
:title="`${t('import.curl')}`"
@close="hideModal"
>
<template #body>
<div class="px-2 h-46">
<div
ref="curlEditor"
class="h-full border rounded border-dividerLight"
></div>
</div>
</template>
<template #footer>
<span class="flex">
<ButtonPrimary
ref="importButton"
:label="`${t('import.title')}`"
@click="handleImport"
/>
<ButtonSecondary :label="`${t('action.cancel')}`" @click="hideModal" />
</span>
<span class="flex">
<ButtonSecondary
:icon="pasteIcon"
:label="`${t('action.paste')}`"
filled
@click="handlePaste"
/>
</span>
</template>
</SmartModal>
</template>
<script setup lang="ts">
import { ref, watch } from "vue"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror"
import { setRESTRequest } from "~/newstore/RESTSession"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { parseCurlToHoppRESTReq } from "~/helpers/curl"
import IconClipboard from "~icons/lucide/clipboard"
import IconCheck from "~icons/lucide/check"
const t = useI18n()
const toast = useToast()
const curl = ref("")
const curlEditor = ref<any | null>(null)
const props = defineProps<{ show: boolean; text: string }>()
useCodemirror(curlEditor, curl, {
extendedEditorConfig: {
mode: "application/x-sh",
placeholder: `${t("request.enter_curl")}`,
},
linter: null,
completer: null,
environmentHighlights: false,
})
watch(
() => props.show,
() => {
if (props.show) {
curl.value = props.text.toString()
}
},
{ immediate: false }
)
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const hideModal = () => {
emit("hide-modal")
}
const handleImport = () => {
const text = curl.value
try {
const req = parseCurlToHoppRESTReq(text)
setRESTRequest(req)
} catch (e) {
console.error(e)
toast.error(`${t("error.curl_invalid_format")}`)
}
hideModal()
}
const pasteIcon = refAutoReset<typeof IconClipboard | typeof IconCheck>(
IconClipboard,
1000
)
const handlePaste = async () => {
try {
const text = await navigator.clipboard.readText()
if (text) {
curl.value = text
pasteIcon.value = IconCheck
}
} catch (e) {
console.error("Failed to copy: ", e)
toast.error(t("profile.no_permission").toString())
}
}
</script>

View File

@@ -1,119 +0,0 @@
<template>
<div class="flex flex-col">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="oidcDiscoveryURL"
placeholder="OpenID Connect Discovery URL"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="authURL" placeholder="Authorization URL" />
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="accessTokenURL" placeholder="Access Token URL" />
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="clientSecret" placeholder="Client Secret" />
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="scope" placeholder="Scope" />
</div>
<div class="p-2">
<ButtonSecondary
filled
:label="`${t('authorization.generate_token')}`"
@click="handleAccessTokenRequest()"
/>
</div>
</div>
</template>
<script lang="ts">
import { Ref, defineComponent } from "vue"
import { HoppRESTAuthOAuth2, parseTemplateString } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useStream } from "@composables/stream"
import { useToast } from "@composables/toast"
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
import { tokenRequest } from "~/helpers/oauth"
import { getCombinedEnvVariables } from "~/helpers/preRequest"
export default defineComponent({
setup() {
const t = useI18n()
const toast = useToast()
const auth = useStream(
restAuth$,
{ authType: "none", authActive: true },
setRESTAuth
)
const oidcDiscoveryURL = pluckRef(
auth as Ref<HoppRESTAuthOAuth2>,
"oidcDiscoveryURL"
)
const authURL = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "authURL")
const accessTokenURL = pluckRef(
auth as Ref<HoppRESTAuthOAuth2>,
"accessTokenURL"
)
const clientID = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "clientID")
const clientSecret = pluckRef(
auth as Ref<HoppRESTAuthOAuth2>,
"clientSecret"
)
const scope = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "scope")
const handleAccessTokenRequest = async () => {
if (
oidcDiscoveryURL.value === "" &&
(authURL.value === "" || accessTokenURL.value === "")
) {
toast.error(`${t("error.incomplete_config_urls")}`)
return
}
const envs = getCombinedEnvVariables()
const envVars = [...envs.selected, ...envs.global]
try {
const tokenReqParams = {
grantType: "code",
oidcDiscoveryUrl: parseTemplateString(
oidcDiscoveryURL.value,
envVars
),
authUrl: parseTemplateString(authURL.value, envVars),
accessTokenUrl: parseTemplateString(accessTokenURL.value, envVars),
clientId: parseTemplateString(clientID.value, envVars),
clientSecret: parseTemplateString(clientSecret.value, envVars),
scope: parseTemplateString(scope.value, envVars),
}
await tokenRequest(tokenReqParams)
} catch (e) {
toast.error(`${e}`)
}
}
return {
oidcDiscoveryURL,
authURL,
accessTokenURL,
clientID,
clientSecret,
scope,
handleAccessTokenRequest,
t,
}
},
})
</script>

View File

@@ -1,95 +0,0 @@
<template>
<SmartTabs
v-model="selectedRealtimeTab"
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
render-inactive-tabs
>
<SmartTab
:id="'params'"
:label="`${t('tab.parameters')}`"
:info="`${newActiveParamsCount$}`"
>
<HttpParameters />
</SmartTab>
<SmartTab :id="'bodyParams'" :label="`${t('tab.body')}`">
<HttpBody @change-tab="changeTab" />
</SmartTab>
<SmartTab
:id="'headers'"
:label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`"
>
<HttpHeaders @change-tab="changeTab" />
</SmartTab>
<SmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
<HttpAuthorization />
</SmartTab>
<SmartTab
:id="'preRequestScript'"
:label="`${t('tab.pre_request_script')}`"
:indicator="
preRequestScript && preRequestScript.length > 0 ? true : false
"
>
<HttpPreRequestScript />
</SmartTab>
<SmartTab
:id="'tests'"
:label="`${t('tab.tests')}`"
:indicator="testScript && testScript.length > 0 ? true : false"
>
<HttpTests />
</SmartTab>
</SmartTabs>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { map } from "rxjs/operators"
import { useReadonlyStream } from "@composables/stream"
import {
restActiveHeadersCount$,
restActiveParamsCount$,
usePreRequestScript,
useTestScript,
} from "~/newstore/RESTSession"
import { useI18n } from "@composables/i18n"
export type RequestOptionTabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
const t = useI18n()
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
const changeTab = (e: RequestOptionTabs) => {
selectedRealtimeTab.value = e
}
const newActiveParamsCount$ = useReadonlyStream(
restActiveParamsCount$.pipe(
map((e) => {
if (e === 0) return null
return `${e}`
})
),
null
)
const newActiveHeadersCount$ = useReadonlyStream(
restActiveHeadersCount$.pipe(
map((e) => {
if (e === 0) return null
return `${e}`
})
),
null
)
const preRequestScript = usePreRequestScript()
const testScript = useTestScript()
</script>

View File

@@ -1,42 +0,0 @@
<template>
<div class="flex flex-col flex-1">
<HttpResponseMeta :response="response" />
<LensesResponseBodyRenderer
v-if="!loading && hasResponse"
:response="response"
/>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, watch } from "vue"
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
import { useReadonlyStream } from "@composables/stream"
import { restResponse$ } from "~/newstore/RESTSession"
export default defineComponent({
setup() {
const response = useReadonlyStream(restResponse$, null)
const hasResponse = computed(
() =>
response.value?.type === "success" || response.value?.type === "fail"
)
const loading = computed(
() => response.value === null || response.value.type === "loading"
)
watch(response, () => {
if (response.value?.type === "loading") startPageProgress()
else completePageProgress()
})
return {
hasResponse,
response,
loading,
}
},
})
</script>

View File

@@ -1,96 +0,0 @@
<template>
<SmartTabs
v-if="response"
v-model="selectedLensTab"
styles="sticky z-10 bg-primary top-lowerPrimaryStickyFold"
render-inactive-tabs
>
<SmartTab
v-for="(lens, index) in validLenses"
:id="lens.renderer"
:key="`lens-${index}`"
:label="t(lens.lensName)"
class="flex flex-col flex-1 w-full h-full"
>
<component :is="lens.renderer" :response="response" />
</SmartTab>
<SmartTab
v-if="headerLength"
id="headers"
:label="t('response.headers')"
:info="`${headerLength}`"
class="flex flex-col flex-1"
>
<LensesHeadersRenderer :headers="response.headers" />
</SmartTab>
<SmartTab
id="results"
:label="t('test.results')"
:indicator="
testResults &&
(testResults.expectResults.length ||
testResults.tests.length ||
testResults.envDiff.selected.additions.length ||
testResults.envDiff.selected.updations.length ||
testResults.envDiff.global.updations.length)
? true
: false
"
class="flex flex-col flex-1"
>
<HttpTestResult />
</SmartTab>
</SmartTabs>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import { getSuitableLenses, getLensRenderers } from "~/helpers/lenses/lenses"
import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { restTestResults$ } from "~/newstore/RESTSession"
export default defineComponent({
components: {
// Lens Renderers
...getLensRenderers(),
},
props: {
response: { type: Object, default: () => ({}) },
},
setup() {
const testResults = useReadonlyStream(restTestResults$, null)
return {
testResults,
t: useI18n(),
}
},
data() {
return {
selectedLensTab: "",
}
},
computed: {
headerLength() {
if (!this.response || !this.response.headers) return 0
return Object.keys(this.response.headers).length
},
validLenses() {
if (!this.response) return []
return getSuitableLenses(this.response)
},
},
watch: {
validLenses: {
handler(newValue) {
if (newValue.length === 0) return
this.selectedLensTab = newValue[0].renderer
},
immediate: true,
},
},
})
</script>

View File

@@ -1,116 +0,0 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
:icon="downloadIcon === 'download' ? IconDownload : IconCheck"
@click="downloadResponse"
/>
</div>
</div>
<img
class="flex max-w-full border-b border-dividerLight"
:src="imageSource"
loading="lazy"
:alt="imageSource"
/>
</div>
</template>
<script lang="ts">
import IconDownload from "~icons/lucide/download"
import IconCheck from "~icons/lucide/check"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { defineComponent } from "vue"
export default defineComponent({
props: {
response: { type: Object, default: () => ({}) },
},
setup() {
return {
t: useI18n(),
toast: useToast(),
IconDownload,
IconCheck,
}
},
data() {
return {
imageSource: "",
downloadIcon: "download",
}
},
computed: {
responseType() {
return (
this.response.headers.find(
(h) => h.key.toLowerCase() === "content-type"
).value || ""
)
.split(";")[0]
.toLowerCase()
},
},
watch: {
response: {
immediate: true,
handler() {
this.imageSource = ""
const buf = this.response.body
const bytes = new Uint8Array(buf)
const blob = new Blob([bytes.buffer])
const reader = new FileReader()
reader.onload = ({ target }) => {
this.imageSource = target.result
}
reader.readAsDataURL(blob)
},
},
},
mounted() {
this.imageSource = ""
const buf = this.response.body
const bytes = new Uint8Array(buf)
const blob = new Blob([bytes.buffer])
const reader = new FileReader()
reader.onload = ({ target }) => {
this.imageSource = target.result
}
reader.readAsDataURL(blob)
},
methods: {
downloadResponse() {
const dataToWrite = this.response.body
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"))
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
this.downloadIcon = "download"
}, 1000)
},
},
})
</script>

View File

@@ -1,28 +0,0 @@
<template>
<div
class="relative flex flex-col overflow-hidden space-y-2"
:class="expand ? 'h-full' : 'max-h-32'"
>
<slot name="body"></slot>
<div class="sticky inset-x-0 bottom-0 flex items-center justify-center">
<ButtonSecondary
:icon="expand ? IconChevronUp : IconChevronDown"
:label="expand ? t('action.less') : t('action.more')"
filled
rounded
@click="expand = !expand"
/>
</div>
</div>
</template>
<script setup lang="ts">
import IconChevronUp from "~icons/lucide/chevron-up"
import IconChevronDown from "~icons/lucide/chevron-down"
import { ref } from "vue"
import { useI18n } from "@composables/i18n"
const t = useI18n()
const expand = ref(false)
</script>

View File

@@ -1,140 +0,0 @@
<template>
<SmartLink
:to="to"
:exact="exact"
:blank="blank"
class="inline-flex items-center flex-shrink-0 px-4 py-2 rounded transition hover:bg-primaryDark hover:text-secondaryDark focus:outline-none focus-visible:bg-primaryDark focus-visible:text-secondaryDark"
:class="[
{ 'opacity-75 cursor-not-allowed': disabled },
{ 'pointer-events-none': loading },
{ 'flex-1': label },
{ 'flex-row-reverse justify-end': reverse },
{
'border border-divider hover:border-dividerDark focus-visible:border-dividerDark':
outline,
},
]"
:disabled="disabled"
:tabindex="loading ? '-1' : '0'"
role="menuitem"
>
<span
v-if="!loading"
class="inline-flex items-center"
:class="{ 'self-start': !!infoIcon }"
>
<component
:is="icon"
v-if="icon"
class="opacity-75 svg-icons"
:class="[
label ? (reverse ? 'ml-4' : 'mr-4') : '',
{ 'text-accent': active },
]"
/>
</span>
<SmartSpinner v-else class="mr-4 text-secondaryDark" />
<div
class="inline-flex items-start flex-1 truncate"
:class="{ 'flex-col': description }"
>
<div class="font-semibold truncate">
{{ label }}
</div>
<p v-if="description" class="my-2 text-left text-secondaryLight">
{{ description }}
</p>
</div>
<component
:is="infoIcon"
v-if="infoIcon"
class="items-center self-center ml-4 svg-icons"
:class="{ 'text-accent': activeInfoIcon }"
/>
<div v-if="shortcut.length" class="ml-2 <sm:hidden font-medium">
<kbd
v-for="(key, index) in shortcut"
:key="`key-${index}`"
class="-mr-2 shortcut-key"
>
{{ key }}
</kbd>
</div>
</SmartLink>
</template>
<script setup lang="ts">
defineProps({
to: {
type: String,
default: "",
},
exact: {
type: Boolean,
default: true,
},
blank: {
type: Boolean,
default: false,
},
label: {
type: String,
default: "",
},
description: {
type: String,
default: "",
},
/**
* This will be a component!
*/
icon: {
type: Object,
default: null,
},
/**
* This will be a component!
*/
svg: {
type: Object,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
reverse: {
type: Boolean,
default: false,
},
outline: {
type: Boolean,
default: false,
},
shortcut: {
type: Array,
default: () => [],
},
active: {
type: Boolean,
default: false,
},
activeInfoIcon: {
type: Boolean,
default: false,
},
/**
* This will be a component!
*/
infoIcon: {
type: Object,
default: null,
},
})
</script>

View File

@@ -1,3 +0,0 @@
<template>
<icon-lucide-loader class="animate-spin svg-icons" />
</template>

View File

@@ -1,77 +0,0 @@
/* An `action` is a unique verb that is associated with certain thing that can be done on Hoppscotch.
* For example, sending a request.
*/
import { onBeforeUnmount, onMounted } from "vue"
import { BehaviorSubject } from "rxjs"
export type HoppAction =
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
| "request.reset" // Clear request data
| "request.copy-link" // Copy Request Link
| "request.save" // Save to Collections
| "request.save-as" // Save As
| "request.method.next" // Select Next Method
| "request.method.prev" // Select Previous Method
| "request.method.get" // Select GET Method
| "request.method.head" // Select HEAD Method
| "request.method.post" // Select POST Method
| "request.method.put" // Select PUT Method
| "request.method.delete" // Select DELETE Method
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
| "modals.search.toggle" // Shows the search modal
| "modals.support.toggle" // Shows the support modal
| "modals.share.toggle" // Shows the share modal
| "navigation.jump.rest" // Jump to REST page
| "navigation.jump.graphql" // Jump to GraphQL page
| "navigation.jump.realtime" // Jump to realtime page
| "navigation.jump.documentation" // Jump to documentation page
| "navigation.jump.settings" // Jump to settings page
| "navigation.jump.profile" // Jump to profile page
| "settings.theme.system" // Use system theme
| "settings.theme.light" // Use light theme
| "settings.theme.dark" // Use dark theme
| "settings.theme.black" // Use black theme
type BoundActionList = {
// eslint-disable-next-line no-unused-vars
[_ in HoppAction]?: Array<() => void>
}
const boundActions: BoundActionList = {}
export const activeActions$ = new BehaviorSubject<HoppAction[]>([])
export function bindAction(action: HoppAction, handler: () => void) {
if (boundActions[action]) {
boundActions[action]?.push(handler)
} else {
boundActions[action] = [handler]
}
activeActions$.next(Object.keys(boundActions) as HoppAction[])
}
export function invokeAction(action: HoppAction) {
boundActions[action]?.forEach((handler) => handler())
}
export function unbindAction(action: HoppAction, handler: () => void) {
boundActions[action] = boundActions[action]?.filter((x) => x !== handler)
if (boundActions[action]?.length === 0) {
delete boundActions[action]
}
activeActions$.next(Object.keys(boundActions) as HoppAction[])
}
export function defineActionHandler(action: HoppAction, handler: () => void) {
onMounted(() => {
bindAction(action, handler)
})
onBeforeUnmount(() => {
unbindAction(action, handler)
})
}

View File

@@ -1,9 +0,0 @@
import { GraphCacheKeysConfig } from "../graphql"
export const keyDefs: GraphCacheKeysConfig = {
User: (data) => data.uid!,
TeamMember: (data) => data.membershipID!,
Team: (data) => data.id!,
Shortcode: (data) => data.id!,
TeamInvitation: (data) => data.id!,
}

View File

@@ -1,3 +0,0 @@
import { GraphCacheOptimisticUpdaters } from "../graphql"
export const optimisticDefs: GraphCacheOptimisticUpdaters = {}

View File

@@ -1,31 +0,0 @@
import {
GraphCacheResolvers,
Shortcode,
Team,
TeamInvitation,
WithTypename,
} from "../graphql"
export const resolversDef: GraphCacheResolvers = {
Query: {
team: (_parent, { teamID }) =>
<WithTypename<Team>>{
__typename: "Team" as const,
id: teamID,
},
user: (_parent, { uid }) => ({
__typename: "User",
uid,
}),
teamInvitation: (_parent, args) =>
<WithTypename<TeamInvitation>>{
__typename: "TeamInvitation",
id: args.inviteID,
},
shortcode: (_parent, args) =>
<WithTypename<Shortcode>>{
__typename: "Shortcode",
id: args.code,
},
},
}

View File

@@ -1,122 +0,0 @@
import { gql } from "@urql/core"
import { GraphCacheUpdaters } from "../graphql"
export const updatesDef: GraphCacheUpdaters = {
Subscription: {
teamMemberAdded: (_r, { teamID }, cache) => {
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"teamMembers"
)
},
teamMemberUpdated: (_r, { teamID }, cache) => {
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"teamMembers"
)
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"myRole"
)
},
teamMemberRemoved: (_r, { teamID }, cache) => {
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"teamMembers"
)
},
teamInvitationAdded: (_r, { teamID }, cache) => {
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"teamInvitations"
)
},
teamInvitationRemoved: (_r, { teamID }, cache) => {
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"teamInvitations"
)
},
},
Mutation: {
createTeamInvitation: (result, _args, cache) => {
cache.invalidate(
{
__typename: "Team",
id: result.createTeamInvitation.teamID!,
},
"teamInvitations"
)
},
acceptTeamInvitation: (_result, _args, cache) => {
cache.invalidate({ __typename: "Query" }, "myTeams")
},
revokeTeamInvitation: (_result, args, cache) => {
const targetTeamID = cache.resolve(
{
__typename: "TeamInvitation",
id: args.inviteID,
},
"teamID"
)
if (typeof targetTeamID === "string") {
const newInvites = (
cache.resolve(
{
__typename: "Team",
id: targetTeamID,
},
"teamInvitations"
) as string[]
).filter(
(inviteKey) =>
inviteKey !==
cache.keyOfEntity({
__typename: "TeamInvitation",
id: args.inviteID,
})
)
cache.link(
{ __typename: "Team", id: targetTeamID },
"teamInvitations",
newInvites
)
}
},
createShortcode: (result, _args, cache) => {
cache.writeFragment(
gql`
fragment _ on Shortcode {
id
request
}
`,
{
id: result.createShortcode.id,
request: result.createShortcode.request,
}
)
},
},
}

View File

@@ -1,5 +0,0 @@
mutation MoveRESTTeamRequest($requestID: ID!, $collectionID: ID!) {
moveRequest(requestID: $requestID, destCollID: $collectionID) {
id
}
}

View File

@@ -1,20 +0,0 @@
import { runMutation } from "../GQLClient"
import {
MoveRestTeamRequestDocument,
MoveRestTeamRequestMutation,
MoveRestTeamRequestMutationVariables,
} from "../graphql"
type MoveRestTeamRequestErrors =
| "team_req/not_found"
| "team_req/invalid_target_id"
export const moveRESTTeamRequest = (requestID: string, collectionID: string) =>
runMutation<
MoveRestTeamRequestMutation,
MoveRestTeamRequestMutationVariables,
MoveRestTeamRequestErrors
>(MoveRestTeamRequestDocument, {
requestID,
collectionID,
})

View File

@@ -1,423 +0,0 @@
import {
User,
getAuth,
onAuthStateChanged,
onIdTokenChanged,
signInWithPopup,
GoogleAuthProvider,
GithubAuthProvider,
OAuthProvider,
signInWithEmailAndPassword as signInWithEmailAndPass,
isSignInWithEmailLink as isSignInWithEmailLinkFB,
fetchSignInMethodsForEmail,
sendSignInLinkToEmail,
signInWithEmailLink as signInWithEmailLinkFB,
ActionCodeSettings,
signOut,
linkWithCredential,
AuthCredential,
AuthError,
UserCredential,
updateProfile,
updateEmail,
sendEmailVerification,
reauthenticateWithCredential,
} from "firebase/auth"
import {
onSnapshot,
getFirestore,
setDoc,
doc,
updateDoc,
} from "firebase/firestore"
import { BehaviorSubject, filter, Subject, Subscription } from "rxjs"
import {
setLocalConfig,
getLocalConfig,
removeLocalConfig,
} from "~/newstore/localpersistence"
export type HoppUser = User & {
provider?: string
accessToken?: string
}
export type AuthEvent =
| { event: "probable_login"; user: HoppUser } // We have previous login state, but the app is waiting for authentication
| { event: "login"; user: HoppUser } // We are authenticated
| { event: "logout" } // No authentication and we have no previous state
| { event: "authTokenUpdate"; user: HoppUser; newToken: string | null } // Token has been updated
/**
* A BehaviorSubject emitting the currently logged in user (or null if not logged in)
*/
export const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
/**
* A BehaviorSubject emitting the current idToken
*/
export const authIdToken$ = new BehaviorSubject<string | null>(null)
/**
* A subject that emits events related to authentication flows
*/
export const authEvents$ = new Subject<AuthEvent>()
/**
* Like currentUser$ but also gives probable user value
*/
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
/**
* Resolves when the probable login resolves into proper login
*/
export const waitProbableLoginToConfirm = () =>
new Promise<void>((resolve, reject) => {
if (authIdToken$.value) resolve()
if (!probableUser$.value) reject(new Error("no_probable_user"))
let sub: Subscription | null = null
sub = authIdToken$.pipe(filter((token) => !!token)).subscribe(() => {
sub?.unsubscribe()
resolve()
})
})
/**
* Initializes the firebase authentication related subjects
*/
export function initAuth() {
const auth = getAuth()
const firestore = getFirestore()
let extraSnapshotStop: (() => void) | null = null
probableUser$.next(JSON.parse(getLocalConfig("login_state") ?? "null"))
onAuthStateChanged(auth, (user) => {
/** Whether the user was logged in before */
const wasLoggedIn = currentUser$.value !== null
if (user) {
probableUser$.next(user)
} else {
probableUser$.next(null)
removeLocalConfig("login_state")
}
if (!user && extraSnapshotStop) {
extraSnapshotStop()
extraSnapshotStop = null
} else if (user) {
// Merge all the user info from all the authenticated providers
user.providerData.forEach((profile) => {
if (!profile) return
const us = {
updatedOn: new Date(),
provider: profile.providerId,
name: profile.displayName,
email: profile.email,
photoUrl: profile.photoURL,
uid: profile.uid,
}
setDoc(doc(firestore, "users", user.uid), us, { merge: true }).catch(
(e) => console.error("error updating", us, e)
)
})
extraSnapshotStop = onSnapshot(
doc(firestore, "users", user.uid),
(doc) => {
const data = doc.data()
const userUpdate: HoppUser = user
if (data) {
// Write extra provider data
userUpdate.provider = data.provider
userUpdate.accessToken = data.accessToken
}
currentUser$.next(userUpdate)
}
)
}
currentUser$.next(user)
// User wasn't found before, but now is there (login happened)
if (!wasLoggedIn && user) {
authEvents$.next({
event: "login",
user: currentUser$.value!,
})
} else if (wasLoggedIn && !user) {
// User was found before, but now is not there (logout happened)
authEvents$.next({
event: "logout",
})
}
})
onIdTokenChanged(auth, async (user) => {
if (user) {
authIdToken$.next(await user.getIdToken())
authEvents$.next({
event: "authTokenUpdate",
newToken: authIdToken$.value,
user: currentUser$.value!, // Force not-null because user is defined
})
setLocalConfig("login_state", JSON.stringify(user))
} else {
authIdToken$.next(null)
}
})
}
export function getAuthIDToken(): string | null {
return authIdToken$.getValue()
}
/**
* Sign user in with a popup using Google
*/
export async function signInUserWithGoogle() {
return await signInWithPopup(getAuth(), new GoogleAuthProvider())
}
/**
* Sign user in with a popup using Github
*/
export async function signInUserWithGithub() {
return await signInWithPopup(
getAuth(),
new GithubAuthProvider().addScope("gist")
)
}
/**
* Sign user in with a popup using Microsoft
*/
export async function signInUserWithMicrosoft() {
return await signInWithPopup(getAuth(), new OAuthProvider("microsoft.com"))
}
/**
* Sign user in with email and password
*/
export async function signInWithEmailAndPassword(
email: string,
password: string
) {
return await signInWithEmailAndPass(getAuth(), email, password)
}
/**
* Gets the sign in methods for a given email address
*
* @param email - Email to get the methods of
*
* @returns Promise for string array of the auth provider methods accessible
*/
export async function getSignInMethodsForEmail(email: string) {
return await fetchSignInMethodsForEmail(getAuth(), email)
}
export async function linkWithFBCredential(
user: User,
credential: AuthCredential
) {
return await linkWithCredential(user, credential)
}
/**
* Links account with another account given in a auth/account-exists-with-different-credential error
*
* @param user - User who has the errors
*
* @param error - Error caught after trying to login
*
* @returns Promise of UserCredential
*/
export async function linkWithFBCredentialFromAuthError(
user: User,
error: unknown
) {
// Marked as not null since this function is supposed to be called after an auth/account-exists-with-different-credential error, ie credentials actually exist
const credentials = OAuthProvider.credentialFromError(error as AuthError)!
return await linkWithCredential(user, credentials)
}
/**
* Sends an email with the signin link to the user
*
* @param email - Email to send the email to
* @param actionCodeSettings - The settings to apply to the link
*/
export async function signInWithEmail(
email: string,
actionCodeSettings: ActionCodeSettings
) {
return await sendSignInLinkToEmail(getAuth(), email, actionCodeSettings)
}
/**
* Checks and returns whether the sign in link is an email link
*
* @param url - The URL to look in
*/
export function isSignInWithEmailLink(url: string) {
return isSignInWithEmailLinkFB(getAuth(), url)
}
/**
* Sends an email with sign in with email link
*
* @param email - Email to log in to
* @param url - The action URL which is used to validate login
*/
export async function signInWithEmailLink(email: string, url: string) {
return await signInWithEmailLinkFB(getAuth(), email, url)
}
/**
* Signs out the user
*/
export async function signOutUser() {
if (!currentUser$.value) throw new Error("No user has logged in")
await signOut(getAuth())
}
/**
* Sets the provider id and relevant provider auth token
* as user metadata
*
* @param id - The provider ID
* @param token - The relevant auth token for the given provider
*/
export async function setProviderInfo(id: string, token: string) {
if (!currentUser$.value) throw new Error("No user has logged in")
const us = {
updatedOn: new Date(),
provider: id,
accessToken: token,
}
try {
await updateDoc(
doc(getFirestore(), "users", currentUser$.value.uid),
us
).catch((e) => console.error("error updating", us, e))
} catch (e) {
console.error("error updating", e)
throw e
}
}
/**
* Sets the user's display name
*
* @param name - The new display name
*/
export async function setDisplayName(name: string) {
if (!currentUser$.value) throw new Error("No user has logged in")
const us = {
displayName: name,
}
try {
await updateProfile(currentUser$.value, us)
} catch (e) {
console.error("error updating", e)
throw e
}
}
/**
* Send user's email address verification mail
*/
export async function verifyEmailAddress() {
if (!currentUser$.value) throw new Error("No user has logged in")
try {
await sendEmailVerification(currentUser$.value)
} catch (e) {
console.error("error updating", e)
throw e
}
}
/**
* Sets the user's email address
*
* @param email - The new email address
*/
export async function setEmailAddress(email: string) {
if (!currentUser$.value) throw new Error("No user has logged in")
try {
await updateEmail(currentUser$.value, email)
} catch (e) {
await reauthenticateUser()
console.error("error updating", e)
throw e
}
}
/**
* Reauthenticate the user with the given credential
*/
async function reauthenticateUser() {
if (!currentUser$.value) throw new Error("No user has logged in")
const currentAuthMethod = currentUser$.value.provider
let credential
if (currentAuthMethod === "google.com") {
const result = await signInUserWithGithub()
credential = GithubAuthProvider.credentialFromResult(result)
} else if (currentAuthMethod === "github.com") {
const result = await signInUserWithGoogle()
credential = GoogleAuthProvider.credentialFromResult(result)
} else if (currentAuthMethod === "microsoft.com") {
const result = await signInUserWithMicrosoft()
credential = OAuthProvider.credentialFromResult(result)
} else if (currentAuthMethod === "password") {
const email = prompt(
"Reauthenticate your account using your current email:"
)
const actionCodeSettings = {
url: `${process.env.BASE_URL}/enter`,
handleCodeInApp: true,
}
await signInWithEmail(email as string, actionCodeSettings)
.then(() =>
alert(
`Check your inbox - we sent an email to ${email}. It contains a magic link that will reauthenticate your account.`
)
)
.catch((e) => {
alert(`Error: ${e.message}`)
console.error(e)
})
return
}
try {
await reauthenticateWithCredential(
currentUser$.value,
credential as AuthCredential
)
} catch (e) {
console.error("error updating", e)
throw e
}
}
export function getGithubCredentialFromResult(result: UserCredential) {
return GithubAuthProvider.credentialFromResult(result)
}

View File

@@ -1,84 +0,0 @@
import {
audit,
combineLatest,
distinctUntilChanged,
EMPTY,
from,
map,
Subscription,
} from "rxjs"
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"
import { cloneDeep } from "lodash-es"
import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data"
import { currentUser$, HoppUser } from "./auth"
import { restRequest$ } from "~/newstore/RESTSession"
/**
* Writes a request to a user's firestore sync
*
* @param user The user to write to
* @param request The request to write to the request sync
*/
function writeCurrentRequest(user: HoppUser, request: HoppRESTRequest) {
const req = cloneDeep(request)
// Remove FormData entries because those can't be stored on Firestore
if (req.body.contentType === "multipart/form-data") {
req.body.body = req.body.body.map((formData) => {
if (!formData.isFile) return formData
return {
active: formData.active,
isFile: false,
key: formData.key,
value: "",
}
})
}
return setDoc(doc(getFirestore(), "users", user.uid, "requests", "rest"), req)
}
/**
* Loads the synced request from the firestore sync
*
* @returns Fetched request object if exists else null
*/
export async function loadRequestFromSync(): Promise<HoppRESTRequest | null> {
const currentUser = currentUser$.value
if (!currentUser)
throw new Error("Cannot load request from sync without login")
const fbDoc = await getDoc(
doc(getFirestore(), "users", currentUser.uid, "requests", "rest")
)
const data = fbDoc.data()
if (!data) return null
else return translateToNewRequest(data)
}
/**
* Performs sync of the REST Request session with Firestore.
*
* @returns A subscription to the sync observable stream.
* Unsubscribe to stop syncing.
*/
export function startRequestSync(): Subscription {
const sub = combineLatest([
currentUser$,
restRequest$.pipe(distinctUntilChanged()),
])
.pipe(
map(([user, request]) =>
user ? from(writeCurrentRequest(user, request)) : EMPTY
),
audit((x) => x)
)
.subscribe(() => {
// NOTE: This subscription should be kept
})
return sub
}

View File

@@ -1,75 +0,0 @@
import { pipe } from "fp-ts/function"
import * as E from "fp-ts/Either"
import { BehaviorSubject } from "rxjs"
import { authIdToken$ } from "../fb/auth"
import { runGQLQuery } from "../backend/GQLClient"
import { GetUserInfoDocument } from "../backend/graphql"
/*
* This file deals with interfacing data provided by the
* Hoppscotch Backend server
*/
/**
* Defines the information provided about a user
*/
export interface UserInfo {
/**
* UID of the user
*/
uid: string
/**
* Displayable name of the user (or null if none available)
*/
displayName: string | null
/**
* Email of the user (or null if none available)
*/
email: string | null
/**
* URL to the profile photo of the user (or null if none available)
*/
photoURL: string | null
}
/**
* An observable subject onto the currently logged in user info (is null if not logged in)
*/
export const currentUserInfo$ = new BehaviorSubject<UserInfo | null>(null)
/**
* Initializes the currenUserInfo$ view and sets up its update mechanism
*/
export function initUserInfo() {
authIdToken$.subscribe((token) => {
if (token) {
updateUserInfo()
} else {
currentUserInfo$.next(null)
}
})
}
/**
* Runs the actual user info fetching
*/
async function updateUserInfo() {
const result = await runGQLQuery({
query: GetUserInfoDocument,
})
currentUserInfo$.next(
pipe(
result,
E.matchW(
() => null,
(x) => ({
uid: x.me.uid,
displayName: x.me.displayName ?? null,
email: x.me.email ?? null,
photoURL: x.me.photoURL ?? null,
})
)
)
)
}

View File

@@ -1,34 +0,0 @@
import { createApp } from "vue"
import { setupLocalPersistence } from "./newstore/localpersistence"
import { performMigrations } from "./helpers/migrations"
import { initializeFirebase } from "./helpers/fb"
import { initUserInfo } from "./helpers/teams/BackendUserInfo"
import { HOPP_MODULES } from "@modules/."
import "virtual:windi.css"
import "../assets/scss/themes.scss"
import "../assets/scss/styles.scss"
import "nprogress/nprogress.css"
import App from "./App.vue"
const app = createApp(App)
// Some basic work that needs to be done before module inits even
initializeFirebase()
setupLocalPersistence()
performMigrations()
initUserInfo()
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
app.mount("#app")
console.info(
"%cWe ❤︎ open source!",
"background-color:white;padding:8px 16px;border-radius:8px;font-size:32px;color:red;"
)
console.info(
"%cContribute: https://github.com/hoppscotch/hoppscotch",
"background-color:black;padding:4px 8px;border-radius:8px;font-size:16px;color:white;"
)

View File

@@ -1,91 +0,0 @@
import { HoppModule } from "."
import * as Sentry from "@sentry/vue"
import { BrowserTracing } from "@sentry/tracing"
import { Route } from "@sentry/vue/types/router"
import { RouteLocationNormalized, Router } from "vue-router"
import { settingsStore } from "~/newstore/settings"
import { App } from "vue"
import { APP_IS_IN_DEV_MODE } from "~/helpers/dev"
interface SentryVueRouter {
onError: (fn: (err: Error) => void) => void
beforeEach: (fn: (to: Route, from: Route, next: () => void) => void) => void
}
function normalizedRouteToSentryRoute(route: RouteLocationNormalized): Route {
return {
matched: route.matched,
// route.params' type translates just to a fancy version of this, hence assertion
params: route.params as Route["params"],
path: route.path,
// route.query's type translates just to a fancy version of this, hence assertion
query: route.query as Route["query"],
name: route.name,
}
}
function getInstrumentationVueRouter(router: Router): SentryVueRouter {
return <SentryVueRouter>{
onError: router.onError,
beforeEach(func) {
router.beforeEach((to, from, next) => {
func(
normalizedRouteToSentryRoute(to),
normalizedRouteToSentryRoute(from),
next
)
})
},
}
}
let sentryActive = false
function initSentry(dsn: string, router: Router, app: App) {
Sentry.init({
app,
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: APP_IS_IN_DEV_MODE
? "dev"
: import.meta.env.VITE_SENTRY_ENVIRONMENT,
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(
getInstrumentationVueRouter(router)
),
// TODO: We may want to limit this later on
tracingOrigins: [new URL(import.meta.env.VITE_BACKEND_GQL_URL).origin],
}),
],
tracesSampleRate: 0.8,
})
sentryActive = true
}
function deinitSentry() {
Sentry.close()
sentryActive = false
}
export default <HoppModule>{
onRouterInit(app, router) {
if (!import.meta.env.VITE_SENTRY_DSN) {
console.log(
"Sentry tracing is not enabled because 'VITE_SENTRY_DSN' env is not defined"
)
return
}
if (settingsStore.value.TELEMETRY_ENABLED) {
initSentry(import.meta.env.VITE_SENTRY_DSN, router, app)
}
settingsStore.subject$.subscribe(({ TELEMETRY_ENABLED }) => {
if (!TELEMETRY_ENABLED && sentryActive) {
deinitSentry()
} else if (TELEMETRY_ENABLED && !sentryActive) {
initSentry(import.meta.env.VITE_SENTRY_DSN!, router, app)
}
})
},
}

View File

@@ -1,710 +0,0 @@
import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators"
import { Ref } from "vue"
import {
FormDataKeyValue,
HoppRESTHeader,
HoppRESTParam,
HoppRESTReqBody,
HoppRESTRequest,
RESTReqSchemaVersion,
HoppRESTAuth,
ValidContentTypes,
} from "@hoppscotch/data"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { useStream } from "@composables/stream"
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
type RESTSession = {
request: HoppRESTRequest
response: HoppRESTResponse | null
testResults: HoppTestResult | null
saveContext: HoppRequestSaveContext | null
}
export const getDefaultRESTRequest = (): HoppRESTRequest => ({
v: RESTReqSchemaVersion,
endpoint: "https://echo.hoppscotch.io",
name: "Untitled request",
params: [],
headers: [],
method: "GET",
auth: {
authType: "none",
authActive: true,
},
preRequestScript: "",
testScript: "",
body: {
contentType: null,
body: null,
},
})
const defaultRESTSession: RESTSession = {
request: getDefaultRESTRequest(),
response: null,
testResults: null,
saveContext: null,
}
const dispatchers = defineDispatchers({
setRequest(_: RESTSession, { req }: { req: HoppRESTRequest }) {
return {
request: req,
}
},
setRequestName(curr: RESTSession, { newName }: { newName: string }) {
return {
request: {
...curr.request,
name: newName,
},
}
},
setEndpoint(curr: RESTSession, { newEndpoint }: { newEndpoint: string }) {
return {
request: {
...curr.request,
endpoint: newEndpoint,
},
}
},
setParams(curr: RESTSession, { entries }: { entries: HoppRESTParam[] }) {
return {
request: {
...curr.request,
params: entries,
},
}
},
addParam(curr: RESTSession, { newParam }: { newParam: HoppRESTParam }) {
return {
request: {
...curr.request,
params: [...curr.request.params, newParam],
},
}
},
updateParam(
curr: RESTSession,
{ index, updatedParam }: { index: number; updatedParam: HoppRESTParam }
) {
const newParams = curr.request.params.map((param, i) => {
if (i === index) return updatedParam
else return param
})
return {
request: {
...curr.request,
params: newParams,
},
}
},
deleteParam(curr: RESTSession, { index }: { index: number }) {
const newParams = curr.request.params.filter((_x, i) => i !== index)
return {
request: {
...curr.request,
params: newParams,
},
}
},
deleteAllParams(curr: RESTSession) {
return {
request: {
...curr.request,
params: [],
},
}
},
updateMethod(curr: RESTSession, { newMethod }: { newMethod: string }) {
return {
request: {
...curr.request,
method: newMethod,
},
}
},
setHeaders(curr: RESTSession, { entries }: { entries: HoppRESTHeader[] }) {
return {
request: {
...curr.request,
headers: entries,
},
}
},
addHeader(curr: RESTSession, { entry }: { entry: HoppRESTHeader }) {
return {
request: {
...curr.request,
headers: [...curr.request.headers, entry],
},
}
},
updateHeader(
curr: RESTSession,
{ index, updatedEntry }: { index: number; updatedEntry: HoppRESTHeader }
) {
return {
request: {
...curr.request,
headers: curr.request.headers.map((header, i) => {
if (i === index) return updatedEntry
else return header
}),
},
}
},
deleteHeader(curr: RESTSession, { index }: { index: number }) {
return {
request: {
...curr.request,
headers: curr.request.headers.filter((_, i) => i !== index),
},
}
},
deleteAllHeaders(curr: RESTSession) {
return {
request: {
...curr.request,
headers: [],
},
}
},
setAuth(curr: RESTSession, { newAuth }: { newAuth: HoppRESTAuth }) {
return {
request: {
...curr.request,
auth: newAuth,
},
}
},
setPreRequestScript(curr: RESTSession, { newScript }: { newScript: string }) {
return {
request: {
...curr.request,
preRequestScript: newScript,
},
}
},
setTestScript(curr: RESTSession, { newScript }: { newScript: string }) {
return {
request: {
...curr.request,
testScript: newScript,
},
}
},
setContentType(
curr: RESTSession,
{ newContentType }: { newContentType: ValidContentTypes | null }
) {
// TODO: persist body evenafter switching content typees
return {
request: {
...curr.request,
body: applyBodyTransition(curr.request.body, newContentType),
},
}
},
addFormDataEntry(curr: RESTSession, { entry }: { entry: FormDataKeyValue }) {
// Only perform update if the current content-type is formdata
if (curr.request.body.contentType !== "multipart/form-data") return {}
return {
request: {
...curr.request,
body: <HoppRESTReqBody>{
contentType: "multipart/form-data",
body: [...curr.request.body.body, entry],
},
},
}
},
deleteFormDataEntry(curr: RESTSession, { index }: { index: number }) {
// Only perform update if the current content-type is formdata
if (curr.request.body.contentType !== "multipart/form-data") return {}
return {
request: {
...curr.request,
body: <HoppRESTReqBody>{
contentType: "multipart/form-data",
body: curr.request.body.body.filter((_, i) => i !== index),
},
},
}
},
updateFormDataEntry(
curr: RESTSession,
{ index, entry }: { index: number; entry: FormDataKeyValue }
) {
// Only perform update if the current content-type is formdata
if (curr.request.body.contentType !== "multipart/form-data") return {}
return {
request: {
...curr.request,
body: <HoppRESTReqBody>{
contentType: "multipart/form-data",
body: curr.request.body.body.map((x, i) => (i !== index ? x : entry)),
},
},
}
},
deleteAllFormDataEntries(curr: RESTSession) {
// Only perform update if the current content-type is formdata
if (curr.request.body.contentType !== "multipart/form-data") return {}
return {
request: {
...curr.request,
body: <HoppRESTReqBody>{
contentType: "multipart/form-data",
body: [],
},
},
}
},
setRequestBody(curr: RESTSession, { newBody }: { newBody: HoppRESTReqBody }) {
return {
request: {
...curr.request,
body: newBody,
},
}
},
updateResponse(
_curr: RESTSession,
{ updatedRes }: { updatedRes: HoppRESTResponse | null }
) {
return {
response: updatedRes,
}
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
clearResponse(_curr: RESTSession) {
return {
response: null,
}
},
setTestResults(
_curr: RESTSession,
{ newResults }: { newResults: HoppTestResult | null }
) {
return {
testResults: newResults,
}
},
setSaveContext(
_,
{ newContext }: { newContext: HoppRequestSaveContext | null }
) {
return {
saveContext: newContext,
}
},
})
const restSessionStore = new DispatchingStore(defaultRESTSession, dispatchers)
export function getRESTRequest() {
return restSessionStore.subject$.value.request
}
export function setRESTRequest(
req: HoppRESTRequest,
saveContext?: HoppRequestSaveContext | null
) {
restSessionStore.dispatch({
dispatcher: "setRequest",
payload: {
req,
},
})
if (saveContext) setRESTSaveContext(saveContext)
}
export function setRESTSaveContext(saveContext: HoppRequestSaveContext | null) {
restSessionStore.dispatch({
dispatcher: "setSaveContext",
payload: {
newContext: saveContext,
},
})
}
export function getRESTSaveContext() {
return restSessionStore.value.saveContext
}
export function resetRESTRequest() {
setRESTRequest(getDefaultRESTRequest())
}
export function setRESTEndpoint(newEndpoint: string) {
restSessionStore.dispatch({
dispatcher: "setEndpoint",
payload: {
newEndpoint,
},
})
}
export function setRESTRequestName(newName: string) {
restSessionStore.dispatch({
dispatcher: "setRequestName",
payload: {
newName,
},
})
}
export function setRESTParams(entries: HoppRESTParam[]) {
restSessionStore.dispatch({
dispatcher: "setParams",
payload: {
entries,
},
})
}
export function addRESTParam(newParam: HoppRESTParam) {
restSessionStore.dispatch({
dispatcher: "addParam",
payload: {
newParam,
},
})
}
export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
restSessionStore.dispatch({
dispatcher: "updateParam",
payload: {
updatedParam,
index,
},
})
}
export function deleteRESTParam(index: number) {
restSessionStore.dispatch({
dispatcher: "deleteParam",
payload: {
index,
},
})
}
export function deleteAllRESTParams() {
restSessionStore.dispatch({
dispatcher: "deleteAllParams",
payload: {},
})
}
export function updateRESTMethod(newMethod: string) {
restSessionStore.dispatch({
dispatcher: "updateMethod",
payload: {
newMethod,
},
})
}
export function setRESTHeaders(entries: HoppRESTHeader[]) {
restSessionStore.dispatch({
dispatcher: "setHeaders",
payload: {
entries,
},
})
}
export function addRESTHeader(entry: HoppRESTHeader) {
restSessionStore.dispatch({
dispatcher: "addHeader",
payload: {
entry,
},
})
}
export function updateRESTHeader(index: number, updatedEntry: HoppRESTHeader) {
restSessionStore.dispatch({
dispatcher: "updateHeader",
payload: {
index,
updatedEntry,
},
})
}
export function deleteRESTHeader(index: number) {
restSessionStore.dispatch({
dispatcher: "deleteHeader",
payload: {
index,
},
})
}
export function deleteAllRESTHeaders() {
restSessionStore.dispatch({
dispatcher: "deleteAllHeaders",
payload: {},
})
}
export function setRESTAuth(newAuth: HoppRESTAuth) {
restSessionStore.dispatch({
dispatcher: "setAuth",
payload: {
newAuth,
},
})
}
export function setRESTPreRequestScript(newScript: string) {
restSessionStore.dispatch({
dispatcher: "setPreRequestScript",
payload: {
newScript,
},
})
}
export function setRESTTestScript(newScript: string) {
restSessionStore.dispatch({
dispatcher: "setTestScript",
payload: {
newScript,
},
})
}
export function setRESTReqBody(newBody: HoppRESTReqBody | null) {
restSessionStore.dispatch({
dispatcher: "setRequestBody",
payload: {
newBody,
},
})
}
export function updateRESTResponse(updatedRes: HoppRESTResponse | null) {
restSessionStore.dispatch({
dispatcher: "updateResponse",
payload: {
updatedRes,
},
})
}
export function clearRESTResponse() {
restSessionStore.dispatch({
dispatcher: "clearResponse",
payload: {},
})
}
export function setRESTTestResults(newResults: HoppTestResult | null) {
restSessionStore.dispatch({
dispatcher: "setTestResults",
payload: {
newResults,
},
})
}
export function addFormDataEntry(entry: FormDataKeyValue) {
restSessionStore.dispatch({
dispatcher: "addFormDataEntry",
payload: {
entry,
},
})
}
export function deleteFormDataEntry(index: number) {
restSessionStore.dispatch({
dispatcher: "deleteFormDataEntry",
payload: {
index,
},
})
}
export function updateFormDataEntry(index: number, entry: FormDataKeyValue) {
restSessionStore.dispatch({
dispatcher: "updateFormDataEntry",
payload: {
index,
entry,
},
})
}
export function setRESTContentType(newContentType: ValidContentTypes | null) {
restSessionStore.dispatch({
dispatcher: "setContentType",
payload: {
newContentType,
},
})
}
export function deleteAllFormDataEntries() {
restSessionStore.dispatch({
dispatcher: "deleteAllFormDataEntries",
payload: {},
})
}
export const restSaveContext$ = restSessionStore.subject$.pipe(
pluck("saveContext"),
distinctUntilChanged()
)
export const restRequest$ = restSessionStore.subject$.pipe(
pluck("request"),
distinctUntilChanged()
)
export const restRequestName$ = restRequest$.pipe(
pluck("name"),
distinctUntilChanged()
)
export const restEndpoint$ = restSessionStore.subject$.pipe(
pluck("request", "endpoint"),
distinctUntilChanged()
)
export const restParams$ = restSessionStore.subject$.pipe(
pluck("request", "params"),
distinctUntilChanged()
)
export const restActiveParamsCount$ = restParams$.pipe(
map(
(params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
)
)
export const restMethod$ = restSessionStore.subject$.pipe(
pluck("request", "method"),
distinctUntilChanged()
)
export const restHeaders$ = restSessionStore.subject$.pipe(
pluck("request", "headers"),
distinctUntilChanged()
)
export const restActiveHeadersCount$ = restHeaders$.pipe(
map(
(params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
)
)
export const restAuth$ = restRequest$.pipe(pluck("auth"))
export const restPreRequestScript$ = restSessionStore.subject$.pipe(
pluck("request", "preRequestScript"),
distinctUntilChanged()
)
export const restContentType$ = restRequest$.pipe(
pluck("body", "contentType"),
distinctUntilChanged()
)
export const restTestScript$ = restSessionStore.subject$.pipe(
pluck("request", "testScript"),
distinctUntilChanged()
)
export const restReqBody$ = restSessionStore.subject$.pipe(
pluck("request", "body"),
distinctUntilChanged()
)
export const restResponse$ = restSessionStore.subject$.pipe(
pluck("response"),
distinctUntilChanged()
)
export const completedRESTResponse$ = restResponse$.pipe(
filter(
(res) =>
res !== null &&
res.type !== "loading" &&
res.type !== "network_fail" &&
res.type !== "script_fail"
)
)
export const restTestResults$ = restSessionStore.subject$.pipe(
pluck("testResults"),
distinctUntilChanged()
)
/**
* A Vue 3 composable function that gives access to a ref
* which is updated to the preRequestScript value in the store.
* The ref value is kept in sync with the store and all writes
* to the ref are dispatched to the store as `setPreRequestScript`
* dispatches.
*/
export function usePreRequestScript(): Ref<string> {
return useStream(
restPreRequestScript$,
restSessionStore.value.request.preRequestScript,
(value) => {
setRESTPreRequestScript(value)
}
)
}
/**
* A Vue 3 composable function that gives access to a ref
* which is updated to the testScript value in the store.
* The ref value is kept in sync with the store and all writes
* to the ref are dispatched to the store as `setTestScript`
* dispatches.
*/
export function useTestScript(): Ref<string> {
return useStream(
restTestScript$,
restSessionStore.value.request.testScript,
(value) => {
setRESTTestScript(value)
}
)
}
export function useRESTRequestBody(): Ref<HoppRESTReqBody> {
return useStream(
restReqBody$,
restSessionStore.value.request.body,
setRESTReqBody
)
}
export function useRESTRequestName(): Ref<string> {
return useStream(
restRequestName$,
restSessionStore.value.request.name,
setRESTRequestName
)
}

View File

@@ -1,70 +0,0 @@
<template>
<div class="flex flex-col items-center justify-center min-h-screen">
<div v-if="signingInWithEmail">
<SmartSpinner />
<div class="mt-2 text-sm text-secondaryLight">
{{ t("state.loading") }}
</div>
</div>
<div v-else>
<AppLogo class="w-16 h-16 rounded" />
</div>
<pre v-if="error" class="mt-4 text-secondaryLight">{{ error }}</pre>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import { useI18n } from "@composables/i18n"
import { initializeFirebase } from "~/helpers/fb"
import { isSignInWithEmailLink, signInWithEmailLink } from "~/helpers/fb/auth"
import { getLocalConfig, removeLocalConfig } from "~/newstore/localpersistence"
export default defineComponent({
setup() {
return {
t: useI18n(),
}
},
data() {
return {
signingInWithEmail: false,
error: null,
}
},
beforeMount() {
initializeFirebase()
},
async mounted() {
if (isSignInWithEmailLink(window.location.href)) {
this.signingInWithEmail = true
let email = getLocalConfig("emailForSignIn")
if (!email) {
email = window.prompt(
"Please provide your email for confirmation"
) as string
}
await signInWithEmailLink(email, window.location.href)
.then(() => {
removeLocalConfig("emailForSignIn")
this.$router.push({ path: "/" })
})
.catch((e) => {
this.signingInWithEmail = false
this.error = e.message
})
.finally(() => {
this.signingInWithEmail = false
})
}
},
})
</script>
<route lang="yaml">
meta:
layout: empty
</route>

View File

@@ -1,173 +0,0 @@
<template>
<AppPaneLayout layout-id="http">
<template #primary>
<HttpRequest />
<HttpRequestOptions />
</template>
<template #secondary>
<HttpResponse />
</template>
<template #sidebar>
<HttpSidebar />
</template>
</AppPaneLayout>
</template>
<script lang="ts">
import {
defineComponent,
onBeforeMount,
onBeforeUnmount,
onMounted,
Ref,
ref,
watch,
} from "vue"
import type { Subscription } from "rxjs"
import {
HoppRESTRequest,
HoppRESTAuthOAuth2,
safelyExtractRESTRequest,
isEqualHoppRESTRequest,
} from "@hoppscotch/data"
import {
getRESTRequest,
setRESTRequest,
setRESTAuth,
restAuth$,
getDefaultRESTRequest,
} from "~/newstore/RESTSession"
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useStream } from "@composables/stream"
import { useToast } from "@composables/toast"
import { onLoggedIn } from "@composables/auth"
import { loadRequestFromSync, startRequestSync } from "~/helpers/fb/request"
import { oauthRedirect } from "~/helpers/oauth"
import { useRoute } from "vue-router"
function bindRequestToURLParams() {
const route = useRoute()
// Get URL parameters and set that as the request
onMounted(() => {
const query = route.query
// If query params are empty, or contains code or error param (these are from Oauth Redirect)
// We skip URL params parsing
if (Object.keys(query).length === 0 || query.code || query.error) return
setRESTRequest(
safelyExtractRESTRequest(
translateExtURLParams(query),
getDefaultRESTRequest()
)
)
})
}
function oAuthURL() {
const auth = useStream(
restAuth$,
{ authType: "none", authActive: true },
setRESTAuth
)
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
onBeforeMount(async () => {
try {
const tokenInfo = await oauthRedirect()
if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) {
if (typeof tokenInfo === "object") {
oauth2Token.value = tokenInfo.access_token
}
}
// eslint-disable-next-line no-empty
} catch (_) {}
})
}
function setupRequestSync(
confirmSync: Ref<boolean>,
requestForSync: Ref<HoppRESTRequest | null>
) {
const route = useRoute()
// Subscription to request sync
let sub: Subscription | null = null
// Load request on login resolve and start sync
onLoggedIn(async () => {
if (
Object.keys(route.query).length === 0 &&
!(route.query.code || route.query.error)
) {
const request = await loadRequestFromSync()
if (request) {
if (!isEqualHoppRESTRequest(request, getRESTRequest())) {
requestForSync.value = request
confirmSync.value = true
}
}
}
sub = startRequestSync()
})
// Stop subscription to stop syncing
onBeforeUnmount(() => {
sub?.unsubscribe()
})
}
export default defineComponent({
setup() {
const requestForSync = ref<HoppRESTRequest | null>(null)
const confirmSync = ref(false)
const toast = useToast()
const t = useI18n()
watch(confirmSync, (newValue) => {
if (newValue) {
toast.show(`${t("confirm.sync")}`, {
duration: 0,
action: [
{
text: `${t("action.yes")}`,
onClick: (_, toastObject) => {
syncRequest()
toastObject.goAway(0)
},
},
{
text: `${t("action.no")}`,
onClick: (_, toastObject) => {
toastObject.goAway(0)
},
},
],
})
}
})
const syncRequest = () => {
setRESTRequest(
safelyExtractRESTRequest(requestForSync.value!, getDefaultRESTRequest())
)
}
setupRequestSync(confirmSync, requestForSync)
bindRequestToURLParams()
oAuthURL()
return {
confirmSync,
syncRequest,
oAuthURL,
requestForSync,
}
},
})
</script>

View File

@@ -1,343 +0,0 @@
<template>
<AppPaneLayout layout-id="mqtt">
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary"
>
<div class="inline-flex flex-1 space-x-2">
<input
id="mqtt-url"
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
:placeholder="t('mqtt.url')"
:disabled="
connectionState === 'CONNECTED' ||
connectionState === 'CONNECTING'
"
@keyup.enter="isUrlValid ? toggleConnection() : null"
/>
<ButtonPrimary
id="connect"
:disabled="!isUrlValid"
class="w-32"
:label="
connectionState === 'DISCONNECTED'
? t('action.connect')
: t('action.disconnect')
"
:loading="connectionState === 'CONNECTING'"
@click="toggleConnection"
/>
</div>
<div class="flex space-x-2">
<input
id="mqtt-username"
v-model="username"
type="text"
spellcheck="false"
class="input"
:placeholder="t('authorization.username')"
/>
<input
id="mqtt-password"
v-model="password"
type="password"
spellcheck="false"
class="input"
:placeholder="t('authorization.password')"
/>
</div>
</div>
</template>
<template #secondary>
<RealtimeLog
:title="t('mqtt.log')"
:log="log"
@delete="clearLogEntries()"
/>
</template>
<template #sidebar>
<div class="flex items-center justify-between p-4">
<label for="pubTopic" class="font-semibold text-secondaryLight">
{{ t("mqtt.topic") }}
</label>
</div>
<div class="flex px-4">
<input
id="pubTopic"
v-model="pubTopic"
class="input"
:placeholder="t('mqtt.topic_name')"
type="text"
autocomplete="off"
spellcheck="false"
/>
</div>
<div class="flex items-center justify-between p-4">
<label for="mqtt-message" class="font-semibold text-secondaryLight">
{{ t("mqtt.communication") }}
</label>
</div>
<div class="flex px-4 space-x-2">
<input
id="mqtt-message"
v-model="message"
class="input"
type="text"
autocomplete="off"
:placeholder="t('mqtt.message')"
spellcheck="false"
/>
<ButtonPrimary
id="publish"
name="get"
:disabled="!canPublish"
:label="t('mqtt.publish')"
@click="publish"
/>
</div>
<div
class="flex items-center justify-between p-4 mt-4 border-t border-dividerLight"
>
<label for="subTopic" class="font-semibold text-secondaryLight">
{{ t("mqtt.topic") }}
</label>
</div>
<div class="flex px-4 space-x-2">
<input
id="subTopic"
v-model="subTopic"
type="text"
autocomplete="off"
:placeholder="t('mqtt.topic_name')"
spellcheck="false"
class="input"
/>
<ButtonPrimary
id="subscribe"
name="get"
:disabled="!canSubscribe"
:label="
subscriptionState ? t('mqtt.unsubscribe') : t('mqtt.subscribe')
"
reverse
@click="toggleSubscription"
/>
</div>
</template>
</AppPaneLayout>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from "vue"
import { debounce } from "lodash-es"
import { MQTTConnection, MQTTError } from "~/helpers/realtime/MQTTConnection"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import {
useReadonlyStream,
useStream,
useStreamSubscriber,
} from "@composables/stream"
import {
addMQTTLogLine,
MQTTConn$,
MQTTEndpoint$,
MQTTLog$,
setMQTTConn,
setMQTTEndpoint,
setMQTTLog,
} from "~/newstore/MQTTSession"
import RegexWorker from "@workers/regex?worker"
const t = useI18n()
const toast = useToast()
const { subscribeToStream } = useStreamSubscriber()
const url = useStream(MQTTEndpoint$, "", setMQTTEndpoint)
const log = useStream(MQTTLog$, [], setMQTTLog)
const socket = useStream(MQTTConn$, new MQTTConnection(), setMQTTConn)
const connectionState = useReadonlyStream(
socket.value.connectionState$,
"DISCONNECTED"
)
const subscriptionState = useReadonlyStream(
socket.value.subscriptionState$,
false
)
const isUrlValid = ref(true)
const pubTopic = ref("")
const subTopic = ref("")
const message = ref("")
const username = ref("")
const password = ref("")
let worker: Worker
const canPublish = computed(
() =>
pubTopic.value !== "" &&
message.value !== "" &&
connectionState.value === "CONNECTED"
)
const canSubscribe = computed(
() => subTopic.value !== "" && connectionState.value === "CONNECTED"
)
const workerResponseHandler = ({
data,
}: {
data: { url: string; result: boolean }
}) => {
if (data.url === url.value) isUrlValid.value = data.result
}
onMounted(() => {
worker = new RegexWorker()
worker.addEventListener("message", workerResponseHandler)
subscribeToStream(socket.value.event$, (event) => {
switch (event?.type) {
case "CONNECTING":
log.value = [
{
payload: `${t("state.connecting_to", { name: url.value })}`,
source: "info",
color: "var(--accent-color)",
ts: undefined,
},
]
break
case "CONNECTED":
log.value = [
{
payload: `${t("state.connected_to", { name: url.value })}`,
source: "info",
color: "var(--accent-color)",
ts: Date.now(),
},
]
toast.success(`${t("state.connected")}`)
break
case "MESSAGE_SENT":
addMQTTLogLine({
prefix: `${event.message.topic}`,
payload: event.message.message,
source: "client",
ts: Date.now(),
})
break
case "MESSAGE_RECEIVED":
addMQTTLogLine({
prefix: `${event.message.topic}`,
payload: event.message.message,
source: "server",
ts: event.time,
})
break
case "SUBSCRIBED":
addMQTTLogLine({
payload: subscriptionState.value
? `${t("state.subscribed_success", { topic: subTopic.value })}`
: `${t("state.unsubscribed_success", { topic: subTopic.value })}`,
source: "server",
ts: event.time,
})
break
case "SUBSCRIPTION_FAILED":
addMQTTLogLine({
payload: subscriptionState.value
? `${t("state.subscribed_failed", { topic: subTopic.value })}`
: `${t("state.unsubscribed_failed", { topic: subTopic.value })}`,
source: "server",
ts: event.time,
})
break
case "ERROR":
addMQTTLogLine({
payload: getI18nError(event.error),
source: "info",
color: "#ff5555",
ts: event.time,
})
break
case "DISCONNECTED":
addMQTTLogLine({
payload: t("state.disconnected_from", { name: url.value }).toString(),
source: "info",
color: "#ff5555",
ts: event.time,
})
toast.error(`${t("state.disconnected")}`)
break
}
})
})
const debouncer = debounce(function () {
worker.postMessage({ type: "ws", url: url.value })
}, 1000)
watch(url, (newUrl) => {
if (newUrl) debouncer()
})
onUnmounted(() => {
worker.terminate()
})
// METHODS
const toggleConnection = () => {
// If it is connecting:
if (connectionState.value === "DISCONNECTED") {
return socket.value.connect(url.value, username.value, password.value)
}
// Otherwise, it's disconnecting.
socket.value.disconnect()
}
const publish = () => {
socket.value?.publish(pubTopic.value, message.value)
}
const toggleSubscription = () => {
if (subscriptionState.value) {
socket.value.unsubscribe(subTopic.value)
} else {
socket.value.subscribe(subTopic.value)
}
}
const getI18nError = (error: MQTTError): string => {
if (typeof error === "string") return error
switch (error.type) {
case "CONNECTION_NOT_ESTABLISHED":
return t("state.connection_lost").toString()
case "SUBSCRIPTION_FAILED":
return t("state.mqtt_subscription_failed", {
topic: error.topic,
}).toString()
case "PUBLISH_ERROR":
return t("state.publish_error", { topic: error.topic }).toString()
case "CONNECTION_LOST":
return t("state.connection_lost").toString()
case "CONNECTION_FAILED":
return t("state.connection_failed").toString()
default:
return t("state.disconnected_from", { name: url.value }).toString()
}
}
const clearLogEntries = () => {
log.value = []
}
</script>

View File

@@ -1,4 +1,17 @@
# Hoppscotch CLI <sup>ALPHA</sup>
<div align="center">
<a href="https://hoppscotch.io">
<img
src="https://avatars.githubusercontent.com/u/56705483"
alt="Hoppscotch Logo"
height="64"
/>
</a>
</div>
<div align="center">
# Hoppscotch CLI <font size=2><sup>ALPHA</sup></font>
</div>
A CLI to run Hoppscotch test scripts in CI environments.
@@ -33,7 +46,7 @@ hopp [options or commands] arguments
#### Options:
##### `-e <file_path>` / `--env <file_path>`
- Accepts path to env.json with contents in below format:
- Accepts path to env.json with contents in below format:
```json
{
"ENV1":"value1",
@@ -41,7 +54,7 @@ hopp [options or commands] arguments
}
```
- You can now access those variables using `pw.env.get('<var_name>')`
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
## Install

View File

@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/cli",
"version": "0.3.0",
"version": "0.3.1",
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io",
"main": "dist/index.js",
@@ -36,8 +36,8 @@
"license": "MIT",
"private": false,
"devDependencies": {
"@hoppscotch/data": "workspace:^0.4.3",
"@hoppscotch/js-sandbox": "workspace:^2.0.0",
"@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^",
"@relmify/jest-fp-ts": "^2.0.2",
"@swc/core": "^1.2.181",
"@types/axios": "^0.14.0",
@@ -54,7 +54,7 @@
"io-ts": "^2.2.16",
"jest": "^27.5.1",
"lodash": "^4.17.21",
"prettier": "^2.6.2",
"prettier": "^2.8.4",
"qs": "^6.10.3",
"ts-jest": "^27.1.4",
"tsup": "^5.12.7",

View File

@@ -5,42 +5,52 @@ import { execAsync, getErrorCode, getTestJsonFilePath } from "../utils";
describe("Test 'hopp test <file>' command:", () => {
test("No collection file path provided.", async () => {
const cmd = `node ./bin/hopp test`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Collection file not found.", async () => {
const cmd = `node ./bin/hopp test notfound.json`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("Malformed collection file.", async () => {
test("Collection file is invalid JSON.", async () => {
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
"malformed-collection.json"
)}`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
test("Malformed collection file.", async () => {
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
"malformed-collection2.json"
)}`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
});
test("Invalid arguement.", async () => {
const cmd = `node ./bin/hopp invalid-arg`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Collection file not JSON type.", async () => {
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("notjson.txt")}`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
@@ -70,24 +80,24 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
test("No env file path provided.", async () => {
const cmd = `${VALID_TEST_CMD} --env`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("ENV file not JSON type.", async () => {
const cmd = `${VALID_TEST_CMD} --env ${getTestJsonFilePath("notjson.txt")}`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("ENV file not found.", async () => {
const cmd = `${VALID_TEST_CMD} --env notfound.json`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
@@ -109,17 +119,17 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
test("No value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Invalid value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
console.log("invalid value thing", out)
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});

View File

@@ -1,28 +0,0 @@
import { HoppCLIError } from "../../../types/errors";
import { checkFile } from "../../../utils/checks";
import "@relmify/jest-fp-ts";
describe("checkFile", () => {
test("File doesn't exists.", () => {
return expect(
checkFile("./src/samples/this-file-not-exists.json")()
).resolves.toSubsetEqualLeft(<HoppCLIError>{
code: "FILE_NOT_FOUND",
});
});
test("File not of JSON type.", () => {
return expect(
checkFile("./src/__tests__/samples/notjson.txt")()
).resolves.toSubsetEqualLeft(<HoppCLIError>{
code: "INVALID_FILE_TYPE",
});
});
test("Existing JSON file.", () => {
return expect(
checkFile("./src/__tests__/samples/passes.json")()
).resolves.toBeRight();
});
});

View File

@@ -50,7 +50,7 @@ describe("collectionsRunner", () => {
test("Empty HoppCollection.", () => {
return expect(
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })()
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })
).resolves.toStrictEqual([]);
});
@@ -66,7 +66,7 @@ describe("collectionsRunner", () => {
},
],
envs: SAMPLE_ENVS,
})()
})
).resolves.toMatchObject([]);
});
@@ -84,7 +84,7 @@ describe("collectionsRunner", () => {
},
],
envs: SAMPLE_ENVS,
})()
})
).resolves.toMatchObject([
{
path: "collection/request",
@@ -116,7 +116,7 @@ describe("collectionsRunner", () => {
},
],
envs: SAMPLE_ENVS,
})()
})
).resolves.toMatchObject([
{
path: "collection/folder/request",

View File

@@ -1,22 +1,20 @@
import { HoppCLIError } from "../../../types/errors";
import { parseCollectionData } from "../../../utils/mutators";
import "@relmify/jest-fp-ts";
describe("parseCollectionData", () => {
test("Reading non-existing file.", () => {
return expect(
parseCollectionData("./src/__tests__/samples/notexist.json")()
).resolves.toSubsetEqualLeft(<HoppCLIError>{
parseCollectionData("./src/__tests__/samples/notexist.json")
).rejects.toMatchObject(<HoppCLIError>{
code: "FILE_NOT_FOUND",
});
});
test("Unparseable JSON contents.", () => {
return expect(
parseCollectionData("./src/__tests__/samples/malformed-collection.json")()
).resolves.toSubsetEqualLeft(<HoppCLIError>{
code: "MALFORMED_COLLECTION",
parseCollectionData("./src/__tests__/samples/malformed-collection.json")
).rejects.toMatchObject(<HoppCLIError>{
code: "UNKNOWN_ERROR",
});
});
@@ -24,15 +22,15 @@ describe("parseCollectionData", () => {
return expect(
parseCollectionData(
"./src/__tests__/samples/malformed-collection2.json"
)()
).resolves.toSubsetEqualLeft(<HoppCLIError>{
)
).rejects.toMatchObject(<HoppCLIError>{
code: "MALFORMED_COLLECTION",
});
});
test("Valid HoppCollection.", () => {
return expect(
parseCollectionData("./src/__tests__/samples/passes.json")()
).resolves.toBeRight();
parseCollectionData("./src/__tests__/samples/passes.json")
).resolves.toBeTruthy();
});
});

View File

@@ -1,5 +1,3 @@
import * as TE from "fp-ts/TaskEither";
import { pipe, flow } from "fp-ts/function";
import {
collectionsRunner,
collectionsRunnerExit,
@@ -10,18 +8,23 @@ import { parseCollectionData } from "../utils/mutators";
import { parseEnvsData } from "../options/test/env";
import { TestCmdOptions } from "../types/commands";
import { parseDelayOption } from "../options/test/delay";
import { HoppEnvs } from "../types/request";
import { isHoppCLIError } from "../utils/checks";
export const test = (path: string, options: TestCmdOptions) => async () => {
await pipe(
TE.Do,
TE.bind("envs", () => parseEnvsData(options.env)),
TE.bind("collections", () => parseCollectionData(path)),
TE.bind("delay", () => parseDelayOption(options.delay)),
TE.chainTaskK(collectionsRunner),
TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)),
TE.mapLeft((e) => {
handleError(e);
try {
const delay = options.delay ? parseDelayOption(options.delay) : 0
const envs = options.env ? await parseEnvsData(options.env) : <HoppEnvs>{ global: [], selected: [] }
const collections = await parseCollectionData(path)
const report = await collectionsRunner({collections, envs, delay})
const hasSucceeded = collectionsRunnerResult(report)
collectionsRunnerExit(hasSucceeded)
} catch(e) {
if(isHoppCLIError(e)) {
handleError(e)
process.exit(1);
})
)();
}
else throw e
}
};

View File

@@ -1,4 +1,3 @@
import { log } from "console";
import * as S from "fp-ts/string";
import { HoppError, HoppErrorCode } from "../types/errors";
import { hasProperty, isSafeCommanderError } from "../utils/checks";
@@ -7,7 +6,7 @@ import { exceptionColors } from "../utils/getters";
const { BG_FAIL } = exceptionColors;
/**
* Parses unknown error data and narrows it to get information realted to
* Parses unknown error data and narrows it to get information related to
* error in string format.
* @param e Error data to parse.
* @returns Information in string format appropriately parsed, based on error type.
@@ -81,6 +80,6 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
}
if (!S.isEmpty(ERROR_MSG)) {
log(ERROR_CODE, ERROR_MSG);
console.error(ERROR_CODE, ERROR_MSG);
}
};
};

View File

@@ -1,20 +1,14 @@
import * as TE from "fp-ts/TaskEither";
import * as S from "fp-ts/string";
import { pipe } from "fp-ts/function";
import { error, HoppCLIError } from "../../types/errors";
import { error } from "../../types/errors";
export const parseDelayOption = (
delay: unknown
): TE.TaskEither<HoppCLIError, number> =>
!S.isString(delay)
? TE.right(0)
: pipe(
delay,
Number,
TE.fromPredicate(Number.isSafeInteger, () =>
error({
code: "INVALID_ARGUMENT",
data: "Expected '-d, --delay' value to be number",
})
)
);
export function parseDelayOption(delay: string): number {
const maybeInt = Number.parseInt(delay)
if(!Number.isNaN(maybeInt)) {
return maybeInt
} else {
throw error({
code: "INVALID_ARGUMENT",
data: "Expected '-d, --delay' value to be number",
})
}
}

View File

@@ -1,64 +1,27 @@
import fs from "fs/promises";
import { pipe } from "fp-ts/function";
import * as TE from "fp-ts/TaskEither";
import * as E from "fp-ts/Either";
import * as J from "fp-ts/Json";
import * as A from "fp-ts/Array";
import * as S from "fp-ts/string";
import isArray from "lodash/isArray";
import { HoppCLIError, error } from "../../types/errors";
import { error } from "../../types/errors";
import { HoppEnvs, HoppEnvPair } from "../../types/request";
import { checkFile } from "../../utils/checks";
import { readJsonFile } from "../../utils/mutators";
/**
* Parses env json file for given path and validates the parsed env json object.
* @param path Path of env.json file to be parsed.
* @returns For successful parsing we get HoppEnvs object.
*/
export const parseEnvsData = (
path: unknown
): TE.TaskEither<HoppCLIError, HoppEnvs> =>
!S.isString(path)
? TE.right({ global: [], selected: [] })
: pipe(
// Checking if the env.json file exists or not.
checkFile(path),
export async function parseEnvsData(path: string) {
const contents = await readJsonFile(path)
// Trying to read given env json file path.
TE.chainW((checkedPath) =>
TE.tryCatch(
() => fs.readFile(checkedPath),
(reason) =>
error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
)
),
if(!(contents && typeof contents === "object" && !Array.isArray(contents))) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
}
// Trying to JSON parse the read file data and mapping the entries to HoppEnvPairs.
TE.chainEitherKW((data) =>
pipe(
data.toString(),
J.parse,
E.map((jsonData) =>
jsonData && typeof jsonData === "object" && !isArray(jsonData)
? pipe(
jsonData,
Object.entries,
A.map(
([key, value]) =>
<HoppEnvPair>{
key,
value: S.isString(value)
? value
: JSON.stringify(value),
}
)
)
: []
),
E.map((envPairs) => <HoppEnvs>{ global: [], selected: envPairs }),
E.mapLeft((e) =>
error({ code: "MALFORMED_ENV_FILE", path, data: E.toError(e) })
)
)
)
);
const envPairs: Array<HoppEnvPair> = []
for( const [key,value] of Object.entries(contents)) {
if(typeof value !== "string") {
throw error({ code: "MALFORMED_ENV_FILE", path, data: {value: value} })
}
envPairs.push({key, value})
}
return <HoppEnvs>{ global: [], selected: envPairs }
}

View File

@@ -1,6 +1,6 @@
export type TestCmdOptions = {
env: string;
delay: number;
env: string | undefined;
delay: string | undefined;
};
export type HoppEnvFileExt = "json";
export type HOPP_ENV_FILE_EXT = "json";

View File

@@ -1,20 +1,11 @@
import fs from "fs/promises";
import { join } from "path";
import { pipe } from "fp-ts/function";
import {
HoppCollection,
HoppRESTRequest,
isHoppRESTRequest,
} from "@hoppscotch/data";
import * as A from "fp-ts/Array";
import * as S from "fp-ts/string";
import * as TE from "fp-ts/TaskEither";
import * as E from "fp-ts/Either";
import curryRight from "lodash/curryRight";
import { CommanderError } from "commander";
import { error, HoppCLIError, HoppErrnoException } from "../types/errors";
import { HoppCollectionFileExt } from "../types/collections";
import { HoppEnvFileExt } from "../types/commands";
import { HoppCLIError, HoppErrnoException } from "../types/errors";
/**
* Determines whether an object has a property with given name.
@@ -71,59 +62,6 @@ export const isRESTCollection = (
return false;
};
/**
* Checks if the file path matches the requried file type with of required extension.
* @param path The input file path to check.
* @param extension The required extension for input file path.
* @returns Absolute path for valid file extension OR HoppCLIError in case of error.
*/
export const checkFileExt = curryRight(
(
path: unknown,
extension: HoppCollectionFileExt | HoppEnvFileExt
): E.Either<HoppCLIError, string> =>
pipe(
path,
E.fromPredicate(S.isString, (_) => error({ code: "NO_FILE_PATH" })),
E.chainW(
E.fromPredicate(S.endsWith(`.${extension}`), (_) =>
error({ code: "INVALID_FILE_TYPE", data: extension })
)
)
)
);
/**
* Checks if the given file path exists and is of given type.
* @param path The input file path to check.
* @returns Absolute path for valid file path OR HoppCLIError in case of error.
*/
export const checkFile = (path: unknown): TE.TaskEither<HoppCLIError, string> =>
pipe(
path,
// Checking if path is string.
TE.fromPredicate(S.isString, () => error({ code: "NO_FILE_PATH" })),
/**
* After checking file path, we map file path to absolute path and check
* if file is of given extension type.
*/
TE.map(join),
TE.chainEitherK(checkFileExt("json")),
/**
* Trying to access given file path.
* If successfully accessed, we return the path from predicate step.
* Else return HoppCLIError with code FILE_NOT_FOUND.
*/
TE.chainFirstW((checkedPath) =>
TE.tryCatchK(
() => fs.access(checkedPath),
() => error({ code: "FILE_NOT_FOUND", path: checkedPath })
)()
)
);
/**
* Checks if given error data is of type HoppCLIError, based on existence

View File

@@ -1,4 +1,3 @@
import * as T from "fp-ts/Task";
import * as A from "fp-ts/Array";
import { pipe } from "fp-ts/function";
import { bold } from "chalk";
@@ -43,8 +42,8 @@ const { WARN, FAIL } = exceptionColors;
* @returns List of report for each processed request.
*/
export const collectionsRunner =
(param: CollectionRunnerParam): T.Task<RequestReport[]> =>
async () => {
async (param: CollectionRunnerParam): Promise<RequestReport[]> =>
{
const envs: HoppEnvs = param.envs;
const delay = param.delay ?? 0;
const requestsReport: RequestReport[] = [];
@@ -213,10 +212,10 @@ export const collectionsRunnerResult = (
* Else, exit with code 1.
* @param result Boolean defining the collections-runner result.
*/
export const collectionsRunnerExit = (result: boolean) => {
export const collectionsRunnerExit = (result: boolean): never => {
if (!result) {
const EXIT_MSG = FAIL(`\nExited with code 1`);
process.stdout.write(EXIT_MSG);
process.stderr.write(EXIT_MSG);
process.exit(1);
}
process.exit(0);

View File

@@ -1,12 +1,7 @@
import fs from "fs/promises";
import * as E from "fp-ts/Either";
import * as TE from "fp-ts/TaskEither";
import * as A from "fp-ts/Array";
import * as J from "fp-ts/Json";
import { pipe } from "fp-ts/function";
import { FormDataEntry } from "../types/request";
import { error, HoppCLIError } from "../types/errors";
import { isRESTCollection, isHoppErrnoException, checkFile } from "./checks";
import { error } from "../types/errors";
import { isRESTCollection, isHoppErrnoException } from "./checks";
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
/**
@@ -39,49 +34,44 @@ export const parseErrorMessage = (e: unknown) => {
return msg.replace(/\n+$|\s{2,}/g, "").trim();
};
export async function readJsonFile(path: string): Promise<unknown> {
if(!path.endsWith('.json')) {
throw error({ code: "INVALID_FILE_TYPE", data: path })
}
try {
await fs.access(path)
} catch (e) {
throw error({ code: "FILE_NOT_FOUND", path: path })
}
try {
return JSON.parse((await fs.readFile(path)).toString())
} catch(e) {
throw error({ code: "UNKNOWN_ERROR", data: e })
}
}
/**
* Parses collection json file for given path:context.path, and validates
* the parsed collectiona array.
* @param path Collection json file path.
* @returns For successful parsing we get array of HoppCollection<HoppRESTRequest>,
*/
export const parseCollectionData = (
export async function parseCollectionData(
path: string
): TE.TaskEither<HoppCLIError, HoppCollection<HoppRESTRequest>[]> =>
pipe(
TE.of(path),
): Promise<HoppCollection<HoppRESTRequest>[]> {
let contents = await readJsonFile(path)
// Checking if given file path exists or not.
TE.chain(checkFile),
const maybeArrayOfCollections: unknown[] = Array.isArray(contents) ? contents : [contents]
// Trying to read give collection json path.
TE.chainW((checkedPath) =>
TE.tryCatch(
() => fs.readFile(checkedPath),
(reason) => error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
)
),
if(maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
throw error({
code: "MALFORMED_COLLECTION",
path,
data: "Please check the collection data.",
})
}
// Checking if parsed file data is array.
TE.chainEitherKW((data) =>
pipe(
data.toString(),
J.parse,
E.map((jsonData) => (Array.isArray(jsonData) ? jsonData : [jsonData])),
E.mapLeft((e) =>
error({ code: "MALFORMED_COLLECTION", path, data: E.toError(e) })
)
)
),
// Validating collections to be HoppRESTCollection.
TE.chainW(
TE.fromPredicate(A.every(isRESTCollection), () =>
error({
code: "MALFORMED_COLLECTION",
path,
data: "Please check the collection data.",
})
)
)
);
return maybeArrayOfCollections as HoppCollection<HoppRESTRequest>[]
};

View File

@@ -36,6 +36,8 @@ module.exports = {
"vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"@typescript-eslint/no-unused-vars":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"import/default": "off",

View File

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 383 B

View File

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 571 B

View File

Before

Width:  |  Height:  |  Size: 555 B

After

Width:  |  Height:  |  Size: 555 B

View File

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 378 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: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

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