Compare commits

..

486 Commits

Author SHA1 Message Date
Andrew Bastin
8450fb6596 chore: release 2023.4.1 2023-04-23 16:44:51 +05:30
Anwarul Islam
41fa3b5a8c fix: wrong tab selected after navigating from different route (#3012) 2023-04-23 16:06:11 +05:30
Nivedin
522de45a62 fix: request name not updating in the save request modal (#3010) 2023-04-23 15:47:06 +05:30
Anwarul Islam
4acc4b2dda fix: language switching issue to en from slug (#3006)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-04-22 16:22:30 +05:30
Liyas Thomas
c1f4855daf fix: non-prettified output on large JSON objects (#3008) 2023-04-21 21:09:25 +05:30
Nivedin
3506e96cfd fix: selected env changed while sidebar collapsed (#3002) 2023-04-21 20:27:04 +05:30
Liyas Thomas
b42a94ed77 chore: use auto-imported icons (#2998) 2023-04-21 20:20:22 +05:30
Liyas Thomas
80da790a3c chore: improve tabs scrollbar & unsaved request indicator (#3003) 2023-04-21 20:08:43 +05:30
Liyas Thomas
d6c706d0f9 fix: unwanted transitions caused pane layout shift (#2988) 2023-04-21 19:55:59 +05:30
Liyas Thomas
bd09a6ac45 i18n: updated locales to reflect latest strings (#2989) 2023-04-19 13:56:15 +05:30
Liyas Thomas
4ada31b20e docs: added border to screenshots (#2987) 2023-04-18 23:14:52 +05:30
Liyas Thomas
5d8b55e96b docs: fixed broken documentation links (#2997) 2023-04-18 23:14:06 +05:30
Liyas Thomas
eab4893aa2 docs: updated screenshots (#2984) 2023-04-13 22:50:53 +05:30
Balu Babu
4806499040 fix: fixed incorrect GOOGLE_SCOPE env value in .env.example file (#2983) 2023-04-13 16:21:37 +05:30
Andrew Bastin
1b1c02ceaa chore: update README 2023-04-11 18:31:48 +05:30
Andrew Bastin
a8f0a8a253 chore: remove hoppscotch-web from self-hosted 2023-04-11 18:31:08 +05:30
Andrew Bastin
b68115d3b2 refactor: change user collections min title length to 1 2023-04-11 15:40:26 +05:30
Akash K
c353d60ddc refactor: refactor collection to not use mapper (#80) 2023-04-11 15:09:32 +05:30
Andrew Bastin
5d1337f15d chore: merge central/staging into self-hosted/main 2023-04-11 14:37:45 +05:30
Akash K
eeee8af806 chore: changes to support id based syncing for collections (#2977) 2023-04-11 14:30:01 +05:30
Andrew Bastin
61b9aca746 chore: update ci actions 2023-04-10 13:14:47 +05:30
Joel Jacob Stephen
c4358b91a2 fix: changed vite icon to hoppscotch icon and changed title in admin dashboard (#82) 2023-04-10 13:07:59 +05:30
Balu Babu
4ce9e67460 chore: added global lint and test commands to backend package (#81)
* chore: added global lint and test commands to backend package

* chore: removed lint command from root scope execution
2023-04-10 12:25:45 +05:30
Andrew Bastin
8e25598a78 chore: update CODEOWNERS file 2023-04-10 11:22:39 +05:30
Andrew Bastin
e88e6a7bcd chore: run vite and gql-codegen in parallel on selfhost-web dev mode 2023-04-09 22:52:02 +05:30
Andrew Bastin
971dfc4c14 chore: update CODEOWNERS to add selfhost-web and sh-admin 2023-04-09 22:32:59 +05:30
Andrew Bastin
9d9bf84c3f chore: set sh-admin and selfhost-web version to 2023.4.0 2023-04-09 22:29:23 +05:30
Andrew Bastin
f7e170865d chore: merge hoppscotch/staging into self-hosted/main 2023-04-09 21:58:58 +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
45c84beb81 fix: environments being duplicated (#77) 2023-04-09 14:50:12 +05:30
Andrew Bastin
4a4ee19ba9 chore: merge hoppscotch/staging into self-hosted/main 2023-04-09 14:32:34 +05:30
Akash K
f1a812dae2 refactor: add optional ids to environments (#2974) 2023-04-09 14:30:42 +05:30
Joel Jacob Stephen
a4781d5882 refactor: load terms and privacy policy from env variables in dashboard login page (#79) 2023-04-08 16:51:48 +05:30
Nivedin
0dba28c388 chore: admin-dashboard team page UI polish (#75) 2023-04-08 16:48:33 +05:30
Joel Jacob Stephen
67f7e6a6d2 fix: dashboard auth redirection and magic link login issues (#76)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-04-08 15:22:39 +05:30
Balu Babu
7668be50ae chore: added orderBy field to query to fetch childCollectionList 2023-04-08 05:56:08 +05:30
Ankit Sridhar
100664f77e fix: refactor related to checklist observation (#73)
* refactor: removed unused files, dependencies and added valid callback URL

* chore: update env example

* test: fixed issue with subscription on deleteUserCollection in UserCollection module

* test: fixed time related issue with auth service methods

* chore: removed unused dependencies in auth.service file

---------

Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
Co-authored-by: Balu Babu <balub997@gmail.com>
2023-04-07 22:22:41 +05:30
Joel Jacob Stephen
e54f837b83 fix: admin dashboard bugs (#74) 2023-04-07 03:23:55 +05:30
Akash K
a33337ae0c fix: handle user/not_found response from backend (#72) 2023-04-07 03:19:18 +05:30
Andrew Bastin
13aa456c3c chore: merge hoppscotch/staging into self-hosted/main 2023-04-07 03:16:27 +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
Mir Arif Hasan
696cf8490b refactor: removing unused import, commented codes, improved cursor query (#69)
* chore: refactor code in some modules

* refactor: getTeamsOfUser functino

* chore: remove unused import

* chore: revert some changes
2023-04-06 19:54:10 +05:30
Mir Arif Hasan
ffc08227dd fix: all unit test cases for backend modules (HBE-171) (#51)
* fix: if-condition for getCollectionOfRequest func

* fix: all test cases for team request module

* fix: user collection test case

* fix: team module test case

* refactor: updated test description for last implemented changes in admin and removed commented code

---------

Co-authored-by: ankitsridhar16 <ankit.sridhar16@gmail.com>
2023-04-06 19:53:04 +05:30
Mir Arif Hasan
6cb3a2de43 feat: removed unused dockerfile (#71)
* feat: removed unused dockerfile

* chore: update env example

* chore: env example update
2023-04-06 19:04:44 +05:30
Akash K
47543e46f2 chore: disable export as secret gist in selfhosted (#68) 2023-04-06 15:39:31 +05:30
Andrew Bastin
abd7b4f0f4 chore: merge hoppscotch/staging into self-hosted/main 2023-04-06 15:25:36 +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
Balu Babu
2435436580 chore: changes origins to view whitelisted origins in backend (#70) 2023-04-06 14:04:04 +05:30
Balu Babu
22aa8ee334 hotfix: magiclink dynamic email redirection url (#67)
* chore: modified magiclink /signin function to work with origin

* chore: modified testcases for signInMagicLink to reflect new changes

* chore: removed prisma migration file

* chore: removed admin module dulicate from guards folder

* chore: implemented ENUMs for origins in signin method

* chore: added VITE_ADMIN_URL to .env.example file
2023-04-06 12:11:01 +06:00
Ankit Sridhar
6d688ed2bc refactor: removing unused env variables from env example (#66) 2023-04-05 21:53:27 +05:30
Andrew Bastin
46e204165d chore: merge hoppscotch/staging into self-hosted/main 2023-04-05 21:49:59 +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
Andrew Bastin
9bfb965e63 chore: merge hoppscotch/staging into self-hosted/main 2023-04-05 16:37:44 +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
Andrew Bastin
47bfef958b chore: merge hoppscotch/staging into self-hosted/main 2023-04-04 21:12:21 +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
Joel Jacob Stephen
e27dc1f7a2 refactor: polishing of admin dashboard teams module (#64)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-04-04 13:48:02 +05:30
Akash K
ea847d7d32 chore: remove unwanted logs & use new gql generation for selfhosted-web (#65) 2023-04-04 04:18:29 +05:30
Andrew Bastin
87be0ef073 chore: merge hoppscotch/staging into self-hosted/main 2023-04-04 04:11:58 +05:30
Akash K
c3c3fc6720 chore: use IDs instead of Strings in graphql queries (#2961) 2023-04-04 04:10:32 +05:30
Ankit Sridhar
8bdb9a657f feat: self host packaging (HBE-166) (#41)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-04-04 03:17:18 +05:30
Andrew Bastin
71e1ada641 chore: merge hoppscotch/staging into self-hosted/main 2023-04-04 02:17:29 +05:30
Akash K
37a3b72025 chore: move analytics to platform (#2960) 2023-04-04 02:15:20 +05:30
Akash K
c49573db65 fix: serialize user sessions properly (#62)
* fix: cast prisma user object to user object

* chore: improved consistency

* chore: cast function renamed

---------

Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
2023-04-03 09:47:40 +05:30
Akash K
97c3e6089d feat: implement tabs syncing to selfhost-web (#63) 2023-04-01 18:46:54 +05:30
Akash K
8586ced3cc feat: implement user history syncing for selfhost (#60) 2023-04-01 18:24:58 +05:30
Akash K
2b44ede92b feat: implement user settings syncing for selfhost (#59) 2023-04-01 17:42:11 +05:30
Akash K
86a12e2d28 feat: implement collections syncing for selfhosted (#42) 2023-04-01 17:22:42 +05:30
Andrew Bastin
9d7509b4dd chore: merge hoppscotch/staging into self-hosted/main 2023-03-31 01:07:56 +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
Andrew Bastin
7b78d99ac4 chore: merge hoppscotch/staging into self-hosted/main 2023-03-30 15:14:59 +05:30
Akash K
dbb45e7253 chore: add removeDuplicateEntry dispatcher to history store (#2955) 2023-03-30 15:13:25 +05:30
Akash K
7286d3b94f feat: add reqType to userHistoryDeletedMany subscription (#61) 2023-03-30 12:17:00 +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
Nivedin
885c0dc500 fix: sh-admin dashboard bugs and UI polish (#56) 2023-03-29 23:49:34 +05:30
Andrew Bastin
b826b53cee chore: merge hoppscotch/staging into self-hosted/main 2023-03-29 21:03:52 +05:30
Balu Babu
ea93162056 refactor: modifed return types of mutation/subscriptions in UserCollections (#57)
* refactor: modifed userCollectionRemoved subscription to return custom return type

* chore: created new return type for export to JSON mutation in UserCollection

* refactor: added reqType to exportUserCollectionsToJSON query

* chore: remove duplicate enum in user-collection.model.ts file
2023-03-29 15:50:48 +05:30
Akash K
39afeab5f8 refactor: make editFolder, editCollection take Partial collection as parameter (#2952) 2023-03-29 12:00:20 +05:30
Nivedin
b6950332ad refactor: sh admin login polish (#58) 2023-03-28 23:33:50 +05:30
Mir Arif Hasan
ccdce37f88 fix: enum type fixes in SDL (HBE-183) (#55)
* fix: added reqType typed in sdl

* fix: updateUserSession type
2023-03-28 17:46:41 +06:00
Ankit Sridhar
9d6a7f709c feat: introducing get team info by id in admin module as a query (HBE-182) (#54)
* feat: introducing get team info by id in admin module as a query

* chore: adding resolve field for admin

* chore: remove nullable false

* refactor: rename getTeamInfo to teamInfo

* refactor: make myRole nullable
2023-03-28 15:35:38 +05:30
Mir Arif Hasan
96a4125f15 feat: rate-limit annotation added in admin resolver (#53) 2023-03-24 15:21:46 +05:30
Ankit Sridhar
b16e90c10d fix: check for admin users when removing user as admin (HBE-180) (#52)
* fix: check if admin users are there in infra when removing user as an admin

* fix: corrected the logic for length check

* chore: update error message

* chore: add new error message
2023-03-24 15:21:23 +05:30
Joel Jacob Stephen
5164315243 fix: modified graphql files in dashboard users and teams module to match resolver changes (#50)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-03-22 18:19:37 +05:30
Balu Babu
3df0492275 refactor: removing Redis from pubsub in sh-backend (HBE-178) (#49)
* chore: removed production env check and redis as pubsub provider in pubsub module

* chore: removed pnpm-lock.yaml file from backend

* chore: removed migrations folder from prisma

* chore: removed RedisPubSub from pubsub service file and changed signature of asyncIterator method
2023-03-21 16:46:54 +05:30
Mir Arif Hasan
fa8ca0569d feat: introducing rate-limiting on queries, mutations and most of the REST endpoints (HBE-111) (#46)
* feat: rate-limiting guard added and configured in app module

* feat: rate-limit annotation added in controllers and resolvers (query, mutation, not subscription)

* docs: added comments
2023-03-21 16:45:50 +05:30
Ankit Sridhar
f78354a377 feat: Introducing Admin Module to Backend (HBE-83) (#21)
* feat: introducing admin module, resolvers and service files as a module

* feat: adding admin module in the app module

* feat: introducing admin guard and decorator for allowing admin operations

* feat: invited user model

* chore: added user invitation mail description to mailer service

* chore: added admin and user related error

* feat: added invited users as a new model in prisma

* chore: added admin related topics to pubsub

* chore: added service method to fetch all users from user table

* chore: added user deletion base implementation

* Revert "chore: added user deletion base implementation"

This reverts commit d1615ad83db2bae946e2d366a903d2f95051dabb.

* feat: adding team related operations to admin

* chore: adding admin related service methods to teams module service

* chore: adding admin related service methods to team coll invitations requests envs

* chore: added more module error messages

* chore: added admin check service method

* chore: added find individual user by UID in admin

* HBE-106 feat: introduced code to handle first time admin login setup (#23)

* test: wrote test cases for verifyAdmin route service method

* chore: added comments to verifyAdmin service method

* chore: deleted the prisma migration file

* chore: added find admin users

* feat: added user deletion into admin module

* chore: admin user related errors

* chore: fixed registry pattern in the shortcodes and teams to handle user deletion

* chore: add subscription topic for user deletion

* chore: updated user type in data handler

* feat: implement and fix user deletion

* feat: added make user admin mutation

* chore: added unit tests for admin specific service methods in admin module

* chore: added invitation not found error

* chore: added admin specific operation test cases in specific modules

* chore: added tests related to user deletion and admin related operation in user module

* chore: updated to error constant when invitations not found

* chore: fix rebase overwritten methods

* feat: implement remove user as admin

* chore: add new line

* feat: introducing basic metrics into the self-hosted admin module (HBE-104) (#43)

* feat: introducing admin module, resolvers and service files as a module

* feat: adding admin module in the app module

* feat: introducing admin guard and decorator for allowing admin operations

* feat: invited user model

* chore: added user invitation mail description to mailer service

* chore: added admin and user related error

* feat: added invited users as a new model in prisma

* chore: added admin related topics to pubsub

* chore: added service method to fetch all users from user table

* chore: added user deletion base implementation

* Revert "chore: added user deletion base implementation"

This reverts commit d1615ad83db2bae946e2d366a903d2f95051dabb.

* feat: adding team related operations to admin

* chore: adding admin related service methods to teams module service

* chore: adding admin related service methods to team coll invitations requests envs

* chore: added more module error messages

* chore: added admin check service method

* chore: added find individual user by UID in admin

* HBE-106 feat: introduced code to handle first time admin login setup (#23)

* test: wrote test cases for verifyAdmin route service method

* chore: added comments to verifyAdmin service method

* chore: deleted the prisma migration file

* chore: added find admin users

* feat: added user deletion into admin module

* chore: admin user related errors

* chore: fixed registry pattern in the shortcodes and teams to handle user deletion

* chore: add subscription topic for user deletion

* chore: updated user type in data handler

* feat: implement and fix user deletion

* feat: added make user admin mutation

* chore: added unit tests for admin specific service methods in admin module

* chore: added invitation not found error

* chore: added admin specific operation test cases in specific modules

* chore: added tests related to user deletion and admin related operation in user module

* chore: updated to error constant when invitations not found

* chore: fix rebase overwritten methods

* feat: implement remove user as admin

* chore: add new line

* chore: created new GQL return type for admin module

* chore: created resolver and service method for method to fetch org metrics

* chore: removed all entities relevant to seperate query for fetching admin metrics

* chore: created all resolvers for metrics

* feat: completed adding field resolves to query org metrics

* test: wrote tests for all metrics related methods in admin module

* test: added test cases for get count functions in multiple modules

* chore: removed prisma migration folder

* Delete backend-schema.gql

* chore: resolved merge conflicts in team test file

---------

Co-authored-by: ankitsridhar16 <ankit.sridhar16@gmail.com>

* refactor: update mailer service to stop using postmark (#38)

* refactor: update mailer service to stop using postmark

* chore: remove postmark as a dep and move out postmark code

* chore: remove postmark variables from .env.example

* chore: add formal errors for mailer initialization errors

* chore: add and update jsdoc comments in mailer service methods

* chore: added user invitation mail description to mailer service

* chore: updated with review changes requested for admin module

* feat: adding admin resolver to gql schema

* feat: adding input args for admin resolvers

* chore: invited user renamed

* chore: updated mailer service to be compatible with new mailer

* chore: updated team service with review changes

* chore: updated team collection service with review changes

* chore: updated team environments service with review changes

* chore: updated team requests service with review changes

* chore: updated user service with review changes

* refactor: invited user model

* chore: review changes implemented

* chore: implemented the review changes for admin, user and teams module

* chore: removed error handling and implemented review changes

* refactor: naming change for IsAdmin

---------

Co-authored-by: Balu Babu <balub997@gmail.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-03-21 16:42:30 +05:30
Anwarul Islam
8b1d8e6a90 feat: introducing team (HBE-86) (#36) 2023-03-21 16:05:01 +05:30
Andrew Bastin
2244fb0523 chore: add numberScalarMode for schema gen as well 2023-03-20 22:01:06 +05:30
Joel Jacob Stephen
c611b39f52 feat: introducing metrics to admin dashboard homepage (#47) 2023-03-20 19:26:03 +05:30
Joel Jacob Stephen
73a0255ae8 refactor: polish UI of admin dashboard users module (#48)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-03-20 19:18:03 +05:30
Andrew Bastin
e978541bf1 refactor: update mailer service to stop using postmark (#38)
* refactor: update mailer service to stop using postmark

* chore: remove postmark as a dep and move out postmark code

* chore: remove postmark variables from .env.example

* chore: add formal errors for mailer initialization errors

* chore: add and update jsdoc comments in mailer service methods
2023-03-15 14:02:55 +05:30
Andrew Bastin
ae77c60c53 chore: merge hoppscotch/staging into self-hosted/main 2023-03-15 11:32:08 +05:30
Balu Babu
b0d9a934d9 hotfix: fixing type errors in backend graphql layer (HBE-174) (#45)
* chore: added the nest graphql fix for making it use Int over Float

* chore: changed the type of cursor to Int in PaginationArgs

* chore: fixed description mismatch in rootUserCreation functions in UserCollection module

* chore: removed unused title type in UserRequest input-args

* fix: added ID scaler in user-request input type

* fix: added ID scaler in team-invitation and user-history

---------

Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
2023-03-15 11:20:45 +05:30
Andrew Bastin
1583c86c78 chore: add dotenv as dev dependency to fix staging issues 2023-03-14 22:32:28 +05:30
Balu Babu
a779ba5c0e hotfix: adding dynamic redirection in self-host auth system (HBE-173) (#40)
* chore: completed base auth implementation with redirectUrl

* chore: completed base auth fix with redirect_uri

* chore: added whitelist based redirection

* chore: added a env variable for session secret in main.ts

* chore: removed migrations folder from prisma directory
2023-03-14 19:19:22 +05:30
Balu Babu
be46ed2686 refactor: adding JSON import/export functions to UserCollections module (HBE-169) (#37)
* feat: created exportUserCollectionsToJSON mutation for UserCollection module

* chore: added comments to export functions

* chore: added type and user ownership checking to creation methods

* chore: replaced request property with spread request object instead

* chore: completed all changes requested in inital review of PR

* chore: explicitly exporting request title in export function

* chore: explicitly exporting request title in export function

* chore: added codegen folder to gitignore

* chore: removed gql-code gen file from repo
2023-03-14 18:31:47 +05:30
Andrew Bastin
e5002b4ef3 chore: merge hoppscotch/staging into self-hosted/main 2023-03-14 14:18:11 +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
Balu Babu
28dbaf317e chore: added the correct GQL return type to userRequestMoved subscription (#44) 2023-03-14 11:49:27 +05:30
Joel Jacob Stephen
753db25e4c refactor: remove of light mode from admin dashboard + added README, .env.example (#33) 2023-03-13 18:57:29 +05:30
Andrew Bastin
65719b560b feat: introduce gql schema sdl generation to the backend (#35)
* feat: introduce gql schema sdl generation to the backend

* chore: update gql-codegen consumers to get schema from generated sdl

* chore: hoppscotch-backend generates gql sdl on postinstall

* fix: add back missed part of generate-gql-sdl script

* chore: updated generate sdl script to hardcode whitelisted domains

* chore: add prisma generate on postinstall script

---------

Co-authored-by: ankitsridhar16 <ankit.sridhar16@gmail.com>
2023-03-13 18:52:50 +05:30
Andrew Bastin
44402ac6e1 chore: merge hoppscotch/hoppscotch/staging into hoppscotch/self-hosted/main 2023-03-13 17:12:16 +05:30
Akash K
7e1b26c6a9 chore: move collections sync system to platform (#2940) 2023-03-13 17:08:05 +05:30
Mir Arif Hasan
8550c92e37 feat: subscription return response updated for moveUserRequest (#39)
* feat: subscription return response updated for moveUserRequest

* feat: update test cases
2023-03-13 16:15:51 +05:30
Mir Arif Hasan
7d3b2c064a refactor: Refactoring of Team Request with Reordering (HBE-151) (#20)
* feat: createdOn, updatedOn added in team-request schema and updateTeamReq resolver refactored

* feat: resolver name changed for updateTeamRequest

* refactor: searchForTeamRequest resolver

* refactor: some functions refactored

* refactor: team-request service and subscriptions

* refactor: update GqlRequestTeamMemberGuard

* feat: team request reordering add

* feat: handle exception on update Team Request

* chore: change some return statement

* fix: change field name of MoveTeamRequestArgs

* feat: publish message update for reorder team req

* test: fix all the exists cases

* fix: add return statement

* test: add few functions test cases

* feat: made backward compatible

* fix: team-member guard for retrive user

* fix: few bugs

* chore: destructured parameters in service methods

* test: fix test cases

* feat: updateLookUpRequestOrder resolver added

* test: fix test cases

* chore: improved code consistency

* fix: feedback changes

* fix: main changes
2023-03-09 20:59:39 +06:00
Balu Babu
2a715d5348 refactor: refactoring Team-Collections with reordering in self-host (HBE-150) (#34)
* chore: removed firebase module as a dependency from team-collection module

* chore: modified team-collection resolver file to use input-args types

* chore: modified getTeamOfCollection service method and resolver

* chore: modified getParentOfCollection service method in team-collection module

* chore: modified getChildrenOfCollection service method in team-collection module

* chore: added new fields to TeamCollection model in prisma schema file

* chore: modified getCollection service method and resolver in team-collection module

* chore: modified createCollection service method and resolver in team-collection module

* chore: created cast helper function to resolve issue with creation mutation in team-collection

* chore: modified teamCollectionRemoved subscription return types

* chore: removed return types from subscriptions in team-collection module

* chore: removed all instances of getTeamCollections service method in team-collection module

* feat: added mutation to handle moving collections and supporting subscriptions

* feat: added mutation to re-ordering team-collection order

* chore: added teacher comments to both collection modules

* test: added test cases for getTeamOfCollection service method

* test: added test cases for getParentOfCollection service method

* test: added test cases for getChildrenOfCollection service method

* test: added test cases for getTeamRootCollections service method

* test: added test cases for getCollection service method

* test: added test cases for createCollection service method

* chore: renamed renameCollection to renameUserCollection in UserCollection module

* test: added test cases for renameCollection service method

* test: added test cases for deleteCollection service method

* test: added test cases for moveCollection service method

* test: added test cases for updateCollectionOrder service method

* chore: added import and export to JSON mutations to team-collection module

* chore: created replaceCollectionsWithJSON mutation in team-collection module

* chore: moved the mutation and service method of importCollectionFromFirestore to the end of file

* chore: added helper comments to all import,export functions

* chore: exportCollectionsToJSON service method orders collections and requests in ascending order

* chore: added test cases for importCollectionsFromJSON service method

* chore: added ToDo to write test cases for exportCollectionsToJSON

* chore: removed prisma migration folder

* chore: completed all changes requested in inital PR review

* chore: completed all changes requested in second  PR review

* chore: completed all changes requested in third PR review
2023-03-09 19:37:40 +05:30
Anwarul Islam
9b76d62753 feat: introducing Auth for admin dashboard (HBE-138) (#32) 2023-03-09 10:59:40 +05:30
Akash K
80898407c3 feat: implement environments for selfhosted (#30) 2023-03-08 16:47:29 +05:30
Andrew Bastin
40208a13e0 chore: merge central/staging into main. 2023-03-07 16:13:59 +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
Joel Jacob Stephen
90569192b7 feat: implementation of users module of the admin dashboard (#29)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-03-03 19:26:34 +05:30
Mir Arif Hasan
223150550f feat: user request module with re-ordering (HBE-78) (#11)
* feat: added user-request schema in prisma

* feat: basic mutation and queries implementation

* fix: enum registration in graphql

* feat: user resolver added for user requests

* chore: refactor codes

* feat: transaction added in request reordering operation

* feat: pubsub added in user request

* refactor: user request service

* chore: feedback added

* chore: code improvement

* fix: bug fix

* feat: request type update in schema and JSDoc added

* test: fetchUserRequests and fetchUserRequest unit test added

* chore: refactor two functions

* test: unit test added for more functions

* chore: code readability improved

* test: added unit test for reorderRequests function

* feat: subscriptions added

* fix: User reference to AuthUser

* fix: User to AuthUser in test file

* chore: update dto file extensions

* feat: relation added in schema level

* chore: add function for db to model type casting

* feat: filter with title and collectionID add in userRequest resolver

* feat: resolvers added for userCollection in request module, and move inputTypes in a single file

* test: test file updated

* docs: description updated

* feat: createdOn, updatedOn added in user request schema

* chore: (delete in future) user collection module add for testing purpose

* feat: separate resolvers for create, update, delete user request based on req type

* feat: used paginationArgs from common types

* fix: shift InputTypes to ArgsTypes

* docs: update docs

* feat: avoid destructuring

* test: fix test cases for create and update

* docs: update JS doc

* feat: separate object variables for moveRequest function

* test: fix test case for moveRequest function

* feat: saperate parameters for fetchUserRequest

* test: fix test cases for fetchUserRequests

* feat: update some query names and made review changes

* test: fix test cases

* feat: remove filtering with title

* test: fix text cases for fetchUserRequests func

* feat: update subscription key

* feat: edge case handled for user request creation

* test: fix test case

* fix: user field resolver

* fix: fetch user req issue

* fix: update with type check

* test: fix test cases

* feat: type checked on move request

* test: add test case for typeValidity check func

* fix: edge condition added in if statement

* fix: error message

* chore: removed user collection from this branch

* fix: typos
2023-03-03 16:51:49 +06:00
Balu Babu
80c6f600db hotfix: refresh token cookie migration (HBE-167) (#31)
* chore: removed signed cookies from refresh token strategy

* chore: removed signed cookies from refresh RTCookie decorator
2023-03-03 15:52:26 +05:30
Balu Babu
a938be3712 feat: Introducing user-collections into self-host (HBE-98) (#18)
* feat: team module added

* feat: teamEnvironment module added

* feat: teamCollection module added

* feat: team request module added

* feat: team invitation module added

* feat: selfhost auth frontend (#15)

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

* feat: bringing shortcodes from central to selfhost

* chore: added review changes in resolver

* chore: commented out subscriptions

* chore: bump backend prettier version

* feat: created new user-collections module with base files

* feat: added new models for user-collection and user-request tables in schema.prisma file

* feat: mutations to create user-collections complete

* feat: added user field resolver for userCollections

* feat: added parent field resolver for userCollections

* feat: added child field resolver with pagination for userCollections

* feat: added query to fetch root user-collections with pagination for userCollections

* feat: added query to fetch user-collections for userCollections

* feat: added mutation to rename user-collections

* feat: added mutation to delete user-collections

* feat: added mutation to delete user-collections

* refactor: changed the way we fetch root and child user-collection counts for other operations

* feat: added mutation to move user-collections between root and other child collections

* refactor: abstracted orderIndex update logic into helpert function

* chore: mutation to update order root user-collections complete

* feat: user-collections order can be updated when moving it to the end of list

* feat: user-collections order update feature complete

* feat: subscriptions for user-collection module complete

* chore: removed all console.logs from user-collection.service file

* test: added tests for all field resolvers for user-collection module

* test: test cases for getUserCollection is complete

* test: test cases for getUserRootCollections is complete

* test: test cases for createUserCollection is complete

* test: test cases for renameCollection is complete

* test: test cases for moveUserCollection is complete

* test: test cases for updateUserCollectionOrder is complete

* chore: added createdOn and updatedOn fields to userCollections and userRequests schema

* chore: created function to check if title are of valid size

* refactor: simplified user-collection creation code

* chore: made changed requested in initial PR review

* chore: added requestType enum to user-collections

* refactor: created two seperate queries to fetch root REST or GQL queries

* chore: created seperate mutations and queries for REST and GQL root/child collections

* chore: migrated all input args classess into a single file

* chore: modified createUserCollection service method to work with different creation inputs args type

* chore: rewrote all test cases for user-collections service methods with new CollType

* fix: added updated and deleted subscription changes

* fix: made all the changes requested in the initial PR review

* fix: made all the changes requested in the second PR review

* chore: removed migrations from prisma directory

* fix: made all the changes requested in the third PR review

* chore: added collection type checking to updateUserCollectionOrder service method

* chore: refactored all test cases to reflect new additions to service methods

* chore: fixed issues with pnpm-lock

* chore: removed migrations from prisma directory

* chore: hopefully fixed pnpm-lock issues

* chore: removed console logs in auth controller

---------

Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
Co-authored-by: Akash K <57758277+amk-dev@users.noreply.github.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: ankitsridhar16 <ankit.sridhar16@gmail.com>
2023-03-03 15:03:05 +05:30
Andrew Bastin
31c6b0664f chore: merge hoppscotch/hoppscotch/staging into main 2023-03-02 18:55:55 +05:30
Akash K
3fa4052538 chore: move environments firebase things to hoppscotch-web (#2939) 2023-03-02 18:50:13 +05:30
Ankit Sridhar
1780f3858d fix : User history fix for returning most recently executed 50 user history REST/GraphQL items (HBE-165) (#27)
* chore: updated fetchUserHistory operation to return recently executed 50 entries

* chore: updated history to use PaginationArgs for operation

* chore: updated GraphQL resolver name
2023-03-02 14:56:50 +05:30
Liyas Thomas
f2de0dc673 chore: minor ui improvements 2023-02-28 16:25:46 +05:30
Mir Arif Hasan
5eb85fd99c fix: cleaning deprecated resolvers on Team Module (HBE-157) (#25)
* fix: retrive user object from gql-team-member-guard

* feat: removed addTeamMemberByEmail resolver
2023-02-28 14:22:00 +06:00
Joel Jacob Stephen
3f59597864 feat: introducing self hosted admin dashboard package (#12)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-02-28 13:13:27 +05:30
Balu Babu
2ba05a46ee HBE-164 refactor: subscriptions auth cookie fix (#26)
* chore: added error handling to cookie extraction logic for subscriptions

* chore: removed migration file from prisma directory
2023-02-27 19:32:33 +05:30
Ankit Sridhar
292ed87201 fix: added updated and deleted subscription changes (#24) 2023-02-27 19:03:48 +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
Liyas Thomas
bd5f95b1c5 chore: replace material-icons with lucide-icons 2023-02-24 18:41:36 +05:30
Liyas Thomas
167dfc3847 chore: add fonts to ui package [skip ci] 2023-02-24 18:28:39 +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
Balu Babu
1860057a25 HBE-147 refactor: Introduce shortcodes into self-host refactored to pseudo-fp format (#22)
* refactor: refactor all queries,mutations and subscriptions for shortcode module

* test: rewrote test cases for shortcodes

* chore: modified shortcode error code

* chore: created helper function to do shortcode type conversion in service file

* chore: simplifed logic to fetch user shortcodes with cursor pagination

* chore: removed migrations file

* chore: removed unused imports in shortcodes module

* chore: modified generateUniqueShortCodeID function

* chore: modified generateUniqueShortCodeID function

* chore: changed jwtService to use verify instead of decode

* docs: added teacher comments to all shortcodes service methods

* chore: removed stale test cases from shortcode modules
2023-02-22 17:40:53 +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
Mir Arif Hasan
24dd535d9e HBE-155 Common input-args for Pagination (#19)
* feat: common input-args added

* chore: move common input-types.args.ts into types folder

* fix: add gql InputType annotation

* docs: update field description
2023-02-16 11:58:07 +06:00
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
Mir Arif Hasan
b60d45ba76 HBE 145 - fixes cookie parse issue (#17)
* feat: handled cookie parsing

* chore: enum added

* chore: enum name updated
2023-02-10 12:39:04 +06:00
Andrew Bastin
7336a3d9c7 chore: bump backend prettier version 2023-02-09 18:05:41 +05:30
ankitsridhar16
c7829201e1 chore: commented out subscriptions 2023-02-09 17:31:54 +05:30
ankitsridhar16
63b6c76f51 chore: added review changes in resolver 2023-02-09 17:31:54 +05:30
ankitsridhar16
056a5df4e1 feat: bringing shortcodes from central to selfhost 2023-02-09 17:31:54 +05:30
Akash K
757d1add5b feat: selfhost auth frontend (#15)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-02-09 01:12:44 +05:30
Andrew Bastin
3cf3feb2ae chore: updated dockerfile to install deps less 2023-02-08 22:50:37 +05:30
Mir Arif Hasan
46579900cd feat: team invitation module added 2023-02-08 20:21:51 +06:00
Mir Arif Hasan
2d4a5a30f7 test: instance create error commenting mockFB 2023-02-08 20:21:51 +06:00
Mir Arif Hasan
2ed5a045de feat: team request module added 2023-02-08 20:21:51 +06:00
Mir Arif Hasan
4b42496273 feat: teamCollection module added 2023-02-08 20:21:51 +06:00
Mir Arif Hasan
c5d8a446ae feat: teamEnvironment module added 2023-02-08 20:21:51 +06:00
Mir Arif Hasan
9bee62ada9 feat: team module added 2023-02-08 20:21:51 +06:00
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
420359066e Merge remote-tracking branch 'central/main' 2023-02-08 18:43:40 +05:30
Andrew Bastin
99918ee0c0 chore: prettier version bump and related fixes 2023-02-08 18:35:27 +05:30
Balu Babu
480e9ea3ec Merge pull request #13 from hoppscotch/hotfix/subscriptions
Hotfix: fix subscription cookie issue
2023-02-08 16:46:49 +05:30
Balu Babu
2ee4029e04 chore: removed migrations in prisma 2023-02-08 15:50:02 +05:30
Balu Babu
edd186bdfe chore: changed const names in subscriptionContextCookieParser 2023-02-08 15:48:40 +05:30
Liyas Thomas
864d40d934 chore: improved theme colors 2023-02-08 15:13:24 +05:30
Balu Babu
856752db21 chore: added env_file property to SH-backend docker-compose file 2023-02-08 14:41:05 +05:30
Balu Babu
7fde6db9d1 refactor: changed onConnect function in subscriptionHandler to decode cookies for subscriptions 2023-02-08 14:36:01 +05:30
Balu Babu
0aac046a0e refactor: changed context to contain just req,res and connection objects for GraphQLModule 2023-02-08 14:27:24 +05:30
Balu Babu
1ad11adb94 chore: remved signed flag from cookie setter in auth/helper.ts 2023-02-08 14:25:05 +05:30
Balu Babu
505adea0ef chore: changed jwt stratergy to use cookies instead of signedCookies 2023-02-08 14:24:16 +05:30
Balu Babu
9c64721bf0 refactor: refactored gql-user-decorator to work for subscriptions 2023-02-08 14:23:27 +05:30
Balu Babu
965fdad8b1 refactor: refactored gql-auth-guard to work for websocket based subscriptions 2023-02-08 14:20:51 +05:30
Balu Babu
a6d6589811 chore: removed SIGNED_COOKIE_SECRET from cookieParser in main.ts and .env.example files 2023-02-08 14:18:39 +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
amk-dev
3b7a16c439 Merge remote-tracking branch 'hoppscotch/hoppscotch/main' 2023-02-07 19:49:35 +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
Balu Babu
6711d752e2 Merge pull request #9 from hoppscotch/feat/user-authentication
feat: introduce custom user authentication module
2023-02-02 19:09:47 +05:30
Balu Babu
65472bed54 test: fixed date issue in user-history test 2023-02-02 19:09:26 +05:30
Balu Babu
a188ad68ed Merge branch 'main' into feat/user-authentication 2023-02-02 18:54:45 +05:30
Andrew Bastin
bb01afeb99 fix: jest crashing out on tests 2023-02-02 13:39:54 +05:30
Balu Babu
a1be3a3e77 chore: added nestjs version into auth module 2023-02-01 19:19:39 +05:30
Balu Babu
b5e7877912 chore: moved auth utility functions to auth/helper.ts from src/utils.ts 2023-02-01 18:47:11 +05:30
Balu Babu
587e7118c9 chore: added versioning to auth rest module 2023-02-01 18:36:03 +05:30
Balu Babu
8c5ffb88a3 fix: changed user type to AuthUser in user-settings resolver 2023-02-01 18:24:18 +05:30
Balu Babu
2a00f41ef8 test: refactored all test cases with new user type change 2023-02-01 17:52:33 +05:30
Nivedin
f676f94278 fix: graphql save request emit payload (#2913) 2023-02-01 15:20:13 +05:30
Balu Babu
4ca762344c chore: captialized DTO names 2023-02-01 14:58:31 +05:30
Balu Babu
5c5ab5bad5 fix: changed all refrences to passwordlessVerification to verificationTokens in auth module 2023-02-01 11:58:15 +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
Balu Babu
3afc89db6b fix: fixed all issues raised in initial PR review 2023-01-30 18:55:53 +05:30
Andrew Bastin
bfc45993f8 Merge remote-tracking branch 'central/main' 2023-01-30 18:06:19 +05:30
Liyas Thomas
b95e2b365a fix: broken environment highlight color 2023-01-30 10:01:33 +05:30
Balu Babu
a8d50223aa refactor: changed auth module to work with signed cookies 2023-01-30 06:31:10 +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
Balu Babu
1bbcd638b8 chore: converted hardcoded whitelisted domains in GQL module to use env variables 2023-01-27 13:42:28 +05:30
Balu Babu
fe73750d66 fix: fixed the user type error in auth.service file 2023-01-27 13:38:36 +05:30
Balu Babu
ca4c576f78 chore: resolved all merge conflicts 2023-01-24 19:35:40 +05:30
Ankit Sridhar
648637a1a1 Merge pull request #8 from hoppscotch/feat/user-session
feat: introducing current session of rest and gql (backend)
2023-01-24 17:09:30 +05:30
Mir Arif Hasan
91adf379da chore: removed redundant comment 2023-01-24 17:27:40 +06:00
Mir Arif Hasan
08ca57cba2 feat: user session resolver added for sessions updates 2023-01-24 17:19:24 +06:00
Mir Arif Hasan
e6fcb1272a Merge branch 'main' into feat/user-session 2023-01-24 15:40:19 +06:00
Mir Arif Hasan
60a5acdb9d fix: typo 2023-01-24 15:28:27 +06:00
Mir Arif Hasan
2221261ec2 fix: typo 2023-01-24 15:27:03 +06:00
Mir Arif Hasan
6627514e88 chore: pulled from main 2023-01-24 15:24:11 +06:00
Mir Arif Hasan
d7b02da719 test: more test case added for user module 2023-01-24 15:12:30 +06:00
Mir Arif Hasan
ebbe015bbc feat: db schema update from string to json column type 2023-01-24 15:10:54 +06:00
Balu Babu
e03a92f8d8 Merge pull request #7 from hoppscotch/feat/user-history
feat: introduce user history in self hosted
2023-01-24 13:05:43 +05:30
ankitsridhar16
7d98c1b355 Merge remote-tracking branch 'origin/main' into feat/user-history
# Conflicts:
#	packages/hoppscotch-backend/prisma/schema.prisma
2023-01-24 13:04:19 +05:30
ankitsridhar16
e78040a376 refactor: removed migrations 2023-01-24 13:00:34 +05:30
ankitsridhar16
97eedb568c refactor: added fetchUserHistoryByID method and added changes for spread 2023-01-24 12:53:44 +05:30
Balu Babu
161f1db40e Merge pull request #5 from hoppscotch/feat/user-settings
feat: introducing user-settings in self-hosted (backend)
2023-01-24 12:30:50 +05:30
Mir Arif Hasan
b50b97a4d1 chore: removed seed cmd form package.json 2023-01-24 12:50:31 +06:00
Mir Arif Hasan
e8e176ed40 fix: removed seed scripts 2023-01-24 12:49:22 +06:00
Mir Arif Hasan
bc82e9c7fa feat: user settings create subscription added and fixed typos 2023-01-24 07:26:47 +06:00
Balu Babu
73ace77305 refactor: changed the test cases to reflect change to UserHistory schema 2023-01-24 02:02:17 +05:30
Balu Babu
4023dcf09d chore: changed the return data to use spread operator 2023-01-24 00:40:14 +05:30
ankitsridhar16
ebf236b387 chore: added changes to creating a history in resolvers and service method 2023-01-23 22:51:37 +05:30
Mir Arif Hasan
e40d77420c chore: comment text updated 2023-01-23 21:54:19 +06:00
Mir Arif Hasan
27b9f57d7a test: pubsub test case added on user settings create 2023-01-23 21:51:46 +06:00
Mir Arif Hasan
3cd9639f34 test: feedback updated on test script 2023-01-23 21:32:00 +06:00
ankitsridhar16
a5a14f6c76 chore: applied review changes for resolvers 2023-01-23 20:21:44 +05:30
Mir Arif Hasan
bfac3f8ad0 feat: db column update from settings to properties 2023-01-23 20:25:48 +06:00
Mir Arif Hasan
dcadbac4d5 docs: update mutation description 2023-01-23 16:33:48 +06:00
ankitsridhar16
626d703d77 refactor: updated test cases to split subscription 2023-01-23 15:23:53 +05:30
ankitsridhar16
25b7ef3d2e refactor: updated pubsub publishing to leverage new topic defs and rewrote map logic 2023-01-23 15:23:02 +05:30
ankitsridhar16
d812e6ab96 refactor: user history resolver changes 2023-01-23 15:06:01 +05:30
ankitsridhar16
74e4a77ce6 chore: added topics for user history subscriptions 2023-01-23 15:04:05 +05:30
ankitsridhar16
863e1ee113 fix: fixed merge related conflicting data 2023-01-23 15:03:03 +05:30
Mir Arif Hasan
33e4a15830 feat: used new pubsub method to publish 2023-01-23 15:28:55 +06:00
Mir Arif Hasan
bf09786423 feat: used new pubsub method and resolve conflicts 2023-01-23 15:11:52 +06:00
ankitsridhar16
f7070dd3f7 Merge remote-tracking branch 'origin/main' into feat/user-history
# Conflicts:
#	packages/hoppscotch-backend/prisma/schema.prisma
#	packages/hoppscotch-backend/src/app.module.ts
2023-01-23 13:22:12 +05:30
ankitsridhar16
523c650c9d chore: updated primsa type for executed on 2023-01-23 13:13:19 +05:30
Ankit Sridhar
469d408b09 Merge pull request #10 from hoppscotch/refactor/subscriptionHandler
HBE-85 - Refactor SubscriptionHandler
2023-01-23 11:55:14 +05:30
ankitsridhar16
cb1b13bdb4 chore: updated filename to topicDefs 2023-01-23 11:49:41 +05:30
ankitsridhar16
480a34c0f7 fix: updated resolver pubsub topic name 2023-01-23 09:54:20 +05:30
Balu Babu
93479320ee chore: added teacher comments to all service methods 2023-01-23 06:04:23 +05:30
Balu Babu
b2acd5511c chore: user displayName and photoURL property updates itself when found with SSO providers 2023-01-23 05:31:03 +05:30
Balu Babu
96ed2f2119 test: wrote tests for auth service file 2023-01-23 05:07:59 +05:30
ankitsridhar16
606e0120ee chore: removed the logic for primitiveTypes as it is unused 2023-01-20 16:13:35 +05:30
ankitsridhar16
6da85fd286 chore: updated types to def and changed deleted many as a new indexed type 2023-01-20 16:12:45 +05:30
ankitsridhar16
298b960ef7 chore: removed comment 2023-01-20 15:18:04 +05:30
Balu Babu
ee3fbabece chore: updated .env.example file with new data 2023-01-20 15:00:54 +05:30
ankitsridhar16
0bed5cd99a chore: removed subscription handler related files 2023-01-20 14:57:47 +05:30
ankitsridhar16
bc55af27a7 chore: updated user environment to use PubSub instead of SubscriptionHandler 2023-01-20 14:56:28 +05:30
ankitsridhar16
a0006f73ac chore: added message types to PubSub Service 2023-01-20 14:55:26 +05:30
Balu Babu
f79070fe60 test: wrote tests for user service file 2023-01-20 11:56:33 +05:30
Balu Babu
b238f3d060 fix: fixed magic-link account creation bug 2023-01-20 07:59:52 +05:30
Balu Babu
a6ad86bd59 chore: replaced hardcoded values with env variables in app.module.ts, main.ts and utils.ts 2023-01-20 07:56:19 +05:30
Balu Babu
60e2ef7cda Merge pull request #3 from hoppscotch/feat/user-environments
feat: introduce user environments in self hosted
2023-01-20 05:01:41 +05:30
Balu Babu
cde0ba11fa fix: changed the return type of delete many subscription to number from user-environment type 2023-01-20 04:55:35 +05:30
Balu Babu
da9fcd1087 refactor: changed the return type of deleteUserEnvironments method to number from void 2023-01-20 04:36:30 +05:30
Balu Babu
8929b37dbe fix: changed return type of deleteUserEnvironment mutation to boolean in user-environments resolver 2023-01-20 04:26:05 +05:30
Balu Babu
509604833e chore: cleaned unwanted imports in auth module 2023-01-20 00:04:01 +05:30
Mir Arif Hasan
08ac9680d7 fix: keeping timestamp without timezone 2023-01-19 17:11:32 +06:00
ankitsridhar16
ca5404a93b chore: updated ts config with review changes 2023-01-19 16:25:36 +05:30
ankitsridhar16
f6f4547af3 chore: introduced string to json from user-settings and moved types 2023-01-19 16:24:31 +05:30
ankitsridhar16
669f8b0431 chore: made review changes for resolvers and introduced stringToJson from user-settings 2023-01-19 16:22:49 +05:30
ankitsridhar16
86aa0251ab chore: made review changes 2023-01-19 16:21:05 +05:30
ankitsridhar16
2252048d2e chore: updated module type name 2023-01-19 16:20:12 +05:30
Balu Babu
53571a7d72 refactor: logout route now just returning 200 status code not redirecting to app_domain 2023-01-19 15:13:55 +05:30
ankitsridhar16
0a469f4ccf chore: introduced subscription handler and fixed requested review changes 2023-01-19 12:57:59 +05:30
ankitsridhar16
d10ed664bf chore: updated test cases with requested changes to handle publishing seperately 2023-01-19 12:56:41 +05:30
ankitsridhar16
4aad8d36a9 chore: updated comment for Subscription Type and updated JSDoc for publish 2023-01-19 12:53:40 +05:30
ankitsridhar16
3e9295f313 chore: added review changes for updating mutations and naming for descriptions 2023-01-19 12:50:20 +05:30
ankitsridhar16
6aa66e99b5 chore: added review changes for description 2023-01-19 12:48:25 +05:30
Balu Babu
c38ad89cd7 chore: made the required changes in auth and user modules to accommodate changes made in user schema 2023-01-19 05:53:23 +05:30
Balu Babu
8fdcc5dd50 chore: changed the names of properties for User in prisma and user.model 2023-01-19 05:19:47 +05:30
Balu Babu
364381f017 chore: changed the schema of database to only store timestamp without timezone info 2023-01-19 05:13:21 +05:30
Balu Babu
9433aa503b chore: removed duplicate me query in user module 2023-01-19 04:57:43 +05:30
Balu Babu
82dee95cd0 chore: added types to createProviderAccount ,ethod in auth.service file 2023-01-19 04:55:51 +05:30
Balu Babu
813db4a985 feat: added route to log users out 2023-01-19 04:34:35 +05:30
Balu Babu
c63bc28ca0 feat: microsoft SSO auth completed 2023-01-19 04:15:43 +05:30
ankitsridhar16
29e74a2c9e feat: added subscription handler as a provider for user environment module 2023-01-19 01:36:31 +05:30
ankitsridhar16
80fdc6005b chore: added comments to certain fields 2023-01-19 01:35:21 +05:30
ankitsridhar16
9e25aa1f9f feat: introducing new subscription handler interface and user defined/primitive types 2023-01-19 01:34:11 +05:30
ankitsridhar16
f58d5d28cf chore: added error messages and updated existing error messages 2023-01-19 01:31:38 +05:30
Balu Babu
81cb0d43d7 fix: added scope for github strategy in auth module 2023-01-18 22:34:15 +05:30
Andrew Bastin
9d7052c626 chore: add CODEOWNERS 2023-01-13 21:53:37 -05:00
Balu Babu
4edd0e0ab7 chore: added new github auth environment variables into .env.example 2023-01-13 02:50:52 +05:30
Balu Babu
a3d60d393b chore: removed rouge file called typescript from repo 2023-01-13 02:48:16 +05:30
Balu Babu
311ab67ebe feat: github login added 2023-01-13 02:11:42 +05:30
Balu Babu
1f581e7b51 chore: added logic to create and add google accoun to pre-existing user accounts 2023-01-13 01:48:25 +05:30
Balu Babu
5fe934110e fix: fixed the timestamp comparison login in verifyPasswordlessTokens route 2023-01-13 01:46:34 +05:30
Balu Babu
f4df8873be feat: google sso auth added 2023-01-13 00:52:29 +05:30
Balu Babu
6f4c5d7195 chore: created .env.example file to store env variables 2023-01-12 23:19:38 +05:30
Balu Babu
06f1c2fba2 refactor: refactored a few types around users and passwordless tokens in auth module 2023-01-12 21:31:25 +05:30
Balu Babu
36b32a1813 feat: /refresh route complete along with refresh token rotation 2023-01-11 19:29:33 +05:30
Balu Babu
d3a43cb65f chore: created and added new errors in jwt.strategy.ts file 2023-01-11 18:41:06 +05:30
Balu Babu
d98e7b9416 refactor: created utlility functions for setting cookies and handling redirects 2023-01-10 17:00:39 +05:30
Balu Babu
fc284fd0a2 feat: magic-link auth complete 2023-01-10 16:06:42 +05:30
Masaki Tagawa
5841d2eb66 chore(i18n): update i18n translations 2023-01-09 20:53:12 +05:30
Balu Babu
0c154be04e chore: manually committing auth module to remoter 2023-01-09 19:02:14 +05:30
Balu Babu
90bc0483ae fix: fixed improper imports in auth module 2023-01-09 18:56:40 +05:30
Balu Babu
32765b2d34 chore: created the route to initate magic link auth 2023-01-09 17:43:45 +05:30
Balu Babu
d9e80ebef9 feat: modified the prisma.schema file to add new tables for auth 2023-01-09 12:15:21 +05:30
Balu Babu
445102226e chore: created auth module and installed relevant base dependencies 2023-01-09 12:10:06 +05:30
Balu Babu
a6ce882511 feat: created the mailer module with postmark 2023-01-09 12:00:03 +05:30
Mir Arif Hasan
9d20c4c4a9 chore: updated variable names and comments 2023-01-05 15:52:43 +06:00
Mir Arif Hasan
e2d8ea0a70 refactor: error message updated 2023-01-05 15:38:18 +06:00
Mir Arif Hasan
b33d003ba5 feat: error message updated 2023-01-05 15:21:09 +06:00
tzhangm
ee07a90b5e chore: update i18n translations 2023-01-05 13:27:17 +05:30
Mir Arif Hasan
55f79507fe chore: updated seed file 2023-01-05 13:22:19 +06:00
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
Mir Arif Hasan
a372cf0178 chore: addd seed for user-settings 2022-12-23 21:50:39 +06:00
Mir Arif Hasan
d863aa7aa6 chore: postinstall update in package.json 2022-12-23 13:14:57 +06:00
Mir Arif Hasan
b31e54b3e5 feat: user-settings schema update and relative service file modified 2022-12-23 13:13:21 +06:00
Mir Arif Hasan
9b5734f2ff refactor: user-settings module 2022-12-23 12:28:22 +06:00
Mir Arif Hasan
6b59b9988c docs: developer guide text updated 2022-12-22 22:25:16 +06:00
Mir Arif Hasan
f9de546d14 feat: more property added in userInputDto and key updated 2022-12-22 18:56:18 +06:00
Mir Arif Hasan
9e304b947b test: added user update unit tests 2022-12-22 18:47:42 +06:00
Mir Arif Hasan
c11a219c62 fix: typo of pubsub message 2022-12-22 18:41:35 +06:00
Mir Arif Hasan
3cc22575cb feat: user update and subscribers added 2022-12-22 17:35:44 +06:00
Mir Arif Hasan
c42b6e2fdb feat: added fields for user-session of rest and gql 2022-12-22 12:46:03 +06:00
Andrew Bastin
1e5dd1cc53 chore: introduce platform object for platform specific code 2022-12-21 19:21:52 -05:00
Mir Arif Hasan
877532559e Merge branch 'main' into feat/user-settings 2022-12-21 13:25:42 +06:00
ankitsridhar16
cd4750fcce fix: added missing return for star/unstar service method 2022-12-21 11:45:19 +05:30
ankitsridhar16
fc2be71e1f Merge remote-tracking branch 'origin/main' into feat/user-environments
# Conflicts:
#	packages/hoppscotch-backend/jest.setup.js
#	packages/hoppscotch-backend/package.json
2022-12-21 10:30:21 +05:30
ankitsridhar16
0c9aa2f681 chore: added error messages for a user history 2022-12-20 21:08:23 +05:30
ankitsridhar16
1883be95d5 chore: updated resolvers for user-history 2022-12-20 21:06:49 +05:30
ankitsridhar16
f7dadda52a chore: added service files for user history and unit tests 2022-12-20 21:05:58 +05:30
Mir Arif Hasan
2a8fd24504 chore: removed redundent import statement 2022-12-20 16:36:45 +06:00
ankitsridhar16
71c70a1b36 Merge remote-tracking branch 'origin/main' into feat/user-history 2022-12-20 15:52:54 +05:30
Mir Arif Hasan
c34379d936 Merge pull request #6 from hoppscotch/chore/jest-testing
feat: add moduleNameMapper in package.json
2022-12-20 16:21:34 +06:00
Mir Arif Hasan
2dde29c628 feat: add moduleNameMapper in package.json 2022-12-20 16:09:54 +06:00
ankitsridhar16
818e71d49c Merge remote-tracking branch 'origin/main' into feat/user-history 2022-12-20 14:58:15 +05:30
Ankit Sridhar
6bbeb5ef87 Merge pull request #4 from hoppscotch/chore/jest-setup
feat: added jest and jest-fp-ts setup
2022-12-20 14:56:20 +05:30
ankitsridhar16
7ebed70316 fix: fixes nest restart issue 2022-12-20 14:52:54 +05:30
ankitsridhar16
130237fc87 feat: added jest and jest-fp-ts setup 2022-12-20 14:41:50 +05:30
ankitsridhar16
a28774c2c4 feat: added user-history resolvers, service files and module 2022-12-20 14:35:25 +05:30
ankitsridhar16
b9ade5d2a3 feat: added user-history module to app module 2022-12-20 14:31:01 +05:30
ankitsridhar16
b677aa1715 feat: added user history model 2022-12-20 14:30:14 +05:30
Mir Arif Hasan
7a036883e8 test: added user-settings test cases for service file 2022-12-20 15:00:13 +06:00
ankitsridhar16
e665df21da feat: added resolvers for user model and history 2022-12-20 14:29:31 +05:30
ankitsridhar16
d066b9c913 feat: added prisma schema for user history 2022-12-20 14:28:15 +05:30
Mir Arif Hasan
b4290c24b3 fix: invalid user handled on createUserSettings 2022-12-20 14:51:58 +06:00
Mir Arif Hasan
b66656ad84 fix: prisma service import 2022-12-19 23:58:23 +06:00
Mir Arif Hasan
5c032e84be fix: null value checked on user_settings.properties 2022-12-19 23:56:50 +06:00
Mir Arif Hasan
83437ae4ba feat: added fetchUserSettings for 2022-12-19 18:42:13 +06:00
Mir Arif Hasan
24434cc61a feat: added subscriber for update user settings 2022-12-19 18:18:34 +06:00
Mir Arif Hasan
53dc40e8c7 feat: added mutation for update user settings 2022-12-19 18:12:49 +06:00
Mir Arif Hasan
4affb2bc5b feat: added user-settings schema and user-settings module 2022-12-19 17:38:46 +06: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
ankitsridhar16
c87690f378 chore: update resolvers and service files 2022-12-15 23:31:12 +05:30
ankitsridhar16
9fb9fd4568 chore: added test files for user environment service 2022-12-15 23:29:51 +05:30
ankitsridhar16
6bd4fd91ff chore: updated user resolver with updated service methods 2022-12-15 23:25:21 +05:30
ankitsridhar16
164c2463f5 chore: added error messages for user environment related errors 2022-12-15 23:24:09 +05:30
ankitsridhar16
73532e41c5 chore: added jest and jest related setup files to support jest fp-ts 2022-12-15 23:23:13 +05:30
ankitsridhar16
3392b1a1ca chore: minor changes to Dockerfilefor prisma service 2022-12-15 23:21:57 +05:30
ankitsridhar16
08cc7114ac chore: minor changes to Dockerfile 2022-12-15 23:21:21 +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
ankitsridhar16
8e038f6944 feat: added user environments to app module 2022-12-13 16:10:58 +05:30
ankitsridhar16
ce94255a9e feat: added user environment user environments resolvers, service files 2022-12-13 13:27:51 +05:30
ankitsridhar16
b4b63f86d9 feat: added user environment prisma schema 2022-12-13 13:14:13 +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
Ankit Sridhar
2901fb0d72 Merge pull request #2 from hoppscotch/chore/backend-integration
chore: backend integration for existing modules and docker fix
2022-12-09 11:59:49 +05:30
Balu Babu
c5466edf71 chore: cleaned up hopp-backend package and modified docker and docker-compose files 2022-12-08 22:19:14 +05:30
ankitsridhar16
bfc5bfe973 chore: added user model in prisma schema 2022-12-08 20:26:22 +05:30
ankitsridhar16
4da11955f1 style: prettier fix for pubsub 2022-12-07 23:17:56 +05:30
ankitsridhar16
d4c775a537 chore: added existing utils from old backend repo 2022-12-07 23:14:49 +05:30
ankitsridhar16
ef95a8a305 chore: added fp-ts and gql complexity plugin 2022-12-07 23:13:13 +05:30
ankitsridhar16
a173d2c808 chore: imported existing user module files from old repo 2022-12-07 23:11:52 +05:30
ankitsridhar16
ee002df110 chore: added gql decorator and complexity plugin 2022-12-07 23:09:15 +05:30
ankitsridhar16
06ef17048a chore: added prisma module 2022-12-07 20:52:31 +05:30
Balu Babu
9487348ba8 Merge branch 'chore/backend-integration' of https://github.com/hoppscotch/self-hosted into chore/backend-integration 2022-12-07 20:33:08 +05:30
Balu Babu
757060b11f chore: removed comments from pubsub module 2022-12-07 20:31:54 +05:30
ankitsridhar16
ab1f8437ea chore: added existing auth guard and user model 2022-12-07 20:30:02 +05:30
Balu Babu
d7afd31572 chore: created pubsub module and added relevant dependencies for it 2022-12-07 20:28:46 +05:30
Balu Babu
cd3178224a Merge branch 'chore/backend-integration' of https://github.com/hoppscotch/self-hosted into chore/backend-integration 2022-12-07 20:19:40 +05:30
Balu Babu
9193a1a5d6 chore: fixed docker-compose issue with package/hopp-backend 2022-12-07 20:13:42 +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
Andrew Bastin
0f3e36a447 chore: get functioning server running 2022-12-06 15:00:51 -05:00
ankitsridhar16
333dbba393 chore: added docker files for bringing the container up 2022-12-06 13:18:02 +05:30
Liyas Thomas
b04b12c7a0 fix: broken links 2022-12-06 12:09:20 +05:30
Ankit Sridhar
1dc804a2b9 Merge pull request #1 from hoppscotch/feat/create-hoppscotch-backend-package
feat: added hoppscotch-backend as a package
2022-12-05 12:50:50 +05:30
ankitsridhar16
75219d457a feat: added hoppscotch-backend as a package 2022-12-05 12:36:11 +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
1014 changed files with 73459 additions and 19182 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

@@ -1,104 +0,0 @@
Dockerfile
.vscode
.github
# Created by .ignore support plugin (hsz.mobi)
# Firebase
.firebase
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# Mac OSX
.DS_Store
# Vim swap files
*.swp
# Build data
.hoppscotch
# File explorer
.directory

59
.env.example Normal file
View File

@@ -0,0 +1,59 @@
#-----------------------Backend Config------------------------------#
# Prisma Config
DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch
# Auth Tokens Config
JWT_SECRET="secret1233"
TOKEN_SALT_COMPLEXITY=10
MAGIC_LINK_TOKEN_VALIDITY= 3
REFRESH_TOKEN_VALIDITY="604800000" # Default validity is 7 days (604800000 ms) in ms
ACCESS_TOKEN_VALIDITY="86400000" # Default validity is 1 day (86400000 ms) in ms
SESSION_SECRET='add some secret here'
# Hoppscotch App Domain Config
REDIRECT_URL="http://localhost:3000"
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
# Google Auth Config
GOOGLE_CLIENT_ID="************************************************"
GOOGLE_CLIENT_SECRET="************************************************"
GOOGLE_CALLBACK_URL="http://localhost:3170/v1/auth/google/callback"
GOOGLE_SCOPE="email,profile"
# Github Auth Config
GITHUB_CLIENT_ID="************************************************"
GITHUB_CLIENT_SECRET="************************************************"
GITHUB_CALLBACK_URL="http://localhost:3170/v1/auth/github/callback"
GITHUB_SCOPE="user:email"
# Microsoft Auth Config
MICROSOFT_CLIENT_ID="************************************************"
MICROSOFT_CLIENT_SECRET="************************************************"
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
MICROSOFT_SCOPE="user.read"
# Mailer config
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
MAILER_ADDRESS_FROM='"From Name Here" <from@example.com>'
# Rate Limit Config
RATE_LIMIT_TTL=60 # In seconds
RATE_LIMIT_MAX=100 # Max requests per IP
#-----------------------Frontend Config------------------------------#
# Base URLs
VITE_BASE_URL=http://localhost:3000
VITE_SHORTCODE_BASE_URL=http://localhost:3000
VITE_ADMIN_URL=http://localhost:3100
# Backend URLs
VITE_BACKEND_GQL_URL=http://localhost:3170/graphql
VITE_BACKEND_WS_URL=wss://localhost:3170/graphql
VITE_BACKEND_API_URL=http://localhost:3170/v1
# Terms Of Service And Privacy Policy Links (Optional)
VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy

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,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

@@ -1,18 +0,0 @@
name: Deploy to Live Channel
on:
push:
branches:
- main
jobs:
deploy_live_website:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_POSTWOMAN_API }}'
channelId: live
projectId: postwoman-api

View File

@@ -1,45 +0,0 @@
name: Deploy to Staging Netlify
on:
push:
# TODO: Migrate to staging branch only
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: Build Site
env:
VITE_GA_ID: ${{ secrets.STAGING_GA_ID }}
VITE_GTM_ID: ${{ secrets.STAGING_GTM_ID }}
VITE_API_KEY: ${{ secrets.STAGING_FB_API_KEY }}
VITE_AUTH_DOMAIN: ${{ secrets.STAGING_FB_AUTH_DOMAIN }}
VITE_DATABASE_URL: ${{ secrets.STAGING_FB_DATABASE_URL }}
VITE_PROJECT_ID: ${{ secrets.STAGING_FB_PROJECT_ID }}
VITE_STORAGE_BUCKET: ${{ secrets.STAGING_FB_STORAGE_BUCKET }}
VITE_MESSAGING_SENDER_ID: ${{ secrets.STAGING_FB_MESSAGING_SENDER_ID }}
VITE_APP_ID: ${{ secrets.STAGING_FB_APP_ID }}
VITE_BASE_URL: ${{ secrets.STAGING_BASE_URL }}
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_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
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_STAGING_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -1,39 +0,0 @@
name: Publish Docker image
on:
push:
branches: [main]
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: hoppscotch/hoppscotch
flavor: |
latest=true
prefix=
suffix=
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: true
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
version: 8
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

85
.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,9 @@ tests/*/videos
# Local Netlify folder
.netlify
# PNPM
.pnpm-store
# GQL SDL generated for the frontends
gql-gen/

View File

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

30
CODEOWNERS Normal file
View File

@@ -0,0 +1,30 @@
# 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
/packages/hoppscotch-selfhost-web/ @amk-dev
/packages/hoppscotch-sh-admin/ @JoelJacobStephen
/packages/hoppscotch-backend/ @ankitsridhar16 @balub
# 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

@@ -1,29 +0,0 @@
FROM node:lts-alpine
LABEL maintainer="Hoppscotch (support@hoppscotch.io)"
# Add git as the prebuild target requires it to parse version information
RUN apk add --no-cache --virtual .gyp \
python3 \
make \
g++
# Create app directory
WORKDIR /app
ADD . /app/
COPY . .
RUN npm install -g pnpm
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%"
/>
@@ -161,7 +161,7 @@ _Collections are synced with cloud / local session storage_
- Access APIs served in non-HTTPS (`http://`) endpoints
- Use your Proxy URL
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/privacy)**_
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/support/privacy)**_
📜 **Pre-Request Scripts β:** Snippets of code associated with a request that is executed before the request is sent.
@@ -178,7 +178,7 @@ _Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/h
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/features/shortcuts)**
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/documentation/features/shortcuts)**
🌎 **i18n:** Experience the app in your language.
@@ -275,49 +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`.
_Sample keys only work with the [production build](https://hoppscotch.io)._
### Browser-based development environment
- [GitHub codespace](https://docs.github.com/en/codespaces/developing-in-codespaces/creating-a-codespace)
- [Gitpod](https://gitpod.io/#https://github.com/hoppscotch/hoppscotch)
### Local development environment
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. Start the development server with `pnpm run dev`.
5. Open the development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
### Docker compose
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
2. Run `docker-compose up` within the directory that you cloned (probably `hoppscotch`).
3. Open the development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
## **Docker**
**Official container** &nbsp; [![hoppscotch/hoppscotch](https://img.shields.io/docker/pulls/hoppscotch/hoppscotch?style=social)](https://hub.docker.com/r/hoppscotch/hoppscotch)
```bash
docker run --rm --name hoppscotch -p 3000:3000 hoppscotch/hoppscotch:latest
```
## **Releasing**
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
2. Install 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`.
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).
Follow our [self-hosting guide](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
## **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

@@ -1,23 +1,71 @@
# To make it easier to self-host, we have a preset docker compose config that also
# has a container with a Postgres instance running.
# You can tweak around this file to match your instances
version: "3.7"
services:
web:
# This service runs the backend app in the port 3170
hoppscotch-backend:
container_name: hoppscotch-backend
build:
dockerfile: packages/hoppscotch-backend/Dockerfile
context: .
volumes:
- "./.hoppscotch:/app/.hoppscotch"
- "./assets:/app/assets"
- "./directives:/app/directives"
- "./layouts:/app/layouts"
- "./middleware:/app/middleware"
- "./pages:/app/pages"
- "./plugins:/app/plugins"
- "./static:/app/static"
- "./store:/app/store"
- "./components:/app/components"
- "./helpers:/app/helpers"
ports:
- "3000:3000"
target: prod
env_file:
- ./.env
restart: always
environment:
HOST: 0.0.0.0
command: "npm run dev"
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
- PORT=3000
volumes:
- ./packages/hoppscotch-backend/:/usr/src/app
- /usr/src/app/node_modules/
depends_on:
- hoppscotch-db
ports:
- "3170:3000"
# The main hoppscotch app. This will be hosted at port 3000
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
# the SH admin dashboard server at packages/hoppscotch-selfhost-web/Caddyfile
hoppscotch-app:
container_name: hoppscotch-app
build:
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
context: .
env_file:
- ./.env
depends_on:
- hoppscotch-backend
ports:
- "3000:8080"
# The Self Host dashboard for managing the app. This will be hosted at port 3100
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
# the SH admin dashboard server at packages/hoppscotch-sh-admin/Caddyfile
hoppscotch-sh-admin:
container_name: hoppscotch-sh-admin
build:
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
context: .
env_file:
- ./.env
depends_on:
- hoppscotch-backend
ports:
- "3100:8080"
# The preset DB service, you can delete/comment the below lines if
# you are using an external postgres instance
# This will be exposed at port 5432
hoppscotch-db:
image: postgres
ports:
- "5432:5432"
environment:
# NOTE: Please UPDATE THIS PASSWORD!
POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch

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,
@@ -9,13 +9,15 @@
"preinstall": "npx only-allow pnpm",
"prepare": "husky install",
"dev": "pnpm -r do-dev",
"gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl",
"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 +29,8 @@
"devDependencies": {
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@types/node": "^17.0.24"
"@types/node": "^17.0.24",
"cross-env": "^7.0.3",
"http-server": "^14.1.1"
}
}

View File

@@ -1,26 +0,0 @@
# Google Analytics ID
VITE_GA_ID=UA-61422507-4
# Google Tag Manager ID
VITE_GTM_ID=GTM-NMKVBMV
# Firebase config
VITE_API_KEY=AIzaSyCMsFreESs58-hRxTtiqQrIcimh4i1wbsM
VITE_AUTH_DOMAIN=postwoman-api.firebaseapp.com
VITE_DATABASE_URL=https://postwoman-api.firebaseio.com
VITE_PROJECT_ID=postwoman-api
VITE_STORAGE_BUCKET=postwoman-api.appspot.com
VITE_MESSAGING_SENDER_ID=421993993223
VITE_APP_ID=1:421993993223:web:ec0baa8ee8c02ffa1fc6a2
VITE_MEASUREMENT_ID=G-BBJ3R80PJT
# Base URL
VITE_BASE_URL=https://hoppscotch.io
# Backend URLs
VITE_BACKEND_GQL_URL=https://api.hoppscotch.io/graphql
VITE_BACKEND_WS_URL=wss://api.hoppscotch.io/graphql
# Sentry (Optional)
# VITE_SENTRY_DSN: <Sentry DSN here>
# VITE_SENTRY_ENVIRONMENT: <Sentry environment value here>

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,667 +0,0 @@
{
"action": {
"autoscroll": "Autoscroll",
"cancel": "Anulare",
"choose_file": "Alegeți un fișier",
"clear": "clar",
"clear_all": "Curata tot",
"close": "Close",
"connect": "Conectați",
"copy": "Copie",
"delete": "Șterge",
"disconnect": "Deconectat",
"dismiss": "Renunță",
"dont_save": "Don't save",
"download_file": "Descărcare fișier",
"drag_to_reorder": "Drag to reorder",
"duplicate": "Duplicate",
"edit": "Editați | ×",
"filter_response": "Filter response",
"go_back": "Întoarce-te",
"label": "Eticheta",
"learn_more": "Află mai multe",
"less": "Less",
"more": "Mai mult",
"new": "Nou",
"no": "Nu",
"open_workspace": "Open workspace",
"paste": "Paste",
"prettify": "Dăruiește",
"remove": "Elimina",
"restore": "Restabili",
"save": "salva",
"scroll_to_bottom": "Scroll to bottom",
"scroll_to_top": "Scroll to top",
"search": "Căutare",
"send": "Trimite",
"start": "start",
"stop": "Stop",
"to_close": "to close",
"to_navigate": "to navigate",
"to_select": "to select",
"turn_off": "Opriți",
"turn_on": "Aprinde",
"undo": "Anula",
"yes": "da"
},
"add": {
"new": "Adăuga nou",
"star": "Adăugați stea"
},
"app": {
"chat_with_us": "Vorbeste cu noi",
"contact_us": "Contactează-ne",
"copy": "Copie",
"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": "Documentație",
"github": "GitHub",
"help": "Ajutor, feedback și documentație",
"home": "Acasă",
"invite": "A invita",
"invite_description": "În Hoppscotch, am proiectat o interfață simplă și intuitivă pentru crearea și gestionarea API-urilor dvs. Hoppscotch este un instrument care vă ajută să construiți, să testați, să documentați și să partajați API-urile dvs.",
"invite_your_friends": "Invita-ti prietenii",
"join_discord_community": "Alătură-te comunității noastre Discord",
"keyboard_shortcuts": "Comenzi rapide de la tastatură",
"name": "Hoppscotch",
"new_version_found": "Nouă versiune găsită. Reîmprospătați pentru a actualiza.",
"options": "Options",
"proxy_privacy_policy": "Politica de confidențialitate proxy",
"reload": "Reîncarcă",
"search": "Căutare",
"share": "Acțiune",
"shortcuts": "Comenzi rapide",
"spotlight": "În centrul atenției",
"status": "stare",
"status_description": "Check the status of the website",
"terms_and_privacy": "Termeni și confidențialitate",
"twitter": "Stare de nervozitate",
"type_a_command_search": "Tastați o comandă sau căutați ...",
"we_use_cookies": "Folosim cookie-uri",
"whats_new": "Ce mai e nou?",
"wiki": "Wiki"
},
"auth": {
"account_exists": "Contul există cu acreditări diferite - Conectați-vă pentru a conecta ambele conturi",
"all_sign_in_options": "Toate opțiunile de conectare",
"continue_with_email": "Continuați cu e-mailul",
"continue_with_github": "Continuați cu GitHub",
"continue_with_google": "Continuați cu Google",
"continue_with_microsoft": "Continue with Microsoft",
"email": "E-mail",
"logged_out": "Delogat",
"login": "Autentificare",
"login_success": "Conectat cu succes",
"login_to_hoppscotch": "Conectați-vă la Hoppscotch",
"logout": "Deconectați-vă",
"re_enter_email": "Reintroduceti emailul",
"send_magic_link": "Trimiteți un link magic",
"sync": "Sincronizare",
"we_sent_magic_link": "V-am trimis un link magic!",
"we_sent_magic_link_description": "Verificați căsuța de e-mail - am trimis un e-mail la {email}. Conține un link magic care vă va conecta."
},
"authorization": {
"generate_token": "Generați token",
"include_in_url": "Includeți în URL",
"learn": "Afla cum",
"pass_key_by": "Pass by",
"password": "Parola",
"token": "Jeton",
"type": "Tipul de autorizare",
"username": "Nume de utilizator"
},
"collection": {
"created": "Colecție creată",
"edit": "Editați colecția",
"invalid_name": "Vă rugăm să furnizați un nume valid pentru colecție",
"my_collections": "Colecțiile mele",
"name": "Noua mea colecție",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Colecție nouă",
"renamed": "Colecția redenumită",
"request_in_use": "Request in use",
"save_as": "Salvează ca",
"select": "Selectați o colecție",
"select_location": "Selectați locația",
"select_team": "Selectați o echipă",
"team_collections": "Colecții de echipă"
},
"confirm": {
"exit_team": "Are you sure you want to leave this team?",
"logout": "Sigur doriți să vă deconectați?",
"remove_collection": "Sigur doriți să ștergeți definitiv această colecție?",
"remove_environment": "Sigur doriți să ștergeți definitiv acest mediu?",
"remove_folder": "Sigur doriți să ștergeți definitiv acest folder?",
"remove_history": "Sigur doriți să ștergeți definitiv tot istoricul?",
"remove_request": "Sigur doriți să ștergeți definitiv această solicitare?",
"remove_team": "Sigur doriți să ștergeți această echipă?",
"remove_telemetry": "Sigur doriți să renunțați la telemetrie?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"sync": "Sigur doriți să sincronizați acest spațiu de lucru?"
},
"count": {
"header": "Antet {count}",
"message": "Număr de mesaje",
"parameter": "Parametrul {count}",
"protocol": "Protocol {count}",
"value": "Valoare {count}",
"variable": "Variabila {count}"
},
"documentation": {
"generate": "Generați documentație",
"generate_message": "Importați orice colecție Hoppscotch pentru a genera documentație API în mișcare."
},
"empty": {
"authorization": "Această cerere nu folosește nicio autorizație",
"body": "Această cerere nu are un corp",
"collection": "Colecția este goală",
"collections": "Colecțiile sunt goale",
"documentation": "Connect to a GraphQL endpoint to view documentation",
"endpoint": "Endpoint cannot be empty",
"environments": "Mediile sunt goale",
"folder": "Dosarul este gol",
"headers": "Această solicitare nu are anteturi",
"history": "Istoria este goală",
"invites": "Invite list is empty",
"members": "Echipa este goală",
"parameters": "Această solicitare nu are niciun parametru",
"pending_invites": "There are no pending invites for this team",
"profile": "Login in to view your profile",
"protocols": "Protocoalele sunt goale",
"schema": "Conectați-vă la un punct final GraphQL",
"shortcodes": "Shortcodes are empty",
"team_name": "Numele echipei este gol",
"teams": "Echipele sunt goale",
"tests": "Nu există teste pentru această solicitare"
},
"environment": {
"add_to_global": "Add to Global",
"added": "Environment addition",
"create_new": "Creați un mediu nou",
"created": "Environment created",
"deleted": "Environment deletion",
"edit": "Editați mediul",
"invalid_name": "Vă rugăm să furnizați un nume valid pentru mediu",
"nested_overflow": "nested environment variables are limited to 10 levels",
"new": "Noul mediu",
"no_environment": "Fără mediu",
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
"select": "Selectați mediul",
"title": "Medii",
"updated": "Environment updation",
"variable_list": "Lista variabilelor"
},
"error": {
"browser_support_sse": "Acest browser pare să nu aibă suport pentru Server Sent Events.",
"check_console_details": "Verificați jurnalul consolei pentru detalii.",
"curl_invalid_format": "cURL nu este formatat corect",
"empty_req_name": "Nume cerere goală",
"f12_details": "(F12 pentru detalii)",
"gql_prettify_invalid_query": "Nu am putut preta o interogare nevalidă, rezolvarea erorilor de sintaxă a interogării și încercați din nou",
"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": "Nu s-a putut pregăti un corp nevalid, a rezolva erorile de sintaxă json și a încerca din nou",
"network_error": "There seems to be a network error. Please try again.",
"network_fail": "Nu s-a putut trimite solicitarea",
"no_duration": "Fără durată",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"script_fail": "Nu s-a putut executa scriptul de pre-cerere",
"something_went_wrong": "Ceva n-a mers bine",
"test_script_fail": "Could not execute post-request script"
},
"export": {
"as_json": "Exportați ca JSON",
"create_secret_gist": "Creați secțiunea secretă",
"gist_created": "Gist creat",
"require_github": "Conectați-vă cu GitHub pentru a crea o idee secretă",
"title": "Export"
},
"folder": {
"created": "Folder creat",
"edit": "Editați folderul",
"invalid_name": "Vă rugăm să furnizați un nume pentru dosar",
"name_length_insufficient": "Folder name should be at least 3 characters long",
"new": "Dosar nou",
"renamed": "Dosar redenumit"
},
"graphql": {
"mutations": "Mutații",
"schema": "Schemă",
"subscriptions": "Abonamente"
},
"header": {
"install_pwa": "Instalează aplicația",
"login": "Autentificare",
"save_workspace": "Salvați spațiul meu de lucru"
},
"helpers": {
"authorization": "Antetul autorizației va fi generat automat la trimiterea cererii.",
"generate_documentation_first": "Generați mai întâi documentația",
"network_fail": "Imposibil de atins punctul final API. Verificați conexiunea la rețea și încercați din nou.",
"offline": "Pari să fii offline. Este posibil ca datele din acest spațiu de lucru să nu fie actualizate.",
"offline_short": "Pari să fii offline.",
"post_request_tests": "Scripturile de testare sunt scrise în JavaScript și se execută după primirea răspunsului.",
"pre_request_script": "Scripturile de cerere prealabilă sunt scrise în JavaScript și sunt rulate înainte de trimiterea cererii.",
"script_fail": "Se pare că există o eroare în scriptul de pre-cerere. Verificați eroarea de mai jos și remediați scriptul în consecință.",
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
"tests": "Scrieți un script de test pentru automatizarea depanării."
},
"hide": {
"collection": "Collapse Collection Panel",
"more": "Ascunde mai mult",
"preview": "Ascundeți previzualizarea",
"sidebar": "Ascunde bara laterală"
},
"import": {
"collections": "Import colecții",
"curl": "Importați cURL",
"failed": "Importul nu a reușit",
"from_gist": "Import din 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 din colecțiile mele",
"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": "Introduceți adresa 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": "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": "Modul Zen"
},
"modal": {
"collections": "Colecții",
"confirm": "A confirma",
"edit_request": "Solicitare de editare",
"import_export": "Import Export"
},
"mqtt": {
"communication": "Comunicare",
"log": "Buturuga",
"message": "Mesaj",
"publish": "Publica",
"subscribe": "Abonati-va",
"topic": "Subiect",
"topic_name": "Nume subiect",
"topic_title": "Publică / Abonează subiect",
"unsubscribe": "Dezabonează-te",
"url": "URL"
},
"navigation": {
"doc": "Documente",
"graphql": "GraphQL",
"profile": "Profile",
"realtime": "Timp real",
"rest": "REST",
"settings": "Setări"
},
"preRequest": {
"javascript_code": "Cod JavaScript",
"learn": "Citiți documentația",
"script": "Script de cerere prealabilă",
"snippets": "Fragmente"
},
"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ți steaua"
},
"request": {
"added": "Cerere adăugată",
"authorization": "Autorizare",
"body": "Solicitați corpul",
"choose_language": "Alege limba",
"content_type": "Tipul de conținut",
"content_type_titles": {
"others": "Others",
"structured": "Structured",
"text": "Text"
},
"copy_link": "Copiază legătură",
"duration": "Durată",
"enter_curl": "Introduceți cURL",
"generate_code": "Generați cod",
"generated_code": "Cod generat",
"header_list": "Lista anteturilor",
"invalid_name": "Vă rugăm să furnizați un nume pentru cerere",
"method": "Metodă",
"name": "Solicitați numele",
"new": "New Request",
"override": "Override",
"override_help": "Set <xmp>Content-Type</xmp> in Headers",
"overriden": "Overridden",
"parameter_list": "Parametrii interogării",
"parameters": "Parametrii",
"path": "cale",
"payload": "Încărcătură utilă",
"query": "Interogare",
"raw_body": "Corp de solicitare brută",
"renamed": "Cererea redenumită",
"run": "Alerga",
"save": "salva",
"save_as": "Salvează ca",
"saved": "Cererea a fost salvată",
"share": "Acțiune",
"share_description": "Share Hoppscotch with your friends",
"title": "Cerere",
"type": "Tip de solicitare",
"url": "URL",
"variables": "Variabile",
"view_my_links": "View my links"
},
"response": {
"body": "Corpul de răspuns",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Anteturi",
"html": "HTML",
"image": "Imagine",
"json": "JSON",
"pdf": "PDF",
"preview_html": "Previzualizați HTML",
"raw": "Brut",
"size": "mărimea",
"status": "stare",
"time": "Timp",
"title": "Raspuns",
"waiting_for_connection": "așteptând conexiunea",
"xml": "XML"
},
"settings": {
"accent_color": "Culoare de accent",
"account": "Cont",
"account_description": "Personalizați setările contului.",
"account_email_description": "Adresa dvs. de e-mail principală.",
"account_name_description": "Acesta este numele dvs. afișat.",
"background": "fundal",
"black_mode": "Negru",
"change_font_size": "Schimba marimea fontului",
"choose_language": "Alege limba",
"dark_mode": "Întuneric",
"expand_navigation": "Expand navigation",
"experiments": "Experimente",
"experiments_notice": "Aceasta este o colecție de experimente la care lucrăm, care s-ar putea dovedi a fi utile, distractive, ambele sau nici una. Nu sunt definitive și s-ar putea să nu fie stabile, așa că, dacă se întâmplă ceva prea ciudat, nu vă panicați. Doar oprește chestia dang. Glume deoparte,",
"extension_ver_not_reported": "Nu a fost raportat",
"extension_version": "Versiune extensie",
"extensions": "Extensii",
"extensions_use_toggle": "Utilizați extensia browserului pentru a trimite cereri (dacă există)",
"follow": "Follow Us",
"font_size": "Marimea fontului",
"font_size_large": "Mare",
"font_size_medium": "Mediu",
"font_size_small": "Mic",
"interceptor": "Interceptor",
"interceptor_description": "Middleware între aplicație și API-uri.",
"language": "Limba",
"light_mode": "Ușoară",
"official_proxy_hosting": "Proxy oficial este găzduit de Hoppscotch.",
"profile": "Profile",
"profile_description": "Update your profile details",
"profile_email": "Email address",
"profile_name": "Profile name",
"proxy": "Proxy",
"proxy_url": "URL proxy",
"proxy_use_toggle": "Utilizați middleware-ul proxy pentru a trimite cereri",
"read_the": "Citeste",
"reset_default": "Resetare la valorile implicite",
"short_codes": "Short codes",
"short_codes_description": "Short codes which were created by you.",
"sidebar_on_left": "Sidebar on left",
"sync": "Sincroniza",
"sync_collections": "Colecții",
"sync_description": "Aceste setări sunt sincronizate cu cloud.",
"sync_environments": "Medii",
"sync_history": "Istorie",
"system_mode": "Sistem",
"telemetry": "Telemetrie",
"telemetry_helps_us": "Telemetria ne ajută să ne personalizăm operațiunile și să vă oferim cea mai bună experiență.",
"theme": "Temă",
"theme_description": "Personalizați tema aplicației.",
"use_experimental_url_bar": "Utilizați bara URL experimentală cu evidențierea mediului",
"user": "Utilizator",
"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": "Închideți meniul curent",
"command_menu": "Căutare și meniu de comandă",
"help_menu": "Meniul Ajutor",
"show_all": "Comenzi rapide de la tastatură",
"title": "General"
},
"miscellaneous": {
"invite": "Invitați oamenii la Hoppscotch",
"title": "Diverse"
},
"navigation": {
"back": "Reveniți la pagina anterioară",
"documentation": "Accesați pagina Documentație",
"forward": "Mergeți la pagina următoare",
"graphql": "Accesați pagina GraphQL",
"profile": "Go to Profile page",
"realtime": "Accesați pagina în timp real",
"rest": "Accesați pagina REST",
"settings": "Accesați pagina Setări",
"title": "Navigare"
},
"request": {
"copy_request_link": "Copiați legătura de solicitare",
"delete_method": "Selectați metoda DELETE",
"get_method": "Selectați metoda GET",
"head_method": "Selectați metoda HEAD",
"method": "Metodă",
"next_method": "Selectați Metoda următoare",
"post_method": "Selectați metoda POST",
"previous_method": "Selectați Metoda anterioară",
"put_method": "Selectați metoda PUT",
"reset_request": "Cerere de resetare",
"save_to_collections": "Salvați în colecții",
"send_request": "Trimite cerere",
"title": "Cerere"
},
"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": "Afișați codul",
"collection": "Expand Collection Panel",
"more": "Afișați mai multe",
"sidebar": "Afișați bara laterală"
},
"socketio": {
"communication": "Comunicare",
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
"event_name": "Numele evenimentului",
"events": "Evenimente",
"log": "Buturuga",
"url": "URL"
},
"sse": {
"event_type": "Tip de eveniment",
"log": "Buturuga",
"url": "URL"
},
"state": {
"bulk_mode": "Editare în bloc",
"bulk_mode_placeholder": "Intrările sunt separate prin linie nouă\nCheile și valorile sunt separate prin:\nPrepend # la orice rând pe care doriți să îl adăugați, dar păstrați-l dezactivat",
"cleared": "Eliminat",
"connected": "Conectat",
"connected_to": "Conectat la {name}",
"connecting_to": "Se conectează la {name} ...",
"connection_error": "Failed to connect",
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"copied_to_clipboard": "Copiat în clipboard",
"deleted": "Șters",
"deprecated": "DEPRECAT",
"disabled": "Dezactivat",
"disconnected": "Deconectat",
"disconnected_from": "Deconectat de la {name}",
"docs_generated": "Documentație generată",
"download_started": "Descărcarea a început",
"enabled": "Activat",
"file_imported": "Fișier importat",
"finished_in": "Finalizat în {duration} ms",
"history_deleted": "Istoricul a fost șters",
"linewrap": "Înfășurați liniile",
"loading": "Se încarcă...",
"message_received": "Message: {message} arrived on topic: {topic}",
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
"none": "Nici unul",
"nothing_found": "Nimic găsit pentru",
"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": "Se așteaptă trimiterea cererii"
},
"support": {
"changelog": "Citiți mai multe despre ultimele versiuni",
"chat": "Întrebări? Vorbeste cu noi!",
"community": "Puneți întrebări și ajutați-i pe ceilalți",
"documentation": "Citiți mai multe despre Hoppscotch",
"forum": "Puneți întrebări și primiți răspunsuri",
"github": "Follow us on Github",
"shortcuts": "Răsfoiți aplicația mai repede",
"team": "Luați legătura cu echipa",
"title": "A sustine",
"twitter": "Urmăriți-ne pe Twitter"
},
"tab": {
"authorization": "Autorizare",
"body": "Corp",
"collections": "Colecții",
"documentation": "Documentație",
"headers": "Anteturi",
"history": "Istorie",
"mqtt": "MQTT",
"parameters": "Parametrii",
"pre_request_script": "Script de cerere prealabilă",
"queries": "Întrebări",
"query": "Interogare",
"schema": "Schema",
"socketio": "Socket.IO",
"sse": "SSE",
"tests": "Teste",
"types": "Tipuri",
"variables": "Variabile",
"websocket": "WebSocket"
},
"team": {
"already_member": "You are already a member of this team. Contact your team owner.",
"create_new": "Creați o echipă nouă",
"deleted": "Echipa a fost ștearsă",
"edit": "Editați echipa",
"email": "E-mail",
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Ieșiți din echipă",
"exit_disabled": "Numai proprietarul nu poate ieși din echipă",
"invalid_email_format": "Formatul de e-mail nu este valid",
"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": "Vă rugăm să oferiți o permisiune validă membrilor echipei",
"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": "Alăturați-vă programului beta pentru a accesa echipe.",
"join_team": "Join {team}",
"joined_team": "You have joined {team}",
"joined_team_description": "You are now a member of this team",
"left": "Ai părăsit echipa",
"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": "Utilizatorul a fost eliminat",
"member_role_updated": "Rolurile utilizatorului au fost actualizate",
"members": "Membri",
"name_length_insufficient": "Numele echipei trebuie să aibă cel puțin 6 caractere",
"name_updated": "Team name updated",
"new": "Echipă nouă",
"new_created": "Nouă echipă creată",
"new_name": "Noua mea echipă",
"no_access": "Nu aveți acces de editare la aceste colecții",
"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": "Permisiuni",
"saved": "Echipa salvată",
"select_a_team": "Select a team",
"title": "Echipe",
"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": "Cod JavaScript",
"learn": "Citiți documentația",
"passed": "test passed",
"report": "Raport de testare",
"results": "Rezultatele testului",
"script": "Script",
"snippets": "Fragmente"
},
"websocket": {
"communication": "Comunicare",
"log": "Buturuga",
"message": "Mesaj",
"protocols": "Protocoale",
"url": "URL"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 831 KiB

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,245 +0,0 @@
<template>
<div class="autocomplete-wrapper">
<input
ref="acInput"
:value="text"
type="text"
autocomplete="off"
:placeholder="placeholder"
:spellcheck="spellcheck"
:autocapitalize="autocapitalize"
:class="styles"
@input.stop="
(e) => {
$emit('input', e.target.value)
updateSuggestions(e)
}
"
@keyup="updateSuggestions"
@click="updateSuggestions"
@keydown="handleKeystroke"
@change="$emit('change', $event)"
/>
<ul
v-if="suggestions.length > 0 && suggestionsVisible"
class="suggestions"
:style="{ transform: `translate(${suggestionsOffsetLeft}px, 0)` }"
>
<li
v-for="(suggestion, index) in suggestions"
:key="`suggestion-${index}`"
:class="{ active: currentSuggestionIndex === index }"
@click.prevent="forceSuggestion(suggestion)"
>
{{ suggestion }}
</li>
</ul>
</div>
</template>
<script>
import { defineComponent } from "vue"
export default defineComponent({
props: {
spellcheck: {
type: Boolean,
default: true,
required: false,
},
autocapitalize: {
type: String,
default: "off",
required: false,
},
placeholder: {
type: String,
default: "",
required: false,
},
source: {
type: Array,
required: true,
},
value: {
type: String,
default: "",
required: false,
},
styles: {
type: String,
default: "",
},
},
emits: ["input", "change"],
data() {
return {
text: this.value,
selectionStart: 0,
suggestionsOffsetLeft: 0,
currentSuggestionIndex: -1,
suggestionsVisible: false,
}
},
computed: {
/**
* Gets the suggestions list to be displayed under the input box.
*
* @returns {default.props.source|{type, required}}
*/
suggestions() {
const input = this.text.substring(0, this.selectionStart)
return (
this.source
.filter(
(entry) =>
entry.toLowerCase().startsWith(input.toLowerCase()) &&
input.toLowerCase() !== entry.toLowerCase()
)
// Cut off the part that's already been typed.
.map((entry) => entry.substring(this.selectionStart))
// We only want the top 10 suggestions.
.slice(0, 10)
)
},
},
watch: {
value(newValue) {
this.text = newValue
},
},
mounted() {
this.updateSuggestions({
target: this.$refs.acInput,
})
},
methods: {
updateSuggestions(event) {
// Hide suggestions if ESC pressed.
if (event.code && event.code === "Escape") {
event.preventDefault()
this.suggestionsVisible = false
this.currentSuggestionIndex = -1
return
}
// As suggestions is a reactive property, this implicitly
// causes suggestions to update.
this.selectionStart = this.$refs.acInput.selectionStart
this.suggestionsOffsetLeft = 12 * this.selectionStart
this.suggestionsVisible = true
},
forceSuggestion(text) {
const input = this.text.substring(0, this.selectionStart)
this.text = input + text
this.selectionStart = this.text.length
this.suggestionsVisible = true
this.currentSuggestionIndex = -1
this.$emit("input", this.text)
},
handleKeystroke(event) {
switch (event.code) {
case "Enter":
event.preventDefault()
if (this.currentSuggestionIndex > -1)
this.forceSuggestion(
this.suggestions.find(
(_item, index) => index === this.currentSuggestionIndex
)
)
break
case "ArrowUp":
event.preventDefault()
this.currentSuggestionIndex =
this.currentSuggestionIndex - 1 >= 0
? this.currentSuggestionIndex - 1
: 0
break
case "ArrowDown":
event.preventDefault()
this.currentSuggestionIndex =
this.currentSuggestionIndex < this.suggestions.length - 1
? this.currentSuggestionIndex + 1
: this.suggestions.length - 1
break
case "Tab": {
const activeSuggestion =
this.suggestions[
this.currentSuggestionIndex >= 0 ? this.currentSuggestionIndex : 0
]
if (!activeSuggestion) {
return
}
event.preventDefault()
const input = this.text.substring(0, this.selectionStart)
this.text = input + activeSuggestion
break
}
}
},
},
})
</script>
<style lang="scss" scoped>
.autocomplete-wrapper {
@apply relative;
@apply contents;
input:focus + ul.suggestions,
ul.suggestions:hover {
@apply block;
}
ul.suggestions {
@apply hidden;
@apply bg-popover;
@apply absolute;
@apply mx-2;
@apply left-0;
@apply z-50;
@apply shadow-lg;
@apply max-h-46;
@apply overflow-y-auto;
top: calc(100% - 4px);
border-radius: 0 0 8px 8px;
li {
@apply w-full;
@apply block;
@apply py-2 px-4;
@apply text-secondary;
&:last-child {
border-radius: 0 0 8px 8px;
}
&:hover,
&.active {
@apply bg-accentDark;
@apply text-accentContrast;
@apply cursor-pointer;
}
}
}
}
</style>

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,22 +0,0 @@
<template>
<span class="chip">
<component :is="IconFile" class="opacity-75 svg-icons" />
<span class="px-2 truncate max-w-32"><slot></slot></span>
</span>
</template>
<script setup lang="ts">
import IconFile from "~icons/lucide/file"
</script>
<style lang="scss" scoped>
.chip {
@apply inline-flex;
@apply items-center;
@apply justify-center;
@apply rounded;
@apply pl-2;
@apply pr-0.5;
@apply bg-primaryDark;
}
</style>

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,107 +0,0 @@
import {
Analytics,
getAnalytics,
logEvent,
setAnalyticsCollectionEnabled,
setUserId,
setUserProperties,
} from "firebase/analytics"
import { authEvents$ } from "./auth"
import {
HoppAccentColor,
HoppBgColor,
settings$,
settingsStore,
} from "~/newstore/settings"
let analytics: Analytics | null = null
type SettingsCustomDimensions = {
usesProxy: boolean
usesExtension: boolean
syncCollections: boolean
syncEnvironments: boolean
syncHistory: boolean
usesBg: HoppBgColor
usesAccent: HoppAccentColor
usesTelemetry: boolean
}
type HoppRequestEvent =
| {
platform: "rest" | "graphql-query" | "graphql-schema"
strategy: "normal" | "proxy" | "extension"
}
| { platform: "wss" | "sse" | "socketio" | "mqtt" }
export function initAnalytics() {
analytics = getAnalytics()
initLoginListeners()
initSettingsListeners()
}
function initLoginListeners() {
authEvents$.subscribe((ev) => {
if (ev.event === "login") {
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
setUserId(analytics, ev.user.uid)
logEvent(analytics, "login", {
method: ev.user.providerData[0]?.providerId, // Assume the first provider is the login provider
})
}
} else if (ev.event === "logout") {
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
logEvent(analytics, "logout")
}
}
})
}
function initSettingsListeners() {
// Keep track of the telemetry status
let telemetryStatus = settingsStore.value.TELEMETRY_ENABLED
settings$.subscribe((settings) => {
const conf: SettingsCustomDimensions = {
usesProxy: settings.PROXY_ENABLED,
usesExtension: settings.EXTENSIONS_ENABLED,
syncCollections: settings.syncCollections,
syncEnvironments: settings.syncEnvironments,
syncHistory: settings.syncHistory,
usesAccent: settings.THEME_COLOR,
usesBg: settings.BG_COLOR,
usesTelemetry: settings.TELEMETRY_ENABLED,
}
// User toggled telemetry mode to off or to on
if (
((telemetryStatus && !settings.TELEMETRY_ENABLED) ||
settings.TELEMETRY_ENABLED) &&
analytics
) {
setUserProperties(analytics, conf)
}
telemetryStatus = settings.TELEMETRY_ENABLED
if (analytics) setAnalyticsCollectionEnabled(analytics, telemetryStatus)
})
if (analytics) setAnalyticsCollectionEnabled(analytics, telemetryStatus)
}
export function logHoppRequestRunToAnalytics(ev: HoppRequestEvent) {
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
logEvent(analytics, "hopp-request", ev)
}
}
export function logPageView(pagePath: string) {
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
logEvent(analytics, "page_view", {
page_path: pagePath,
})
}
}

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,176 +0,0 @@
import {
collection,
doc,
getFirestore,
onSnapshot,
setDoc,
} from "firebase/firestore"
import {
translateToNewRESTCollection,
translateToNewGQLCollection,
} from "@hoppscotch/data"
import { currentUser$ } from "./auth"
import {
restCollections$,
graphqlCollections$,
setRESTCollections,
setGraphqlCollections,
} from "~/newstore/collections"
import { getSettingSubject, settingsStore } from "~/newstore/settings"
type CollectionFlags = "collectionsGraphql" | "collections"
/**
* Whether the collections are loaded. If this is set to true
* Updates to the collections store are written into firebase.
*
* If you have want to update the store and not fire the store update
* subscription, set this variable to false, do the update and then
* set it to true
*/
let loadedRESTCollections = false
/**
* Whether the collections are loaded. If this is set to true
* Updates to the collections store are written into firebase.
*
* If you have want to update the store and not fire the store update
* subscription, set this variable to false, do the update and then
* set it to true
*/
let loadedGraphqlCollections = false
export async function writeCollections(
collection: any[],
flag: CollectionFlags
) {
if (currentUser$.value === null)
throw new Error("User not logged in to write collections")
const cl = {
updatedOn: new Date(),
author: currentUser$.value.uid,
author_name: currentUser$.value.displayName,
author_image: currentUser$.value.photoURL,
collection,
}
try {
await setDoc(
doc(getFirestore(), "users", currentUser$.value.uid, flag, "sync"),
cl
)
} catch (e) {
console.error("error updating", cl, e)
throw e
}
}
export function initCollections() {
const restCollSub = restCollections$.subscribe((collections) => {
if (
loadedRESTCollections &&
currentUser$.value &&
settingsStore.value.syncCollections
) {
writeCollections(collections, "collections")
}
})
const gqlCollSub = graphqlCollections$.subscribe((collections) => {
if (
loadedGraphqlCollections &&
currentUser$.value &&
settingsStore.value.syncCollections
) {
writeCollections(collections, "collectionsGraphql")
}
})
let restSnapshotStop: (() => void) | null = null
let graphqlSnapshotStop: (() => void) | null = null
const currentUserSub = currentUser$.subscribe((user) => {
if (!user) {
if (restSnapshotStop) {
restSnapshotStop()
restSnapshotStop = null
}
if (graphqlSnapshotStop) {
graphqlSnapshotStop()
graphqlSnapshotStop = null
}
} else {
restSnapshotStop = onSnapshot(
collection(getFirestore(), "users", user.uid, "collections"),
(collectionsRef) => {
const collections: any[] = []
collectionsRef.forEach((doc) => {
const collection = doc.data()
collection.id = doc.id
collections.push(collection)
})
// Prevent infinite ping-pong of updates
loadedRESTCollections = false
// TODO: Wth is with collections[0]
if (collections.length > 0 && settingsStore.value.syncCollections) {
setRESTCollections(
(collections[0].collection ?? []).map(
translateToNewRESTCollection
)
)
}
loadedRESTCollections = true
}
)
graphqlSnapshotStop = onSnapshot(
collection(getFirestore(), "users", user.uid, "collectionsGraphql"),
(collectionsRef) => {
const collections: any[] = []
collectionsRef.forEach((doc) => {
const collection = doc.data()
collection.id = doc.id
collections.push(collection)
})
// Prevent infinite ping-pong of updates
loadedGraphqlCollections = false
// TODO: Wth is with collections[0]
if (collections.length > 0 && settingsStore.value.syncCollections) {
setGraphqlCollections(
(collections[0].collection ?? []).map(translateToNewGQLCollection)
)
}
loadedGraphqlCollections = true
}
)
}
})
let oldSyncStatus = settingsStore.value.syncCollections
const syncStop = getSettingSubject("syncCollections").subscribe(
(newStatus) => {
if (oldSyncStatus === true && newStatus === false) {
restSnapshotStop?.()
graphqlSnapshotStop?.()
oldSyncStatus = newStatus
} else if (oldSyncStatus === false && newStatus === true) {
syncStop.unsubscribe()
restCollSub.unsubscribe()
gqlCollSub.unsubscribe()
currentUserSub.unsubscribe()
initCollections()
}
}
)
}

View File

@@ -1,178 +0,0 @@
import { Environment } from "@hoppscotch/data"
import {
collection,
doc,
getFirestore,
onSnapshot,
setDoc,
} from "firebase/firestore"
import { currentUser$ } from "./auth"
import {
environments$,
globalEnv$,
replaceEnvironments,
setGlobalEnvVariables,
} from "~/newstore/environments"
import { getSettingSubject, settingsStore } from "~/newstore/settings"
/**
* Used locally to prevent infinite loop when environment sync update
* is applied to the store which then fires the store sync listener.
* When you want to update environments and not want to fire the update listener,
* set this to true and then set it back to false once it is done
*/
let loadedEnvironments = false
/**
* Used locally to prevent infinite loop when global env sync update
* is applied to the store which then fires the store sync listener.
* When you want to update global env and not want to fire the update listener,
* set this to true and then set it back to false once it is done
*/
let loadedGlobals = true
async function writeEnvironments(environment: Environment[]) {
if (currentUser$.value == null)
throw new Error("Cannot write environments when signed out")
const ev = {
updatedOn: new Date(),
author: currentUser$.value.uid,
author_name: currentUser$.value.displayName,
author_image: currentUser$.value.photoURL,
environment,
}
try {
await setDoc(
doc(
getFirestore(),
"users",
currentUser$.value.uid,
"environments",
"sync"
),
ev
)
} catch (e) {
console.error("error updating", ev, e)
throw e
}
}
async function writeGlobalEnvironment(variables: Environment["variables"]) {
if (currentUser$.value == null)
throw new Error("Cannot write global environment when signed out")
const ev = {
updatedOn: new Date(),
author: currentUser$.value.uid,
author_name: currentUser$.value.displayName,
author_image: currentUser$.value.photoURL,
variables,
}
try {
await setDoc(
doc(getFirestore(), "users", currentUser$.value.uid, "globalEnv", "sync"),
ev
)
} catch (e) {
console.error("error updating", ev, e)
throw e
}
}
export function initEnvironments() {
const envListenSub = environments$.subscribe((envs) => {
if (
currentUser$.value &&
settingsStore.value.syncEnvironments &&
loadedEnvironments
) {
writeEnvironments(envs)
}
})
const globalListenSub = globalEnv$.subscribe((vars) => {
if (
currentUser$.value &&
settingsStore.value.syncEnvironments &&
loadedGlobals
) {
writeGlobalEnvironment(vars)
}
})
let envSnapshotStop: (() => void) | null = null
let globalsSnapshotStop: (() => void) | null = null
const currentUserSub = currentUser$.subscribe((user) => {
if (!user) {
// User logged out, clean up snapshot listener
if (envSnapshotStop) {
envSnapshotStop()
envSnapshotStop = null
}
if (globalsSnapshotStop) {
globalsSnapshotStop()
globalsSnapshotStop = null
}
} else if (user) {
envSnapshotStop = onSnapshot(
collection(getFirestore(), "users", user.uid, "environments"),
(environmentsRef) => {
const environments: any[] = []
environmentsRef.forEach((doc) => {
const environment = doc.data()
environment.id = doc.id
environments.push(environment)
})
loadedEnvironments = false
if (environments.length > 0 && settingsStore.value.syncEnvironments) {
replaceEnvironments(environments[0].environment)
}
loadedEnvironments = true
}
)
globalsSnapshotStop = onSnapshot(
collection(getFirestore(), "users", user.uid, "globalEnv"),
(globalsRef) => {
if (globalsRef.docs.length === 0) {
loadedGlobals = true
return
}
const doc = globalsRef.docs[0].data()
loadedGlobals = false
if (settingsStore.value.syncEnvironments)
setGlobalEnvVariables(doc.variables)
loadedGlobals = true
}
)
}
})
let oldSyncStatus = settingsStore.value.syncEnvironments
const syncStop = getSettingSubject("syncEnvironments").subscribe(
(newStatus) => {
if (oldSyncStatus === true && newStatus === false) {
envSnapshotStop?.()
globalsSnapshotStop?.()
oldSyncStatus = newStatus
} else if (oldSyncStatus === false && newStatus === true) {
syncStop.unsubscribe()
envListenSub.unsubscribe()
globalListenSub.unsubscribe()
currentUserSub.unsubscribe()
initEnvironments()
}
}
)
}

View File

@@ -1,262 +0,0 @@
import {
addDoc,
collection,
deleteDoc,
doc,
getDocs,
getFirestore,
limit,
onSnapshot,
orderBy,
query,
updateDoc,
} from "firebase/firestore"
import { FormDataKeyValue } from "@hoppscotch/data"
import { currentUser$ } from "./auth"
import { getSettingSubject, settingsStore } from "~/newstore/settings"
import {
GQLHistoryEntry,
graphqlHistoryStore,
HISTORY_LIMIT,
RESTHistoryEntry,
restHistoryStore,
setGraphqlHistoryEntries,
setRESTHistoryEntries,
translateToNewGQLHistory,
translateToNewRESTHistory,
} from "~/newstore/history"
type HistoryFBCollections = "history" | "graphqlHistory"
/**
* Whether the history are loaded. If this is set to true
* Updates to the history store are written into firebase.
*
* If you have want to update the store and not fire the store update
* subscription, set this variable to false, do the update and then
* set it to true
*/
let loadedRESTHistory = false
/**
* Whether the history are loaded. If this is set to true
* Updates to the history store are written into firebase.
*
* If you have want to update the store and not fire the store update
* subscription, set this variable to false, do the update and then
* set it to true
*/
let loadedGraphqlHistory = false
const purgeFormDataFromRequest = (req: RESTHistoryEntry): RESTHistoryEntry => {
if (req.request.body.contentType !== "multipart/form-data") return req
req.request.body.body = req.request.body.body.map<FormDataKeyValue>(
(formData) => {
if (!formData.isFile) return formData
return {
active: formData.active,
isFile: false, // Something we can do to keep the status ?
key: formData.key,
value: "",
}
}
)
return req
}
async function writeHistory(
entry: RESTHistoryEntry | GQLHistoryEntry,
col: HistoryFBCollections
) {
const processedEntry =
col === "history"
? purgeFormDataFromRequest(entry as RESTHistoryEntry)
: entry
if (currentUser$.value == null)
throw new Error("User not logged in to sync history")
const hs = {
...processedEntry,
updatedOn: new Date(),
}
try {
await addDoc(
collection(getFirestore(), "users", currentUser$.value.uid, col),
hs
)
} catch (e) {
console.error("error writing to history", hs, e)
throw e
}
}
async function deleteHistory(
entry: (RESTHistoryEntry | GQLHistoryEntry) & { id: string },
col: HistoryFBCollections
) {
if (currentUser$.value == null)
throw new Error("User not logged in to delete history")
try {
await deleteDoc(
doc(getFirestore(), "users", currentUser$.value.uid, col, entry.id)
)
} catch (e) {
console.error("error deleting history", entry, e)
throw e
}
}
async function clearHistory(col: HistoryFBCollections) {
if (currentUser$.value == null)
throw new Error("User not logged in to clear history")
const { docs } = await getDocs(
collection(getFirestore(), "users", currentUser$.value.uid, col)
)
await Promise.all(docs.map((e) => deleteHistory(e as any, col)))
}
async function toggleStar(
entry: (RESTHistoryEntry | GQLHistoryEntry) & { id: string },
col: HistoryFBCollections
) {
if (currentUser$.value == null)
throw new Error("User not logged in to toggle star")
try {
await updateDoc(
doc(getFirestore(), "users", currentUser$.value.uid, col, entry.id),
{ star: !entry.star }
)
} catch (e) {
console.error("error toggling star", entry, e)
throw e
}
}
export function initHistory() {
const restHistorySub = restHistoryStore.dispatches$.subscribe((dispatch) => {
if (
loadedRESTHistory &&
currentUser$.value &&
settingsStore.value.syncHistory
) {
if (dispatch.dispatcher === "addEntry") {
writeHistory(dispatch.payload.entry, "history")
} else if (dispatch.dispatcher === "deleteEntry") {
deleteHistory(dispatch.payload.entry, "history")
} else if (dispatch.dispatcher === "clearHistory") {
clearHistory("history")
} else if (dispatch.dispatcher === "toggleStar") {
toggleStar(dispatch.payload.entry, "history")
}
}
})
const gqlHistorySub = graphqlHistoryStore.dispatches$.subscribe(
(dispatch) => {
if (
loadedGraphqlHistory &&
currentUser$.value &&
settingsStore.value.syncHistory
) {
if (dispatch.dispatcher === "addEntry") {
writeHistory(dispatch.payload.entry, "graphqlHistory")
} else if (dispatch.dispatcher === "deleteEntry") {
deleteHistory(dispatch.payload.entry, "graphqlHistory")
} else if (dispatch.dispatcher === "clearHistory") {
clearHistory("graphqlHistory")
} else if (dispatch.dispatcher === "toggleStar") {
toggleStar(dispatch.payload.entry, "graphqlHistory")
}
}
}
)
let restSnapshotStop: (() => void) | null = null
let graphqlSnapshotStop: (() => void) | null = null
const currentUserSub = currentUser$.subscribe((user) => {
if (!user) {
// Clear the snapshot listeners when the user logs out
if (restSnapshotStop) {
restSnapshotStop()
restSnapshotStop = null
}
if (graphqlSnapshotStop) {
graphqlSnapshotStop()
graphqlSnapshotStop = null
}
} else {
restSnapshotStop = onSnapshot(
query(
collection(getFirestore(), "users", user.uid, "history"),
orderBy("updatedOn", "desc"),
limit(HISTORY_LIMIT)
),
(historyRef) => {
const history: RESTHistoryEntry[] = []
historyRef.forEach((doc) => {
const entry = doc.data()
entry.id = doc.id
entry.updatedOn = doc.data().updatedOn.toDate()
history.push(translateToNewRESTHistory(entry))
})
loadedRESTHistory = false
setRESTHistoryEntries(history)
loadedRESTHistory = true
}
)
graphqlSnapshotStop = onSnapshot(
query(
collection(getFirestore(), "users", user.uid, "graphqlHistory"),
orderBy("updatedOn", "desc"),
limit(HISTORY_LIMIT)
),
(historyRef) => {
const history: GQLHistoryEntry[] = []
historyRef.forEach((doc) => {
const entry = doc.data()
entry.id = doc.id
entry.updatedOn = doc.data().updatedOn.toDate()
history.push(translateToNewGQLHistory(entry))
})
loadedGraphqlHistory = false
setGraphqlHistoryEntries(history)
loadedGraphqlHistory = true
}
)
}
})
let oldSyncStatus = settingsStore.value.syncHistory
const syncStop = getSettingSubject("syncHistory").subscribe((newStatus) => {
if (oldSyncStatus === true && newStatus === false) {
restSnapshotStop?.()
graphqlSnapshotStop?.()
oldSyncStatus = newStatus
} else if (oldSyncStatus === false && newStatus === true) {
syncStop.unsubscribe()
restHistorySub.unsubscribe()
gqlHistorySub.unsubscribe()
currentUserSub.unsubscribe()
initHistory()
}
})
}

View File

@@ -1,40 +0,0 @@
import { initializeApp } from "firebase/app"
import { initAnalytics } from "./analytics"
import { initAuth } from "./auth"
import { initCollections } from "./collections"
import { initEnvironments } from "./environments"
import { initHistory } from "./history"
import { initSettings } from "./settings"
const firebaseConfig = {
apiKey: import.meta.env.VITE_API_KEY,
authDomain: import.meta.env.VITE_AUTH_DOMAIN,
databaseURL: import.meta.env.VITE_DATABASE_URL,
projectId: import.meta.env.VITE_PROJECT_ID,
storageBucket: import.meta.env.VITE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_APP_TITLE,
measurementId: import.meta.env.VITE_MEASUREMENT_ID,
}
let initialized = false
export function initializeFirebase() {
if (!initialized) {
try {
initializeApp(firebaseConfig)
initAuth()
initSettings()
initCollections()
initHistory()
initEnvironments()
initAnalytics()
initialized = true
} catch (e) {
// initializeApp throws exception if we reinitialize
initialized = true
}
}
}

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,93 +0,0 @@
import {
collection,
doc,
getFirestore,
onSnapshot,
setDoc,
} from "firebase/firestore"
import { currentUser$ } from "./auth"
import { applySetting, settingsStore, SettingsType } from "~/newstore/settings"
/**
* Used locally to prevent infinite loop when settings sync update
* is applied to the store which then fires the store sync listener.
* When you want to update settings and not want to fire the update listener,
* set this to true and then set it back to false once it is done
*/
let loadedSettings = false
/**
* Write Transform
*/
async function writeSettings(setting: string, value: any) {
if (currentUser$.value === null)
throw new Error("Cannot write setting, user not signed in")
const st = {
updatedOn: new Date(),
author: currentUser$.value.uid,
author_name: currentUser$.value.displayName,
author_image: currentUser$.value.photoURL,
name: setting,
value,
}
try {
await setDoc(
doc(getFirestore(), "users", currentUser$.value.uid, "settings", setting),
st
)
} catch (e) {
console.error("error updating", st, e)
throw e
}
}
export function initSettings() {
settingsStore.dispatches$.subscribe((dispatch) => {
if (currentUser$.value && loadedSettings) {
if (dispatch.dispatcher === "bulkApplySettings") {
Object.keys(dispatch.payload).forEach((key) => {
writeSettings(key, dispatch.payload[key])
})
} else {
writeSettings(
dispatch.payload.settingKey,
settingsStore.value[dispatch.payload.settingKey as keyof SettingsType]
)
}
}
})
let snapshotStop: (() => void) | null = null
// Subscribe and unsubscribe event listeners
currentUser$.subscribe((user) => {
if (!user && snapshotStop) {
// User logged out
snapshotStop()
snapshotStop = null
} else if (user) {
snapshotStop = onSnapshot(
collection(getFirestore(), "users", user.uid, "settings"),
(settingsRef) => {
const settings: any[] = []
settingsRef.forEach((doc) => {
const setting = doc.data()
setting.id = doc.id
settings.push(setting)
})
loadedSettings = false
settings.forEach((e) => {
if (e && e.name && e.value != null) {
applySetting(e.name, e.value)
}
})
loadedSettings = true
}
)
}
})
}

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

@@ -0,0 +1 @@
./node_modules

View File

@@ -0,0 +1,27 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "error"
},
};

43
packages/hoppscotch-backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# compiled output
/dist
/node_modules
.vscode
.env
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Generated artifacts (GQL Schema SDL generation etc.)
gen/

View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -0,0 +1,38 @@
FROM node:18.8.0 AS builder
WORKDIR /usr/src/app
# # Install pnpm
RUN npm i -g pnpm
COPY .env .
COPY pnpm-lock.yaml .
RUN pnpm fetch
ENV APP_PORT=${PORT}
ENV DB_URL=${DATABASE_URL}
# # PNPM package install
COPY ./packages/hoppscotch-backend .
RUN pnpm i --filter hoppscotch-backend
# Prisma bits
RUN pnpm exec prisma generate
FROM builder AS dev
ENV PRODUCTION="false"
CMD ["pnpm", "run", "start:dev"]
EXPOSE 3170
FROM builder AS prod
ENV PRODUCTION="true"
CMD ["pnpm", "run", "start:prod"]
EXPOSE 3170

View File

View File

View File

@@ -0,0 +1 @@
import '@relmify/jest-fp-ts';

View File

@@ -0,0 +1 @@
require('@relmify/jest-fp-ts');

View File

@@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [
"**/*.hbs"
],
"watchAssets": true
}
}

View File

@@ -0,0 +1,121 @@
{
"name": "hoppscotch-backend",
"version": "2023.4.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"generate-gql-sdl": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' GENERATE_GQL_SCHEMA=true WHITELISTED_ORIGINS='' nest start",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"postinstall": "prisma generate && pnpm run generate-gql-sdl",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"do-test": "pnpm run test"
},
"dependencies": {
"@nestjs-modules/mailer": "^1.8.1",
"@nestjs/apollo": "^10.1.6",
"@nestjs/common": "^9.2.1",
"@nestjs/core": "^9.2.1",
"@nestjs/graphql": "^10.1.6",
"@nestjs/jwt": "^10.0.1",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.2.1",
"@nestjs/throttler": "^4.0.0",
"@prisma/client": "^4.7.1",
"apollo-server-express": "^3.11.1",
"apollo-server-plugin-base": "^3.7.1",
"argon2": "^0.30.3",
"bcrypt": "^5.1.0",
"cookie": "^0.5.0",
"cookie-parser": "^1.4.6",
"express": "^4.17.1",
"express-session": "^1.17.3",
"fp-ts": "^2.13.1",
"graphql": "^15.5.0",
"graphql-query-complexity": "^0.12.0",
"graphql-redis-subscriptions": "^2.5.0",
"graphql-subscriptions": "^2.0.0",
"handlebars": "^4.7.7",
"io-ts": "^2.2.16",
"luxon": "^3.2.1",
"nodemailer": "^6.9.1",
"passport": "^0.6.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-microsoft": "^1.0.0",
"prisma": "^4.7.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.6.0"
},
"devDependencies": {
"@nestjs/cli": "^9.1.5",
"@nestjs/schematics": "^9.0.3",
"@nestjs/testing": "^9.2.1",
"@relmify/jest-fp-ts": "^2.0.2",
"@types/argon2": "^0.15.0",
"@types/bcrypt": "^5.0.0",
"@types/cookie": "^0.5.1",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.14",
"@types/jest": "^29.4.0",
"@types/luxon": "^3.2.0",
"@types/node": "^18.11.10",
"@types/nodemailer": "^6.4.7",
"@types/passport-github2": "^1.2.5",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/passport-microsoft": "^0.0.0",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"cross-env": "^7.0.3",
"eslint": "^8.29.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.4.1",
"jest-mock-extended": "^3.0.1",
"jwt": "link:@types/nestjs/jwt",
"prettier": "^2.8.4",
"source-map-support": "^0.5.21",
"supertest": "^6.3.2",
"ts-jest": "29.0.5",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tsconfig-paths": "4.1.1",
"typescript": "^4.9.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"setupFilesAfterEnv": [
"../jest.setup.js"
],
"preset": "ts-jest",
"clearMocks": true,
"collectCoverage": true,
"coverageDirectory": "coverage",
"coverageProvider": "v8",
"rootDir": "src",
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/$1"
}
}
}

View File

@@ -0,0 +1,270 @@
-- CreateEnum
CREATE TYPE "ReqType" AS ENUM ('REST', 'GQL');
-- CreateEnum
CREATE TYPE "TeamMemberRole" AS ENUM ('OWNER', 'VIEWER', 'EDITOR');
-- CreateTable
CREATE TABLE "Team" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "Team_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TeamMember" (
"id" TEXT NOT NULL,
"role" "TeamMemberRole" NOT NULL,
"userUid" TEXT NOT NULL,
"teamID" TEXT NOT NULL,
CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TeamInvitation" (
"id" TEXT NOT NULL,
"teamID" TEXT NOT NULL,
"creatorUid" TEXT NOT NULL,
"inviteeEmail" TEXT NOT NULL,
"inviteeRole" "TeamMemberRole" NOT NULL,
CONSTRAINT "TeamInvitation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TeamCollection" (
"id" TEXT NOT NULL,
"parentID" TEXT,
"teamID" TEXT NOT NULL,
"title" TEXT NOT NULL,
"orderIndex" INTEGER NOT NULL,
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedOn" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TeamCollection_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TeamRequest" (
"id" TEXT NOT NULL,
"collectionID" TEXT NOT NULL,
"teamID" TEXT NOT NULL,
"title" TEXT NOT NULL,
"request" JSONB NOT NULL,
"orderIndex" INTEGER NOT NULL,
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedOn" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TeamRequest_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Shortcode" (
"id" TEXT NOT NULL,
"request" JSONB NOT NULL,
"creatorUid" TEXT,
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Shortcode_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TeamEnvironment" (
"id" TEXT NOT NULL,
"teamID" TEXT NOT NULL,
"name" TEXT NOT NULL,
"variables" JSONB NOT NULL,
CONSTRAINT "TeamEnvironment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"uid" TEXT NOT NULL,
"displayName" TEXT,
"email" TEXT,
"photoURL" TEXT,
"isAdmin" BOOLEAN NOT NULL DEFAULT false,
"refreshToken" TEXT,
"currentRESTSession" JSONB,
"currentGQLSession" JSONB,
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "User_pkey" PRIMARY KEY ("uid")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"providerRefreshToken" TEXT,
"providerAccessToken" TEXT,
"providerScope" TEXT,
"loggedIn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"deviceIdentifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"userUid" TEXT NOT NULL,
"expiresOn" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "UserSettings" (
"id" TEXT NOT NULL,
"userUid" TEXT NOT NULL,
"properties" JSONB NOT NULL,
"updatedOn" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserSettings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserHistory" (
"id" TEXT NOT NULL,
"userUid" TEXT NOT NULL,
"reqType" "ReqType" NOT NULL,
"request" JSONB NOT NULL,
"responseMetadata" JSONB NOT NULL,
"isStarred" BOOLEAN NOT NULL,
"executedOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UserHistory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserEnvironment" (
"id" TEXT NOT NULL,
"userUid" TEXT NOT NULL,
"name" TEXT,
"variables" JSONB NOT NULL,
"isGlobal" BOOLEAN NOT NULL,
CONSTRAINT "UserEnvironment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InvitedUsers" (
"adminUid" TEXT NOT NULL,
"adminEmail" TEXT NOT NULL,
"inviteeEmail" TEXT NOT NULL,
"invitedOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "UserRequest" (
"id" TEXT NOT NULL,
"collectionID" TEXT NOT NULL,
"userUid" TEXT NOT NULL,
"title" TEXT NOT NULL,
"request" JSONB NOT NULL,
"type" "ReqType" NOT NULL,
"orderIndex" INTEGER NOT NULL,
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedOn" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserRequest_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserCollection" (
"id" TEXT NOT NULL,
"parentID" TEXT,
"userUid" TEXT NOT NULL,
"title" TEXT NOT NULL,
"orderIndex" INTEGER NOT NULL,
"type" "ReqType" NOT NULL,
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedOn" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserCollection_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "TeamMember_teamID_userUid_key" ON "TeamMember"("teamID", "userUid");
-- CreateIndex
CREATE INDEX "TeamInvitation_teamID_idx" ON "TeamInvitation"("teamID");
-- CreateIndex
CREATE UNIQUE INDEX "TeamInvitation_teamID_inviteeEmail_key" ON "TeamInvitation"("teamID", "inviteeEmail");
-- CreateIndex
CREATE UNIQUE INDEX "Shortcode_id_creatorUid_key" ON "Shortcode"("id", "creatorUid");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_deviceIdentifier_token_key" ON "VerificationToken"("deviceIdentifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "UserSettings_userUid_key" ON "UserSettings"("userUid");
-- CreateIndex
CREATE UNIQUE INDEX "InvitedUsers_inviteeEmail_key" ON "InvitedUsers"("inviteeEmail");
-- AddForeignKey
ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_teamID_fkey" FOREIGN KEY ("teamID") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TeamInvitation" ADD CONSTRAINT "TeamInvitation_teamID_fkey" FOREIGN KEY ("teamID") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TeamCollection" ADD CONSTRAINT "TeamCollection_parentID_fkey" FOREIGN KEY ("parentID") REFERENCES "TeamCollection"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TeamCollection" ADD CONSTRAINT "TeamCollection_teamID_fkey" FOREIGN KEY ("teamID") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TeamRequest" ADD CONSTRAINT "TeamRequest_collectionID_fkey" FOREIGN KEY ("collectionID") REFERENCES "TeamCollection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TeamRequest" ADD CONSTRAINT "TeamRequest_teamID_fkey" FOREIGN KEY ("teamID") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TeamEnvironment" ADD CONSTRAINT "TeamEnvironment_teamID_fkey" FOREIGN KEY ("teamID") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "VerificationToken" ADD CONSTRAINT "VerificationToken_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserSettings" ADD CONSTRAINT "UserSettings_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserHistory" ADD CONSTRAINT "UserHistory_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserEnvironment" ADD CONSTRAINT "UserEnvironment_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InvitedUsers" ADD CONSTRAINT "InvitedUsers_adminUid_fkey" FOREIGN KEY ("adminUid") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserRequest" ADD CONSTRAINT "UserRequest_collectionID_fkey" FOREIGN KEY ("collectionID") REFERENCES "UserCollection"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserRequest" ADD CONSTRAINT "UserRequest_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserCollection" ADD CONSTRAINT "UserCollection_parentID_fkey" FOREIGN KEY ("parentID") REFERENCES "UserCollection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserCollection" ADD CONSTRAINT "UserCollection_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@@ -0,0 +1,205 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x"]
}
model Team {
id String @id @default(cuid())
name String
members TeamMember[]
TeamInvitation TeamInvitation[]
TeamCollection TeamCollection[]
TeamRequest TeamRequest[]
TeamEnvironment TeamEnvironment[]
}
model TeamMember {
id String @id @default(uuid()) // Membership ID
role TeamMemberRole
userUid String
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
@@unique([teamID, userUid])
}
model TeamInvitation {
id String @id @default(cuid())
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
creatorUid String
inviteeEmail String
inviteeRole TeamMemberRole
@@unique([teamID, inviteeEmail])
@@index([teamID])
}
model TeamCollection {
id String @id @default(cuid())
parentID String?
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent")
requests TeamRequest[]
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
model TeamRequest {
id String @id @default(cuid())
collectionID String
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String
request Json
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
model Shortcode {
id String @id
request Json
creatorUid String?
createdOn DateTime @default(now())
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
}
model TeamEnvironment {
id String @id @default(cuid())
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
name String
variables Json
}
model User {
uid String @id @default(cuid())
displayName String?
email String? @unique
photoURL String?
isAdmin Boolean @default(false)
refreshToken String?
providerAccounts Account[]
VerificationToken VerificationToken[]
settings UserSettings?
UserHistory UserHistory[]
UserEnvironments UserEnvironment[]
userCollections UserCollection[]
userRequests UserRequest[]
currentRESTSession Json?
currentGQLSession Json?
createdOn DateTime @default(now()) @db.Timestamp(3)
invitedUsers InvitedUsers[]
}
model Account {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [uid], onDelete: Cascade)
provider String
providerAccountId String
providerRefreshToken String?
providerAccessToken String?
providerScope String?
loggedIn DateTime @default(now()) @db.Timestamp(3)
@@unique(fields: [provider, providerAccountId], name: "verifyProviderAccount")
}
model VerificationToken {
deviceIdentifier String
token String @unique @default(cuid())
userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
expiresOn DateTime @db.Timestamp(3)
@@unique(fields: [deviceIdentifier, token], name: "passwordless_deviceIdentifier_tokens")
}
model UserSettings {
id String @id @default(cuid())
userUid String @unique
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
properties Json
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
model UserHistory {
id String @id @default(cuid())
userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
reqType ReqType
request Json
responseMetadata Json
isStarred Boolean
executedOn DateTime @default(now()) @db.Timestamp(3)
}
enum ReqType {
REST
GQL
}
model UserEnvironment {
id String @id @default(cuid())
userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
name String?
variables Json
isGlobal Boolean
}
model InvitedUsers {
adminUid String
user User @relation(fields: [adminUid], references: [uid], onDelete: Cascade)
adminEmail String
inviteeEmail String @unique
invitedOn DateTime @default(now()) @db.Timestamp(3)
}
model UserRequest {
id String @id @default(cuid())
userCollection UserCollection @relation(fields: [collectionID], references: [id])
collectionID String
userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
title String
request Json
type ReqType
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
model UserCollection {
id String @id @default(cuid())
parentID String?
parent UserCollection? @relation("ParentUserCollection", fields: [parentID], references: [id], onDelete: Cascade)
children UserCollection[] @relation("ParentUserCollection")
requests UserRequest[]
userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
title String
orderIndex Int
type ReqType
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
enum TeamMemberRole {
OWNER
VIEWER
EDITOR
}

View File

@@ -0,0 +1,4 @@
import { ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Admin {}

View File

@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { AdminResolver } from './admin.resolver';
import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module';
import { PubSubModule } from '../pubsub/pubsub.module';
import { UserModule } from '../user/user.module';
import { MailerModule } from '../mailer/mailer.module';
import { TeamModule } from '../team/team.module';
import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
import { TeamCollectionModule } from '../team-collection/team-collection.module';
import { TeamRequestModule } from '../team-request/team-request.module';
@Module({
imports: [
PrismaModule,
PubSubModule,
UserModule,
MailerModule,
TeamModule,
TeamInvitationModule,
TeamEnvironmentsModule,
TeamCollectionModule,
TeamRequestModule,
],
providers: [AdminResolver, AdminService],
exports: [AdminService],
})
export class AdminModule {}

View File

@@ -0,0 +1,425 @@
import {
Args,
ID,
Mutation,
Parent,
Query,
ResolveField,
Resolver,
Subscription,
} from '@nestjs/graphql';
import { Admin } from './admin.model';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from '../guards/gql-auth.guard';
import { GqlAdminGuard } from './guards/gql-admin.guard';
import { GqlAdmin } from './decorators/gql-admin.decorator';
import { AdminService } from './admin.service';
import * as E from 'fp-ts/Either';
import { throwErr } from '../utils';
import { AuthUser } from '../types/AuthUser';
import { InvitedUser } from './invited-user.model';
import { GqlUser } from '../decorators/gql-user.decorator';
import { PubSubService } from '../pubsub/pubsub.service';
import { Team, TeamMember } from '../team/team.model';
import { User } from '../user/user.model';
import { TeamInvitation } from '../team-invitation/team-invitation.model';
import { PaginationArgs } from '../types/input-types.args';
import {
AddUserToTeamArgs,
ChangeUserRoleInTeamArgs,
} from './input-types.args';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => Admin)
export class AdminResolver {
constructor(
private adminService: AdminService,
private readonly pubsub: PubSubService,
) {}
/* Query */
@Query(() => Admin, {
description: 'Gives details of the admin executing this query',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
admin(@GqlAdmin() admin: Admin) {
return admin;
}
@ResolveField(() => [User], {
description: 'Returns a list of all admin users in infra',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async admins() {
const admins = await this.adminService.fetchAdmins();
return admins;
}
@ResolveField(() => User, {
description: 'Returns a user info by UID',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async userInfo(
@Args({
name: 'userUid',
type: () => ID,
description: 'The user UID',
})
userUid: string,
): Promise<AuthUser> {
const user = await this.adminService.fetchUserInfo(userUid);
if (E.isLeft(user)) throwErr(user.left);
return user.right;
}
@ResolveField(() => [User], {
description: 'Returns a list of all the users in infra',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async allUsers(
@Parent() admin: Admin,
@Args() args: PaginationArgs,
): Promise<AuthUser[]> {
const users = await this.adminService.fetchUsers(args.cursor, args.take);
return users;
}
@ResolveField(() => [InvitedUser], {
description: 'Returns a list of all the invited users',
})
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
const users = await this.adminService.fetchInvitedUsers();
return users;
}
@ResolveField(() => [Team], {
description: 'Returns a list of all the teams in the infra',
})
async allTeams(
@Parent() admin: Admin,
@Args() args: PaginationArgs,
): Promise<Team[]> {
const teams = await this.adminService.fetchAllTeams(args.cursor, args.take);
return teams;
}
@ResolveField(() => Team, {
description: 'Returns a team info by ID when requested by Admin',
})
async teamInfo(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which info to fetch',
})
teamID: string,
): Promise<Team> {
const team = await this.adminService.getTeamInfo(teamID);
if (E.isLeft(team)) throwErr(team.left);
return team.right;
}
@ResolveField(() => Number, {
description: 'Return count of all the members in a team',
})
async membersCountInTeam(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
nullable: false,
})
teamID: string,
): Promise<number> {
const teamMembersCount = await this.adminService.membersCountInTeam(teamID);
return teamMembersCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored collections in a team',
})
async collectionCountInTeam(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const teamCollCount = await this.adminService.collectionCountInTeam(teamID);
return teamCollCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored requests in a team',
})
async requestCountInTeam(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const teamReqCount = await this.adminService.requestCountInTeam(teamID);
return teamReqCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored environments in a team',
})
async environmentCountInTeam(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const envsCount = await this.adminService.environmentCountInTeam(teamID);
return envsCount;
}
@ResolveField(() => [TeamInvitation], {
description: 'Return all the pending invitations in a team',
})
async pendingInvitationCountInTeam(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
) {
const invitations = await this.adminService.pendingInvitationCountInTeam(
teamID,
);
return invitations;
}
@ResolveField(() => Number, {
description: 'Return total number of Users in organization',
})
async usersCount() {
return this.adminService.getUsersCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Teams in organization',
})
async teamsCount() {
return this.adminService.getTeamsCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Team Collections in organization',
})
async teamCollectionsCount() {
return this.adminService.getTeamCollectionsCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Team Requests in organization',
})
async teamRequestsCount() {
return this.adminService.getTeamRequestsCount();
}
/* Mutations */
@Mutation(() => InvitedUser, {
description: 'Invite a user to the infra using email',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async inviteNewUser(
@GqlUser() adminUser: AuthUser,
@Args({
name: 'inviteeEmail',
description: 'invitee email',
})
inviteeEmail: string,
): Promise<InvitedUser> {
const invitedUser = await this.adminService.inviteUserToSignInViaEmail(
adminUser.uid,
adminUser.email,
inviteeEmail,
);
if (E.isLeft(invitedUser)) throwErr(invitedUser.left);
return invitedUser.right;
}
@Mutation(() => Boolean, {
description: 'Delete an user account from infra',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async removeUserByAdmin(
@Args({
name: 'userUID',
description: 'users UID',
type: () => ID,
})
userUID: string,
): Promise<boolean> {
const invitedUser = await this.adminService.removeUserAccount(userUID);
if (E.isLeft(invitedUser)) throwErr(invitedUser.left);
return invitedUser.right;
}
@Mutation(() => Boolean, {
description: 'Make user an admin',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async makeUserAdmin(
@Args({
name: 'userUID',
description: 'users UID',
type: () => ID,
})
userUID: string,
): Promise<boolean> {
const admin = await this.adminService.makeUserAdmin(userUID);
if (E.isLeft(admin)) throwErr(admin.left);
return admin.right;
}
@Mutation(() => Boolean, {
description: 'Remove user as admin',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async removeUserAsAdmin(
@Args({
name: 'userUID',
description: 'users UID',
type: () => ID,
})
userUID: string,
): Promise<boolean> {
const admin = await this.adminService.removeUserAsAdmin(userUID);
if (E.isLeft(admin)) throwErr(admin.left);
return admin.right;
}
@Mutation(() => Team, {
description:
'Create a new team by providing the user uid to nominate as Team owner',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async createTeamByAdmin(
@GqlAdmin() adminUser: Admin,
@Args({
name: 'userUid',
description: 'users uid to make team owner',
type: () => ID,
})
userUid: string,
@Args({ name: 'name', description: 'Displayed name of the team' })
name: string,
): Promise<Team> {
const createdTeam = await this.adminService.createATeam(userUid, name);
if (E.isLeft(createdTeam)) throwErr(createdTeam.left);
return createdTeam.right;
}
@Mutation(() => TeamMember, {
description: 'Change the role of a user in a team',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async changeUserRoleInTeamByAdmin(
@GqlAdmin() adminUser: Admin,
@Args() args: ChangeUserRoleInTeamArgs,
): Promise<TeamMember> {
const updatedRole = await this.adminService.changeRoleOfUserTeam(
args.userUID,
args.teamID,
args.newRole,
);
if (E.isLeft(updatedRole)) throwErr(updatedRole.left);
return updatedRole.right;
}
@Mutation(() => Boolean, {
description: 'Remove the user from a team',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async removeUserFromTeamByAdmin(
@GqlAdmin() adminUser: Admin,
@Args({
name: 'userUid',
description: 'users UID',
type: () => ID,
})
userUid: string,
@Args({
name: 'teamID',
description: 'team ID',
type: () => ID,
})
teamID: string,
): Promise<boolean> {
const removedUser = await this.adminService.removeUserFromTeam(
userUid,
teamID,
);
if (E.isLeft(removedUser)) throwErr(removedUser.left);
return removedUser.right;
}
@Mutation(() => TeamMember, {
description: 'Add a user to a team with email and team member role',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async addUserToTeamByAdmin(
@GqlAdmin() adminUser: Admin,
@Args() args: AddUserToTeamArgs,
): Promise<TeamMember> {
const addedUser = await this.adminService.addUserToTeam(
args.teamID,
args.userEmail,
args.role,
);
if (E.isLeft(addedUser)) throwErr(addedUser.left);
return addedUser.right;
}
@Mutation(() => Team, {
description: 'Change a team name',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async renameTeamByAdmin(
@GqlAdmin() adminUser: Admin,
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
teamID: string,
@Args({ name: 'newName', description: 'The updated name of the team' })
newName: string,
): Promise<Team> {
const renamedTeam = await this.adminService.renameATeam(teamID, newName);
if (E.isLeft(renamedTeam)) throwErr(renamedTeam.left);
return renamedTeam.right;
}
@Mutation(() => Boolean, {
description: 'Delete a team',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async deleteTeamByAdmin(
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
teamID: string,
): Promise<boolean> {
const deletedTeam = await this.adminService.deleteATeam(teamID);
if (E.isLeft(deletedTeam)) throwErr(deletedTeam.left);
return deletedTeam.right;
}
/* Subscriptions */
@Subscription(() => InvitedUser, {
description: 'Listen for User Invitation',
resolve: (value) => value,
})
@SkipThrottle()
@UseGuards(GqlAuthGuard, GqlAdminGuard)
userInvited(@GqlUser() admin: AuthUser) {
return this.pubsub.asyncIterator(`admin/${admin.uid}/invited`);
}
}

View File

@@ -0,0 +1,168 @@
import { AdminService } from './admin.service';
import { PubSubService } from '../pubsub/pubsub.service';
import { mockDeep } from 'jest-mock-extended';
import { InvitedUsers } from '@prisma/client';
import { UserService } from '../user/user.service';
import { TeamService } from '../team/team.service';
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
import { TeamRequestService } from '../team-request/team-request.service';
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
import { TeamCollectionService } from '../team-collection/team-collection.service';
import { MailerService } from '../mailer/mailer.service';
import { PrismaService } from 'src/prisma/prisma.service';
import {
DUPLICATE_EMAIL,
INVALID_EMAIL,
USER_ALREADY_INVITED,
} from '../errors';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
const mockUserService = mockDeep<UserService>();
const mockTeamService = mockDeep<TeamService>();
const mockTeamEnvironmentsService = mockDeep<TeamEnvironmentsService>();
const mockTeamRequestService = mockDeep<TeamRequestService>();
const mockTeamInvitationService = mockDeep<TeamInvitationService>();
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
const mockMailerService = mockDeep<MailerService>();
const adminService = new AdminService(
mockUserService,
mockTeamService,
mockTeamCollectionService,
mockTeamRequestService,
mockTeamEnvironmentsService,
mockTeamInvitationService,
mockPubSub as any,
mockPrisma as any,
mockMailerService,
);
const invitedUsers: InvitedUsers[] = [
{
adminUid: 'uid1',
adminEmail: 'admin1@example.com',
inviteeEmail: 'i@example.com',
invitedOn: new Date(),
},
{
adminUid: 'uid2',
adminEmail: 'admin2@example.com',
inviteeEmail: 'u@example.com',
invitedOn: new Date(),
},
];
describe('AdminService', () => {
describe('fetchInvitedUsers', () => {
test('should resolve right and return an array of invited users', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
const results = await adminService.fetchInvitedUsers();
expect(results).toEqual(invitedUsers);
});
test('should resolve left and return an empty array if invited users not found', async () => {
mockPrisma.invitedUsers.findMany.mockResolvedValue([]);
const results = await adminService.fetchInvitedUsers();
expect(results).toEqual([]);
});
});
describe('inviteUserToSignInViaEmail', () => {
test('should resolve right and create a invited user', async () => {
mockPrisma.invitedUsers.findFirst.mockResolvedValueOnce(null);
mockPrisma.invitedUsers.create.mockResolvedValueOnce(invitedUsers[0]);
const result = await adminService.inviteUserToSignInViaEmail(
invitedUsers[0].adminUid,
invitedUsers[0].adminEmail,
invitedUsers[0].inviteeEmail,
);
expect(mockPrisma.invitedUsers.create).toHaveBeenCalledWith({
data: {
adminUid: invitedUsers[0].adminUid,
adminEmail: invitedUsers[0].adminEmail,
inviteeEmail: invitedUsers[0].inviteeEmail,
},
});
return expect(result).toEqualRight(invitedUsers[0]);
});
test('should resolve right, create a invited user and publish a subscription', async () => {
mockPrisma.invitedUsers.findFirst.mockResolvedValueOnce(null);
mockPrisma.invitedUsers.create.mockResolvedValueOnce(invitedUsers[0]);
await adminService.inviteUserToSignInViaEmail(
invitedUsers[0].adminUid,
invitedUsers[0].adminEmail,
invitedUsers[0].inviteeEmail,
);
return expect(mockPubSub.publish).toHaveBeenCalledWith(
`admin/${invitedUsers[0].adminUid}/invited`,
invitedUsers[0],
);
});
test('should resolve left and return an error when invalid invitee email is passed', async () => {
const result = await adminService.inviteUserToSignInViaEmail(
invitedUsers[0].adminUid,
invitedUsers[0].adminEmail,
'invalidemail',
);
return expect(result).toEqualLeft(INVALID_EMAIL);
});
test('should resolve left and return an error when user already invited', async () => {
mockPrisma.invitedUsers.findFirst.mockResolvedValueOnce(invitedUsers[0]);
const result = await adminService.inviteUserToSignInViaEmail(
invitedUsers[0].adminUid,
invitedUsers[0].adminEmail,
invitedUsers[0].inviteeEmail,
);
return expect(result).toEqualLeft(USER_ALREADY_INVITED);
});
test('should resolve left and return an error when invitee and admin email is same', async () => {
const result = await adminService.inviteUserToSignInViaEmail(
invitedUsers[0].adminUid,
invitedUsers[0].inviteeEmail,
invitedUsers[0].inviteeEmail,
);
return expect(result).toEqualLeft(DUPLICATE_EMAIL);
});
});
describe('getUsersCount', () => {
test('should return count of all users in the organization', async () => {
mockUserService.getUsersCount.mockResolvedValueOnce(10);
const result = await adminService.getUsersCount();
expect(result).toEqual(10);
});
});
describe('getTeamsCount', () => {
test('should return count of all teams in the organization', async () => {
mockTeamService.getTeamsCount.mockResolvedValueOnce(10);
const result = await adminService.getTeamsCount();
expect(result).toEqual(10);
});
});
describe('getTeamCollectionsCount', () => {
test('should return count of all Team Collections in the organization', async () => {
mockTeamCollectionService.getTeamCollectionsCount.mockResolvedValueOnce(
10,
);
const result = await adminService.getTeamCollectionsCount();
expect(result).toEqual(10);
});
});
describe('getTeamRequestsCount', () => {
test('should return count of all Team Collections in the organization', async () => {
mockTeamRequestService.getTeamRequestsCount.mockResolvedValueOnce(10);
const result = await adminService.getTeamRequestsCount();
expect(result).toEqual(10);
});
});
});

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