Compare commits

..

195 Commits

Author SHA1 Message Date
nivedin
01200c9e5f fix: duplicate tab reference bug 2023-09-18 11:11:16 +05:30
Yuri Grand
75193a7aa8 i18n: translate locales to russian (#3312)
chore: translate locales to russian
2023-09-13 12:53:10 +05:30
tyo
b269c239d9 chore(i18n): update translation for Indonesian (#3284) 2023-09-13 12:48:46 +05:30
Liyas Thomas
72b4a1fc4e fix: typo in "twitter link" and "invite to hoppscotch" action (#3346) 2023-09-13 11:55:58 +05:30
DNT
d2d1674d31 i18n: update vi.json (#3241) 2023-09-13 11:52:37 +05:30
Joel Jacob Stephen
a6b57777e3 refactor: remove font sizes from the app (#3341)
* refactor: remove font size from settings

* refactor: remove font size from themes

* refactor: remove font size from spotlight

* refactor: remove default font size

* chore: clean up

---------

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-09-13 11:45:38 +05:30
Joel Jacob Stephen
65ef4db86f refactor: remove zen mode from the app (#3337)
* refactor: remove zen mode from settings

* refactor: remove zen mode from footer and options
2023-09-12 14:10:38 +05:30
Nivedin
7201147b55 fix: context-menu position fixed while scrolling (#3340) 2023-09-12 12:43:10 +05:30
Anwarul Islam
dd143c95a9 fix: unusual behavior while scrolling through spotlight entries (#3324)
* fix: spotlight scroll issue

* fix: entry hidden issue

* chore: back to loop mode
2023-09-12 12:42:44 +05:30
James George
005581ee7d fix: broken link to REST API Testing docs (#3333)
fix: broken link to REST API Testing docs
2023-09-12 12:32:10 +05:30
Joel Jacob Stephen
1431ecc6d7 refactor: keyboard shortcuts now supports different keyboard layouts including Dvorak (#3332)
* refactor: support mulitple keyboard layouts such as dvorak

* chore: replace redundant variable usage
2023-09-08 22:02:39 +05:30
Liyas Thomas
f34d896095 docs: updated screenshots and features list (#3310) 2023-09-05 12:06:47 +05:30
Andrew Bastin
e95ebb9226 chore: add release tag ci pipeline to push to docker hub 2023-08-31 15:49:32 +05:30
Andrew Bastin
57365eeae0 chore: bump version to 2023.8.0 2023-08-31 13:55:36 +05:30
Joel Jacob Stephen
b22bd97818 style: updated font size and truncation on fields in the invited users table in admin dashboard (#3300)
style: updated font size and fixed truncation issue on invited table
2023-08-28 23:27:55 +05:30
Anwarul Islam
b953b32ff4 fix: spotlight actions on graphql (#3299)
* fix: spotlight actions for graphql

* fix: environment actions

* fix: gql rename request

* fix: graphql spotlight actions

* fix: tab shortcuts not working properly

* fix: only show download and copy response when there is a response

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-28 20:40:01 +05:30
Liyas Thomas
0eacd6763b chore: improved command labels and icons (#3295)
* chore: improved command labels and icons

* chore: fix tests

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-28 18:15:00 +05:30
Anwarul Islam
8499ac7fec fix: graphql operation highlight on focus changed (#3297) 2023-08-28 17:55:42 +05:30
Nivedin
4adac4af38 fix: inspections bugs (#3277)
* fix: environment add bug in inspection

* chore: add 127.0.0.1 in url inspection

* chore: update browserextension inspection help url

* fix: team env not showing bug in selector

* chore: rework inspector systems to be reactive

* chore: handling tab changes gracefully

* refactor: move out url interceptor from the platform

* chore: add view function in inspector service to get views into the list

* fix: interceptors not kicking in on initial load

* fix: don't show no internet connection error unless browser deems so

* chore: fix tests

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-28 17:43:46 +05:30
Akash K
fd162e242c fix: issues with codegen (#3293)
* fix: fix issues with httpsnippet upgrade

* chore: fix HttpSnippet import
2023-08-28 15:57:44 +05:30
Andrew Bastin
3e83828722 chore: correct spelling for footer custom entries 2023-08-26 04:43:34 +05:30
Andrew Bastin
f7dc36e3f1 fix: correct typo 'additionalFooterMenuItems' 2023-08-26 03:09:11 +05:30
Andrew Bastin
a7566dfd86 feat: move crisp out of common (#3287)
* feat: move crisp out of common

* fix: update static spotlight searcher

* chore: fix typo
2023-08-26 03:00:58 +05:30
Mir Arif Hasan
d4d7a20fbd HBE-258 hotfix: skip parameter in findMany in shortcode module (#3294)
fix: skip parameter in findMany
2023-08-26 01:35:51 +05:30
Andrew Bastin
dfb281bcf7 chore: update prod.Dockerfile to add step for the backend container to not copy .env in 2023-08-25 21:03:00 +05:30
Andrew Bastin
c62482e81f fix: login component in app not respecting allowed auth provider ids 2023-08-25 19:13:03 +05:30
Anwarul Islam
886847ab7b fix: corrections for spotlight searchers (#3275) 2023-08-25 01:44:29 +05:30
Nivedin
a268cab11e fix: context menu bugs (#3279) 2023-08-25 00:27:03 +05:30
Nivedin
e9509b9fa1 fix: tab right click rename bug (#3286) 2023-08-25 00:20:08 +05:30
Liyas Thomas
8db452089c fix: icons inside tooltip (#3283) 2023-08-24 23:55:22 +05:30
Andrew Bastin
a1764023f3 fix: sh-admin not properly loading in env variables 2023-08-24 20:41:46 +05:30
Andrew Bastin
b08b63dc73 feat: cleaner save context handling for graphql (#3282)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-24 19:07:17 +05:30
Nivedin
a9a4ebf595 fix: autocomplete bug (#3285)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-24 18:46:58 +05:30
Andrew Bastin
a8e279db28 fix: codegen breaking 2023-08-24 15:03:50 +05:30
Andrew Bastin
d09a3e9237 fix: import-meta-env crashes while using dev mode 2023-08-24 09:43:52 +05:30
Andrew Bastin
efa40cf6ea feat: container registry friendly docker images and all-in-one container (#3193)
Co-authored-by: Balu Babu <balub997@gmail.com>
2023-08-24 00:01:28 +05:30
Akash K
1a3d9f18ab fix: issues in displaying the suggestions menu on EnvInput (#3280) 2023-08-23 21:18:48 +05:30
Akash K
653ccd3240 fix: vertical scroll not working on codemirror instance when lines are not wrapped (#3276)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-23 18:14:12 +05:30
Anwarul Islam
c0806cfd07 chore: removed unnecessary dependencies from hoppscotch-ui (#3077) 2023-08-23 18:13:19 +05:30
Liyas Thomas
008eb6b77b chore: minor ui improvements (#3274) 2023-08-22 22:22:43 +05:30
Joel Jacob Stephen
ac60843183 refactor: autofocus can be disabled in smart input hopp ui component (#3273)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-22 20:10:41 +05:30
Anwarul Islam
3c3fb1e4a9 fix: minor spotlight related issues (#3271)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-22 17:58:32 +05:30
Anwarul Islam
88212e8cfe feat: gql revamp (#2644)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-22 17:43:43 +05:30
Andrew Bastin
191fa376d2 chore: remove go to docs search entry and update i18n 2023-08-22 01:09:48 +05:30
Andrew Bastin
6efae3a395 fix: crash when closing tab (fixes HFE-146) 2023-08-22 01:07:16 +05:30
Andrew Bastin
cb8678f07f fix: codegen modal breaking, downgrade back to 2.0 2023-08-22 00:49:13 +05:30
Andrew Bastin
b32b0f9bcb fix: modals inputs not working properly 2023-08-22 00:17:03 +05:30
Anwarul Islam
5a91fb53b2 feat: expanded search capabilities of spotlight (#3255)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-21 20:03:51 +05:30
Liyas Thomas
b0b6edc58e fix: search input autofocus (#3265) 2023-08-21 14:58:44 +05:30
Akash K
8c57d81718 chore: bump dependencies (#3258)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-21 09:06:30 +05:30
Andrew Bastin
10bb68a538 refactor: move from network strategies to generic interceptor service (#3242) 2023-08-21 07:50:35 +05:30
Anwarul Islam
d4d1e27ba9 feat: smart-tree component added to hoppscotch-ui (#3210) 2023-08-20 20:48:32 +05:30
Liyas Thomas
d5c887f311 fix: placeholder size and text overflow on tab head (#3261) 2023-08-18 21:32:10 +05:30
Akash K
ce7adf6da3 feat: load allowed login methods from .env (#3264)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-18 21:23:56 +05:30
Andrew Bastin
c626fb9241 feat: spotlight collection searcher (#3262)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-18 20:56:45 +05:30
Nivedin
f21ed30e10 feat: inspections (#3213)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-18 01:37:21 +05:30
Anwarul Islam
b55970cc7a spotlight: settings based actions added (#3244)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-17 22:04:58 +05:30
Joel Jacob Stephen
74ad2e43a4 feat: ability to conditionally enable auth providers in dashboard (#3225) 2023-08-17 21:53:34 +05:30
Liyas Thomas
2d6282cf8b refactor: polish environment selector (#3260) 2023-08-17 16:46:45 +05:30
Liyas Thomas
e255c46455 feat: environment quick peek (#3119) 2023-08-15 19:30:37 +05:30
Andrew Bastin
15c2c7bb5b feat: divider for the additional platform login items 2023-08-15 16:15:03 +05:30
Andrew Bastin
71bcd22444 feat: allow platforms to define additional entries in the login dialog 2023-08-14 22:18:37 +05:30
Joel Jacob Stephen
2d104160f2 refactor: revoke team invitation in admin dashboard (#3232) 2023-08-14 17:37:05 +05:30
Anwarul Islam
f7c1825de5 spotlight: navigation searcher added (#3245)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-12 22:21:45 +05:30
Mir Arif Hasan
2c1fd5d711 feat: prefix VITE_ added in conditional auth provider env variable (#3246) (HBE-248) 2023-08-08 14:16:13 +05:30
Nivedin
085fbb2a9b feat: tippy menu for history and tab (#3220)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-08 13:23:11 +05:30
Andrew Bastin
05f2d8817b chore: merge hoppscotch/main into release/2023.8.0 2023-08-08 12:18:01 +05:30
Joel Jacob Stephen
81fbb22c51 feat: introducing a new smart input hoppscotch ui component (#3089)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-08-05 23:45:02 +05:30
Liyas Thomas
01cf59c663 fix: disable line wrapping in EnvInput component (#3230) 2023-08-05 23:43:02 +05:30
Liyas Thomas
5c8ebaff3e refactor: fonts are now bundled with packages (#3227) 2023-08-05 23:42:31 +05:30
Andrew Bastin
0e70c28324 feat: dynamically select which auth providers for your instance of hoppscotch (be implementation) 2023-08-03 20:12:54 +05:30
Ankit Sridhar
88f6a4ae26 [feat] : Allow admins to revoke a team invite (HBE-230) (#3162)
feat: added functionality for admin to revoke team invite
2023-08-03 14:08:32 +05:30
Anwarul Islam
610538ca02 chore: type and ux improvement for SmartTree (#3126) 2023-08-02 20:54:02 +05:30
Nivedin
8970ff5c68 feat: context menu (#3180)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-02 20:52:16 +05:30
Liyas Thomas
d1a564d5b8 fix: elastic overscroll on safari (#3221) 2023-08-02 20:47:54 +05:30
Anwarul Islam
8bb1d19c07 fix: firefox browser scrollbar issue (#3201) 2023-08-01 13:20:17 +05:30
Liyas Thomas
c1efa381f0 feat: svg badge asset (#3196) 2023-07-25 20:45:22 +05:30
Andrew Bastin
29171d1b6f fix: generate-ui failing to build 2023-07-18 22:27:37 +05:30
Andrew Bastin
e869d49e16 chore: run tests on and against release branches 2023-07-18 21:46:38 +05:30
Andrew Bastin
6496bea846 chore: bump version to 2023.4.8 2023-07-18 21:46:36 +05:30
NicklasWallgren
39842559b5 fix: reduce the memory consumption during build to prevent OOM (#3148)
Co-authored-by: Nicklas Wallgren <nicklas.wallgren@folksam.se>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-07-18 00:08:06 +05:30
Anwarul Islam
51efb35aa6 fix: keybinding modifier issue (#3163) 2023-07-17 23:56:08 +05:30
NicklasWallgren
9402bb9285 fix: add healthcheck for db and remove unwanted volumes (#3150) 2023-07-17 21:22:56 +05:30
Liyas Thomas
5a516f7242 docs: fixed shortcut keys for spotlight and shortcuts menu (#3192) 2023-07-17 19:27:49 +05:30
Liyas Thomas
3b217d78e7 fix: deps mismatch for vite-plugin-pages-sitemap (#3191) 2023-07-17 19:26:43 +05:30
Liyas Thomas
8e153b38dc redesigned search button (#3187) 2023-07-17 14:40:14 +05:30
Akash K
6f38bfb148 chore: update generateSitemap usage (#3182) 2023-07-17 14:39:32 +05:30
Balu Babu
82b6e08d68 fix: fixed issue in team-environment test cases (#3189) 2023-07-17 12:33:11 +05:30
Liyas Thomas
31fd6567b7 fix: text overflow on spotlight search results (#3181) 2023-07-17 12:32:45 +05:30
Anwarul Islam
25177bd635 fix: update vite-plugin-dts version which fixes build issue on docker/alpine (#3179) 2023-07-17 12:32:25 +05:30
5idereal
6928eb7992 feat(lang): update tw translation (#3170) 2023-07-14 11:36:08 +05:30
Andrew Bastin
8300f9a0a2 chore: merge release/2023.4.8 into release/2023.8.0 2023-07-13 12:10:14 +05:30
Mir Arif Hasan
525ba77739 refactor: team invitation module in pseudo fp-ts (#3175) 2023-07-13 11:58:03 +05:30
Balu Babu
6bc748a267 refactor: introduce team-environments into self-host refactored to pseudo-fp format (#3177) 2023-07-13 11:52:19 +05:30
Andrew Bastin
5230d2d3b8 feat: revamped spotlight (#3171)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-07-11 23:02:33 +05:30
Nivedin
c3531c9d8b feat: auto-complete recent history entries in URL bar (#3141)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-07-11 20:03:42 +05:30
Andrew Bastin
b29c04c28d fix: email not being checked case insensitive on team invitation acceptance (#3174) 2023-07-11 20:03:08 +05:30
Liyas Thomas
b2af353941 chore: new filled star icon to toggle favorite history entry (#3164) 2023-07-06 13:30:38 +05:30
Andrew Bastin
9dbce74f5e chore: bump version to 2023.4.7 2023-06-27 15:43:03 +05:30
Liyas Thomas
db1cf5cc08 fix: explicitly added background color 2023-06-27 15:43:03 +05:30
Liyas Thomas
09360abf81 fix: text overflow on details summary label (#3160)
Co-authored-by: Nivedin <nivedinp@gmail.com>
2023-06-27 15:42:58 +05:30
Andrew Bastin
355bd62b8d feat: introduce more events into the analytics pipeline (#3156) 2023-06-27 15:37:25 +05:30
James Butler
5650de1183 fix: self-host unable to use Azure oauth (#3138) 2023-06-27 15:37:25 +05:30
Akash K
2ee8614b93 fix: use --location param for url when parsing curl (#3152) 2023-06-27 15:37:25 +05:30
Ankit Sridhar
5632334c9a fix: remove existing team invitation for an invitee when adding invitee to team by admin (HBE-229) (#3157) 2023-06-27 15:37:25 +05:30
Anwarul Islam
780dd8a713 fix: graphql authorization headers (#3136) 2023-06-27 15:37:25 +05:30
Nivedin
7db3c6d290 fix: unified bg color in collection tree (#3155) 2023-06-27 15:37:25 +05:30
Omer Baflah
c765270dfe fix: correct typos (#3153) 2023-06-27 15:37:25 +05:30
Webysther Sperandio
03f667c21d feat: custom location on admin redirect to base (#3103) 2023-06-27 15:37:25 +05:30
Balázs Úr
f79f3078dc chore(i18n): updated hungarian translation (#3151) 2023-06-27 15:37:25 +05:30
Nivedin
6e29a2f6d4 fix: shortcode resolution screen is stuck on invalid shortcodes (#3142)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-06-27 15:37:25 +05:30
Balu Babu
6304fd50c3 fix: fixed issue with team-invitations and new user accounts (#3137) 2023-06-27 15:37:25 +05:30
Andrew Bastin
2ec29c47ad chore: merge release/2023.4.7 into main 2023-06-27 14:17:26 +05:30
Andrew Bastin
399a238bf4 chore: bump version to 2023.4.7 2023-06-27 14:15:12 +05:30
Liyas Thomas
b20ab72298 fix: explicitly added background color 2023-06-26 19:57:43 +05:30
Liyas Thomas
f723e6496a fix: text overflow on details summary label (#3160)
Co-authored-by: Nivedin <nivedinp@gmail.com>
2023-06-26 18:30:25 +05:30
Andrew Bastin
8c0aff8863 feat: introduce more events into the analytics pipeline (#3156) 2023-06-24 10:18:35 +05:30
James Butler
64c5077506 fix: self-host unable to use Azure oauth (#3138) 2023-06-22 23:43:05 +05:30
Akash K
2afc87847d fix: use --location param for url when parsing curl (#3152) 2023-06-22 23:40:09 +05:30
Ankit Sridhar
878ec833ce fix: remove existing team invitation for an invitee when adding invitee to team by admin (HBE-229) (#3157) 2023-06-22 23:38:02 +05:30
Anwarul Islam
039de8015f fix: graphql authorization headers (#3136) 2023-06-22 23:32:23 +05:30
Nivedin
f67b366b90 fix: unified bg color in collection tree (#3155) 2023-06-22 00:38:28 +05:30
Andrew Bastin
6f35574d68 refactor: move hoppscotch-common tests to vitest (#3154) 2023-06-22 00:36:25 +05:30
Omer Baflah
77e8a36ab0 fix: correct typos (#3153) 2023-06-22 00:35:57 +05:30
Webysther Sperandio
d7cc9f5dbc feat: custom location on admin redirect to base (#3103) 2023-06-21 00:13:40 +05:30
Anwarul Islam
fc3e3aeaec feat: placeholder component in hoppscotch-ui (#3123)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-06-21 00:09:16 +05:30
Balázs Úr
4ba135f3b9 chore(i18n): updated hungarian translation (#3151) 2023-06-20 14:28:53 +05:30
Nivedin
24894e05dc fix: shortcode resolution screen is stuck on invalid shortcodes (#3142)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-06-19 14:04:07 +05:30
Andrew Bastin
e2b668bee2 chore(ci): add manual workflow dispatch for hoppscotch-ui deploy script 2023-06-19 12:33:52 +05:30
Andrew Bastin
f112c46bb4 chore(ci): re-introduce hoppscotch-ui deploy script 2023-06-19 11:51:14 +05:30
Joel Jacob Stephen
331d482b22 feat: introducing i18n support to admin dashboard (#3051) 2023-06-16 09:47:00 +05:30
Andrew Bastin
b07243f131 chore: merge main@2023.4.6 into release/2023.8.0 2023-06-16 09:45:05 +05:30
Balu Babu
84b0c30d64 fix: fixed issue with team-invitations and new user accounts (#3137) 2023-06-15 17:15:06 +05:30
Andrew Bastin
e3dd9e99a1 chore: bump version to 2023.4.6 2023-06-12 10:43:44 +05:30
Hoai-Thu Vuong
e3091cb6db chore(i18n): fix typo in translation of clear_all (#3133) 2023-06-12 10:31:58 +05:30
Akash K
270f796683 fix: fix url getting overridden when query params are present (#3130) 2023-06-09 21:53:55 +05:30
Anwarul Islam
24c6bce02d fix: failed to execute 'observe' on 'IntersectionObserver' (#3122) 2023-06-09 09:40:09 +05:30
Anwarul Islam
2db567589f fix: collection request name edit issue (#3115)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Nivedin <nivedinp@gmail.com>
2023-06-09 09:36:41 +05:30
Liyas Thomas
1fe83ebdc8 chore: updated i18n strings (#3106) 2023-06-07 23:59:04 +05:30
islamzeki
8320d4f222 chore(i18n): update tr.json 2023-06-07 23:56:49 +05:30
Liyas Thomas
e76c1bc64c fix: stack order of tab inside environment selector (#3108) 2023-06-07 23:47:24 +05:30
Nivedin
1f3f8464ea fix: team environment lost when route changes (#3113)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-06-07 23:46:09 +05:30
Andrew Bastin
81a7e23a12 feat: introduce dioc into hoppscotch-common 2023-06-07 15:20:49 +05:30
Liyas Thomas
e75391cdf1 chore: updated icon with correct size (#3105) 2023-06-04 23:46:47 -04:00
Andrew Bastin
a213c0c26c chore: bump version to 2023.4.5 2023-06-04 23:41:01 -04:00
Andrew Bastin
15424903ed fix: stop logging DATABASE_URL in logs 2023-06-04 23:33:32 -04:00
Andrew Bastin
1cce117b0a chore: bump version to 2023.4.4 2023-06-02 11:06:51 -04:00
Liyas Thomas
abc7b4b6f3 chore: improve mobile responsiveness on environment selector (#3100) 2023-06-02 10:56:18 -04:00
Ankit Sridhar
05e32ef9e4 fix: update team invitation link to domain specified in .env [HBE-202] (#3096) 2023-05-31 10:36:34 -04:00
Nivedin
f0a1fc319c fix: sync popup firing multiple times (#3063) 2023-05-30 23:36:37 -04:00
Allen Zhang
385cabc6aa fix: update package.json script (#3083) 2023-05-30 17:50:47 -04:00
Liyas Thomas
397b26a9f3 chore: environment selector with new ux (#3052)
Co-authored-by: Nivedin <nivedinp@gmail.com>
2023-05-30 17:47:37 -04:00
Nivedin
9a40058329 fix: set team environment from test (#3059) 2023-05-30 17:38:28 -04:00
Akash K
7ec2380ed5 chore: update wss url to ws in .env.example (#3081) 2023-05-29 20:23:02 -04:00
安正超
3d4825305d chore(i18n): Update zh-CN translations (#3068) 2023-05-29 20:19:43 -04:00
Nivedin
26e564288b feat: prettify XML response (#3079) 2023-05-29 20:18:19 -04:00
Sawako
385a587cfd feat(locales): fix and update es (spanish) locale (#3086) 2023-05-29 20:15:39 -04:00
Liyas Thomas
215df02783 chore: make style sheets consistent (#3074) 2023-05-29 20:12:58 -04:00
Liyas Thomas
7c7ed68b20 fix: 403 forbidden error when trying to load profile picture (#3045)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com>
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
fix: pane layout broken when wrap line is off (#3027)
Fix issue with disappearing tab when opening request tabs with long text in body/script (#3030)
2023-05-24 16:23:44 -04:00
Anwarul Islam
c910a0314a feat: rename request by double clicking its name on tabs (#3057)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-05-24 16:18:19 -04:00
Liyas Thomas
ddaec1b9ac feat: add support to audio and video API responses (#3044) 2023-05-24 16:16:14 -04:00
Anwarul Islam
9dbdef9286 fix: dead key issue in mac (#3058) 2023-05-23 16:41:37 -04:00
Bart Kerkvliet
e77eef1532 Fix typo, rename cuttentTime to currentTime (#3053) 2023-05-23 16:38:01 -04:00
Liyas Thomas
1fe0b8861d fix: don't cut off the part that's already been typed (#3054) 2023-05-23 16:36:02 -04:00
Andrew Bastin
aeb9172144 fix: analytics logging behavior being incorrect (#3064) 2023-05-23 16:34:28 -04:00
Mir Arif Hasan
1b413e2f47 fix: timing dependency on test case (#3070) 2023-05-23 16:32:39 -04:00
Andrew Bastin
d6c8400116 chore: bump version to 2023.4.3 2023-05-11 17:05:28 +05:30
Andrew Bastin
4a0205e622 fix: environment section being broken 2023-05-11 16:34:57 +05:30
Andrew Bastin
c2520006ac chore: bump version to 2023.4.2 2023-05-11 14:09:38 +05:30
Nivedin
99817fd8bd fix: reset envs when user switches workspaces (#3039)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-05-11 14:09:38 +05:30
Anwarul Islam
3f35fedd9d fix: tab system breaks when a new tab is created while waiting for response in another tab (#3031) 2023-05-11 14:09:38 +05:30
Akash K
b7c2d13992 fix: invalid environment index can break the app (#3041) 2023-05-11 14:09:38 +05:30
Akash K
a6426587fb chore: add onCodemirrorInstanceMount hook to platform (#3043) 2023-05-11 14:09:38 +05:30
Anwarul Islam
5f68356278 feat: scroll to show the new active tab header (#3013)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-05-11 14:09:38 +05:30
Mir Arif Hasan
08f61e7408 fix: magic link URL (#3028) 2023-05-11 14:09:38 +05:30
Mir Arif Hasan
9beda15f00 fix: returning response from authCookieHandler (#3025) 2023-05-11 14:09:38 +05:30
Anwarul Islam
09d1663f81 feat: picture component moved to hoppscotch-ui (#3032) 2023-05-11 14:09:38 +05:30
Anwarul Islam
f43b6e7cff Fix issue with disappearing tab when opening request tabs with long text in body/script (#3030)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-05-11 14:09:38 +05:30
Akash K
6581eb4fd1 fix: update the hoppscotch-sh-admin magic link route to match hoppscotch-app (#3029) 2023-05-11 14:09:38 +05:30
Nivedin
caedfe5c1e fix: pane layout broken when wrap line is off (#3027)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-05-11 14:09:38 +05:30
Liyas Thomas
f6a234aaf9 docs: updated screenshots (#3046) 2023-05-10 19:17:47 +05:30
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
474 changed files with 31079 additions and 14023 deletions

2
.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules
**/*/node_modules

View File

@@ -13,12 +13,13 @@ 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"
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
# Google Auth Config
GOOGLE_CLIENT_ID="************************************************"
GOOGLE_CLIENT_SECRET="************************************************"
GOOGLE_CALLBACK_URL="http://localhost:3170/v1/auth/google/callback"
GOOGLE_SCOPE="['email', 'profile'],"
GOOGLE_SCOPE="email,profile"
# Github Auth Config
GITHUB_CLIENT_ID="************************************************"
@@ -31,6 +32,7 @@ MICROSOFT_CLIENT_ID="************************************************"
MICROSOFT_CLIENT_SECRET="************************************************"
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
MICROSOFT_SCOPE="user.read"
MICROSOFT_TENANT="common"
# Mailer config
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
@@ -51,9 +53,9 @@ 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_WS_URL=ws://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/terms
VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/privacy
VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy

View File

@@ -0,0 +1,66 @@
name: "Push containers to Docker Hub on release"
on:
push:
tags:
- '*.*.*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup environment
run: cp .env.example .env
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push `${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: backend
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:${{ github.ref_name }}
- name: Build and push `${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: app
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:${{ github.ref_name }}
- name: Build and push `${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: sh_admin
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:${{ github.ref_name }}
- name: Build and push `${{ secrets.DOCKER_AIO_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: aio
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:${{ github.ref_name }}

View File

@@ -2,9 +2,9 @@ name: Node.js CI
on:
push:
branches: [main, staging]
branches: [main, staging, "release/**"]
pull_request:
branches: [main, staging]
branches: [main, staging, "release/**"]
jobs:
test:

42
.github/workflows/ui.yml vendored Normal file
View File

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

View File

@@ -1,3 +1,8 @@
module.exports = {
semi: false
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2
}

View File

@@ -6,8 +6,8 @@ We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
@@ -22,17 +22,17 @@ community include:
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
@@ -82,15 +82,15 @@ behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
@@ -106,23 +106,27 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

191
README.md
View File

@@ -2,23 +2,18 @@
<a href="https://hoppscotch.io">
<img
src="https://avatars.githubusercontent.com/u/56705483"
alt="Hoppscotch Logo"
alt="Hoppscotch"
height="64"
/>
</a>
<br />
<p>
<h3>
<b>
Hoppscotch
</b>
</h3>
</p>
<p>
<h3>
<b>
Open source API development ecosystem
Hoppscotch
</b>
</p>
</h3>
<b>
Open Source API Development Ecosystem
</b>
<p>
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen?logo=github)](CODE_OF_CONDUCT.md) [![Website](https://img.shields.io/website?url=https%3A%2F%2Fhoppscotch.io&logo=hoppscotch)](https://hoppscotch.io) [![Tests](https://github.com/hoppscotch/hoppscotch/actions/workflows/tests.yml/badge.svg)](https://github.com/hoppscotch/hoppscotch/actions) [![Tweet](https://img.shields.io/twitter/url?url=https%3A%2F%2Fhoppscotch.io%2F)](https://twitter.com/share?text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20Open%20source%20API%20development%20ecosystem%20-%20Helps%20you%20create%20requests%20faster,%20saving%20precious%20time%20on%20development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io)
@@ -34,23 +29,18 @@
</p>
<br />
<p>
<a href="https://hoppscotch.io/#gh-light-mode-only" target="_blank">
<img
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-common/public/images/banner-dark.png"
alt="Hoppscotch"
width="100%"
/>
<a href="https://hoppscotch.io">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/hoppscotch-common/public/images/banner-dark.png">
<source media="(prefers-color-scheme: light)" srcset="./packages/hoppscotch-common/public/images/banner-light.png">
<img alt="Hoppscotch" src="./packages/hoppscotch-common/public/images/banner-dark.png">
</picture>
</a>
</p>
</div>
_We highly recommend you take a look at the [**Hoppscotch Documentation**](https://docs.hoppscotch.io) to learn more about the app._
#### **Support**
[![Chat on Discord](https://img.shields.io/badge/chat-Discord-7289DA?logo=discord)](https://hoppscotch.io/discord) [![Chat on Telegram](https://img.shields.io/badge/chat-Telegram-2CA5E0?logo=telegram)](https://hoppscotch.io/telegram) [![Discuss on GitHub](https://img.shields.io/badge/discussions-GitHub-333333?logo=github)](https://github.com/hoppscotch/hoppscotch/discussions)
@@ -59,9 +49,9 @@
❤️ **Lightweight:** Crafted with minimalistic UI design.
⚡️ **Fast:** Send requests and get/copy responses in real-time.
⚡️ **Fast:** Send requests and get responses in real time.
**HTTP Methods**
🗄️ **HTTP Methods:** Request methods define the type of action you are requesting to be performed.
- `GET` - Requests retrieve resource information
- `POST` - The server creates a new entry in a database
@@ -74,17 +64,15 @@
- `TRACE` - Performs a message loop-back test along the path to the target resource
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
🌈 **Make it yours:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
🌈 **Theming:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
**Theming**
- Choose a theme: System (default), Light, Dark, and Black
- Choose accent color: Green (default), Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
- Choose a theme: System preference, Light, Dark, and Black
- Choose accent colors: Green, Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
- Distraction-free Zen mode
_Customized themes are synced with cloud / local session_
_Customized themes are synced with your cloud/local session._
🔥 **PWA:** Install as a [PWA](https://web.dev/what-are-pwas/) on your device.
🔥 **PWA:** Install as a [Progressive Web App](https://web.dev/progressive-web-apps) on your device.
- Instant loading with Service Workers
- Offline support
@@ -107,7 +95,7 @@ _Customized themes are synced with cloud / local session_
📡 **Server-Sent Events:** Receive a stream of updates from a server over an HTTP connection without resorting to polling.
🌩 **Socket.IO:** Send and Receive data with SocketIO server.
🌩 **Socket.IO:** Send and Receive data with the SocketIO server.
🦟 **MQTT:** Subscribe and Publish to topics of an MQTT Broker.
@@ -127,7 +115,7 @@ _Customized themes are synced with cloud / local session_
- OAuth 2.0
- OIDC Access Token/PKCE
📢 **Headers:** Describes the format the body of your request is being sent as.
📢 **Headers:** Describes the format the body of your request is being sent in.
📫 **Parameters:** Use request parameters to set varying parts in simulated requests.
@@ -137,14 +125,14 @@ _Customized themes are synced with cloud / local session_
- FormData, JSON, and many more
- Toggle between key-value and RAW input parameter list
👋 **Response:** Contains the status line, headers, and the message/response body.
📮 **Response:** Contains the status line, headers, and the message/response body.
- Copy response to clipboard
- Download response as a file
- Copy the response to the clipboard
- Download the response as a file
- View response headers
- View raw and preview of HTML, image, JSON, XML responses
- View raw and preview HTML, image, JSON, and XML responses
**History:** Request entries are synced with cloud / local session storage to restore with a single click.
**History:** Request entries are synced with your cloud/local session storage.
📁 **Collections:** Keep your API requests organized with collections and folders. Reuse them with a single click.
@@ -152,7 +140,32 @@ _Customized themes are synced with cloud / local session_
- Nested folders
- Export and import as a file or GitHub gist
_Collections are synced with cloud / local session storage_
_Collections are synced with your cloud/local session storage._
📜 **Pre-Request Scripts:** Snippets of code associated with a request that is executed before the request is sent.
- Set environment variables
- Include timestamp in the request headers
- Send a random alphanumeric string in the URL parameters
- Any JavaScript functions
👨‍👩‍👧‍👦 **Teams:** Helps you collaborate across your teams to design, develop, and test APIs faster.
- Create unlimited teams
- Create unlimited shared collections
- Create unlimited team members
- Role-based access control
- Cloud sync
- Multiple devices
👥 **Workspaces:** Organize your personal and team collections environments into workspaces. Easily switch between workspaces to manage multiple projects.
- Create unlimited workspaces
- Switch between personal and team workspaces
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/documentation/features/shortcuts)**
🌐 **Proxy:** Enable Proxy Mode from Settings to access blocked APIs.
@@ -161,60 +174,31 @@ _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)**_
📜 **Pre-Request Scripts β:** Snippets of code associated with a request that is executed before the request is sent.
- Set environment variables
- Include timestamp in the request headers
- Send a random alphanumeric string in the URL parameters
- Any JavaScript functions
📄 **API Documentation:** Create and share dynamic API documentation easily, quickly.
1. Add your requests to Collections and Folders
2. Export Collections and easily share your APIs with the rest of your team
3. Import Collections and Generate Documentation on-the-go
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/features/shortcuts)**
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/support/privacy)**._
🌎 **i18n:** Experience the app in your language.
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md), and the process for submitting pull requests to us.
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md) and the process for submitting pull requests to us.
📦 **Add-ons:** Official add-ons for hoppscotch.
☁️ **Auth + Sync:** Sign in and sync your data in real-time across all your devices.
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch
- **[CLI β](https://github.com/hoppscotch/hopp-cli)** - A CLI solution for Hoppscotch
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that simplifies access to Hoppscotch
[![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_16x16.png) **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) &nbsp;|&nbsp; [![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_16x16.png) **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
> **Extensions fixes `CORS` issues.**
- **[Hopp-Doc-Gen](https://github.com/hoppscotch/hopp-doc-gen)** - An API doc generator CLI for Hoppscotch
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
☁️ **Auth + Sync:** Sign in and sync your data in real-time.
**Sign in with**
**Sign in with:**
- GitHub
- Google
- Microsoft
- Email
- SSO (Single Sign-On)[^EE]
**Synchronize your data**
**🔄 Synchronize your data:** Handoff to continue tasks on your other devices.
- Workspaces
- History
- Collections
- Environments
- Settings
**Post-Request Tests β:** Write tests associated with a request that is executed after the request's response.
**Post-Request Tests:** Write tests associated with a request that is executed after the request's response.
- Check the status code as an integer
- Filter response headers
@@ -222,7 +206,7 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
- Set environment variables
- Write JavaScript code
🌱 **Environments** : Environment variables allow you to store and reuse values in your requests and scripts.
🌱 **Environments:** Environment variables allow you to store and reuse values in your requests and scripts.
- Unlimited environments and variables
- Initialize through the pre-request script
@@ -241,22 +225,31 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
</details>
👨‍👩‍👧‍👦 **Teams β:** Helps you collaborate across your team to design, develop, and test APIs faster.
- Unlimited teams
- Unlimited shared collections
- Unlimited team members
- Role-based access control
- Cloud sync
- Multiple devices
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
- Entries are separated by newline
- Keys and values are separated by `:`
- Prepend `#` to any row you want to add but keep disabled
**For more features, please read our [documentation](https://docs.hoppscotch.io).**
🎛️ **Admin dashboard:** Manage your team and invite members.
- Insights
- Manage users
- Manage teams
📦 **Add-ons:** Official add-ons for hoppscotch.
- **[Hoppscotch CLI](https://github.com/hoppscotch/hopp-cli)** - Command-line interface for Hoppscotch.
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch.
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that enhance your Hoppscotch experience.
[![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_16x16.png) **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) &nbsp;|&nbsp; [![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_16x16.png) **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
> **Extensions fix `CORS` issues.**
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
**For a complete list of features, please read our [documentation](https://docs.hoppscotch.io).**
## **Demo**
@@ -268,17 +261,9 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
2. Click "Send" to simulate the request
3. View the response
## **Built with**
- [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML)
- [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), [SCSS](https://sass-lang.com), [Windi CSS](https://windicss.org)
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [TypeScript](https://www.typescriptlang.org)
- [Vue](https://vuejs.org)
- [Vite](https://vitejs.dev)
## **Developing**
Follow the guide in the [Self Hosted Docs](https://docs.hoppscotch.io/documentation/self-host/getting-started).
Follow our [self-hosting documentation](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
## **Contributing**
@@ -296,7 +281,7 @@ See the [`CHANGELOG`](CHANGELOG.md) file for details.
## **Authors**
This project exists thanks to all the people who contribute — [contribute](CONTRIBUTING.md).
This project owes its existence to the collective efforts of all those who contribute — [contribute now](CONTRIBUTING.md).
<div align="center">
<a href="https://github.com/hoppscotch/hoppscotch/graphs/contributors">
@@ -308,4 +293,6 @@ This project exists thanks to all the people who contribute — [contribute](CON
## **License**
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) - see the [`LICENSE`](LICENSE) file for details.
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) see the [`LICENSE`](LICENSE) file for details.
[^EE]: Enterprise edition feature. [Learn more](https://docs.hoppscotch.io/documentation/self-host/getting-started).

View File

@@ -2,8 +2,9 @@
This document outlines security procedures and general policies for the Hoppscotch project.
1. [Reporting a security vulnerability](#reporting-a-security-vulnerability)
3. [Incident response process](#incident-response-process)
- [Security Policy](#security-policy)
- [Reporting a security vulnerability](#reporting-a-security-vulnerability)
- [Incident response process](#incident-response-process)
## Reporting a security vulnerability

View File

@@ -9,26 +9,24 @@ Before you start working on a new language, please look through the [open pull r
if there is no existing translation, you can create a new one by following these steps:
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`.**
2. **Checkout the `main` branch for latest translations.**
3. **Create a new branch for your translation with base branch `main`.**
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 [`/packages/hoppscotch-common/languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/languages.json).**
8. **Save & commit changes.**
8. **Save and commit changes.**
9. **Send a pull request.**
_You may send a pull request before all steps above are complete: e.g., you may want to ask for help with translations, or getting tests to pass. However, your pull request will not be merged until all steps above are complete._
`i18n` branch will be merged into `main` branch once every week.
Completing an initial translation of the whole site is a fairly large task. One way to break that task up is to work with other translators through pull requests on your fork. You can also [add collaborators to your fork](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/inviting-collaborators-to-a-personal-repository) if you'd like to invite other translators to commit directly to your fork and share responsibility for merging pull requests.
## Updating a translation
### Corrections
If you notice spelling or grammar errors, typos, or opportunities for better phrasing, open a pull request with your suggested fix. If you see a problem that you aren't sure of or don't have time to fix, open an issue.
If you notice spelling or grammar errors, typos, or opportunities for better phrasing, open a pull request with your suggested fix. If you see a problem that you aren't sure of or don't have time to fix, [open an issue](https://github.com/hoppscotch/hoppscotch/issues/new/choose).
### Broken links

11
aio.Caddyfile Normal file
View File

@@ -0,0 +1,11 @@
:3000 {
try_files {path} /
root * /site/selfhost-web
file_server
}
:3100 {
try_files {path} /
root * /site/sh-admin
file_server
}

72
aio_run.mjs Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/local/bin/node
// @ts-check
import { execSync, spawn } from "child_process"
import fs from "fs"
import process from "process"
function runChildProcessWithPrefix(command, args, prefix) {
const childProcess = spawn(command, args);
childProcess.stdout.on('data', (data) => {
const output = data.toString().trim().split('\n');
output.forEach((line) => {
console.log(`${prefix} | ${line}`);
});
});
childProcess.stderr.on('data', (data) => {
const error = data.toString().trim().split('\n');
error.forEach((line) => {
console.error(`${prefix} | ${line}`);
});
});
childProcess.on('close', (code) => {
console.log(`${prefix} Child process exited with code ${code}`);
});
childProcess.on('error', (stuff) => {
console.log("error")
console.log(stuff)
})
return childProcess
}
const envFileContent = Object.entries(process.env)
.filter(([env]) => env.startsWith("VITE_"))
.map(([env, val]) => `${env}=${
(val.startsWith("\"") && val.endsWith("\""))
? val
: `"${val}"`
}`)
.join("\n")
fs.writeFileSync("build.env", envFileContent)
execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
fs.rmSync("build.env")
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
caddyProcess.on("exit", (code) => {
console.log(`Exiting process because Caddy Server exited with code ${code}`)
process.exit(code)
})
backendProcess.on("exit", (code) => {
console.log(`Exiting process because Backend Server exited with code ${code}`)
process.exit(code)
})
process.on('SIGINT', () => {
console.log("SIGINT received, exiting...")
caddyProcess.kill("SIGINT")
backendProcess.kill("SIGINT")
process.exit(0)
})

View File

@@ -8,23 +8,25 @@ services:
hoppscotch-backend:
container_name: hoppscotch-backend
build:
dockerfile: packages/hoppscotch-backend/Dockerfile
dockerfile: prod.Dockerfile
context: .
target: prod
target: backend
env_file:
- ./.env
restart: always
environment:
# 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
- PORT=3170
volumes:
- ./packages/hoppscotch-backend/:/usr/src/app
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
# - ./packages/hoppscotch-backend/:/usr/src/app
- /usr/src/app/node_modules/
depends_on:
- hoppscotch-db
hoppscotch-db:
condition: service_healthy
ports:
- "3170:3000"
- "3170:3170"
# 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
@@ -32,8 +34,9 @@ services:
hoppscotch-app:
container_name: hoppscotch-app
build:
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
dockerfile: prod.Dockerfile
context: .
target: app
env_file:
- ./.env
depends_on:
@@ -47,8 +50,9 @@ services:
hoppscotch-sh-admin:
container_name: hoppscotch-sh-admin
build:
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
dockerfile: prod.Dockerfile
context: .
target: sh_admin
env_file:
- ./.env
depends_on:
@@ -56,16 +60,91 @@ services:
ports:
- "3100:8080"
# The service that spins up all 3 services at once in one container
hoppscotch-aio:
container_name: hoppscotch-aio
build:
dockerfile: prod.Dockerfile
context: .
target: aio
env_file:
- ./.env
depends_on:
hoppscotch-db:
condition: service_healthy
ports:
- "3000:3000"
- "3100:3100"
- "3170:3170"
# 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
image: postgres:15
ports:
- "5432:5432"
user: postgres
environment:
# The default user defined by the docker image
POSTGRES_USER: postgres
# NOTE: Please UPDATE THIS PASSWORD!
POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch
healthcheck:
test:
[
"CMD-SHELL",
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
]
interval: 5s
timeout: 5s
retries: 10
# All the services listed below are deprececated
hoppscotch-old-backend:
container_name: hoppscotch-old-backend
build:
dockerfile: packages/hoppscotch-backend/Dockerfile
context: .
target: prod
env_file:
- ./.env
restart: always
environment:
# 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:
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
# - ./packages/hoppscotch-backend/:/usr/src/app
- /usr/src/app/node_modules/
depends_on:
hoppscotch-db:
condition: service_healthy
ports:
- "3170:3000"
hoppscotch-old-app:
container_name: hoppscotch-old-app
build:
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
context: .
env_file:
- ./.env
depends_on:
- hoppscotch-old-backend
ports:
- "3000:8080"
hoppscotch-old-sh-admin:
container_name: hoppscotch-old-sh-admin
build:
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
context: .
env_file:
- ./.env
depends_on:
- hoppscotch-old-backend
ports:
- "3100:8080"

14
healthcheck.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
curlCheck() {
if ! curl -s --head "$1" | head -n 1 | grep -q "HTTP/1.[01] [23].."; then
echo "URL request failed!"
exit 1
else
echo "URL request succeeded!"
fi
}
curlCheck "http://localhost:3000"
curlCheck "http://localhost:3100"
curlCheck "http://localhost:3170/ping"

View File

@@ -11,7 +11,7 @@
"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": "http-server packages/hoppscotch-web/dist -p 3000",
"start": "http-server packages/hoppscotch-selfhost-web/dist -p 3000",
"lint": "pnpm -r do-lint",
"typecheck": "pnpm -r do-typecheck",
"lintfix": "pnpm -r do-lintfix",
@@ -32,5 +32,14 @@
"@types/node": "^17.0.24",
"cross-env": "^7.0.3",
"http-server": "^14.1.1"
},
"pnpm": {
"packageExtensions": {
"httpsnippet@^3.0.1": {
"peerDependencies": {
"ajv": "6.12.3"
}
}
}
}
}

View File

@@ -17,12 +17,12 @@
"types": "dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@codemirror/language": "^6.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.2.0"
"@codemirror/language": "^6.9.0",
"@lezer/highlight": "^1.1.6",
"@lezer/lr": "^1.3.10"
},
"devDependencies": {
"@lezer/generator": "^1.1.0",
"@lezer/generator": "^1.5.0",
"mocha": "^9.2.2",
"rollup": "^2.70.2",
"rollup-plugin-dts": "^4.2.1",

24
packages/dioc/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

141
packages/dioc/README.md Normal file
View File

@@ -0,0 +1,141 @@
# dioc
A small and lightweight dependency injection / inversion of control system.
### About
`dioc` is a really simple **DI/IOC** system where you write services (which are singletons per container) that can depend on each other and emit events that can be listened upon.
### Demo
```ts
import { Service, Container } from "dioc"
// Here is a simple service, which you can define by extending the Service class
// and providing an ID static field (of type string)
export class PersistenceService extends Service {
// This should be unique for each container
public static ID = "PERSISTENCE_SERVICE"
public read(key: string): string | undefined {
// ...
}
public write(key: string, value: string) {
// ...
}
}
type TodoServiceEvent =
| { type: "TODO_CREATED"; index: number }
| { type: "TODO_DELETED"; index: number }
// Services have a built in event system
// Define the generic argument to say what are the possible emitted values
export class TodoService extends Service<TodoServiceEvent> {
public static ID = "TODO_SERVICE"
// Inject persistence service into this service
private readonly persistence = this.bind(PersistenceService)
public todos = []
// Service constructors cannot have arguments
constructor() {
super()
this.todos = JSON.parse(this.persistence.read("todos") ?? "[]")
}
public addTodo(text: string) {
// ...
// You can access services via the bound fields
this.persistence.write("todos", JSON.stringify(this.todos))
// This is how you emit an event
this.emit({
type: "TODO_CREATED",
index,
})
}
public removeTodo(index: number) {
// ...
this.emit({
type: "TODO_DELETED",
index,
})
}
}
// Services need a container to run in
const container = new Container()
// You can initialize and get services using Container#bind
// It will automatically initialize the service (and its dependencies)
const todoService = container.bind(TodoService) // Returns an instance of TodoService
```
### Demo (Unit Test)
`dioc/testing` contains `TestContainer` which lets you bind mocked services to the container.
```ts
import { TestContainer } from "dioc/testing"
import { TodoService, PersistenceService } from "./demo.ts" // The above demo code snippet
import { describe, it, expect, vi } from "vitest"
describe("TodoService", () => {
it("addTodo writes to persistence", () => {
const container = new TestContainer()
const writeFn = vi.fn()
// The first parameter is the service to mock and the second parameter
// is the mocked service fields and functions
container.bindMock(PersistenceService, {
read: () => undefined, // Not really important for this test
write: writeFn,
})
// the peristence service bind in TodoService will now use the
// above defined mocked implementation
const todoService = container.bind(TodoService)
todoService.addTodo("sup")
expect(writeFn).toHaveBeenCalledOnce()
expect(writeFn).toHaveBeenCalledWith("todos", JSON.stringify(["sup"]))
})
})
```
### Demo (Vue)
`dioc/vue` contains a Vue Plugin and a `useService` composable that allows Vue components to use the defined services.
In the app entry point:
```ts
import { createApp } from "vue"
import { diocPlugin } from "dioc/vue"
const app = createApp()
app.use(diocPlugin, {
container: new Container(), // You can pass in the container you want to provide to the components here
})
```
In your Vue components:
```vue
<script setup>
import { TodoService } from "./demo.ts" // The above demo
import { useService } from "dioc/vue"
const todoService = useService(TodoService) // Returns an instance of the TodoService class
</script>
```

2
packages/dioc/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export { default } from "./dist/main.d.ts"
export * from "./dist/main.d.ts"

View File

@@ -0,0 +1,147 @@
import { Service } from "./service"
import { Observable, Subject } from 'rxjs'
/**
* Stores the current container instance in the current operating context.
*
* NOTE: This should not be used outside of dioc library code
*/
export let currentContainer: Container | null = null
/**
* The events emitted by the container
*
* `SERVICE_BIND` - emitted when a service is bound to the container directly or as a dependency to another service
* `SERVICE_INIT` - emitted when a service is initialized
*/
export type ContainerEvent =
| {
type: 'SERVICE_BIND';
/** The Service ID of the service being bounded (the dependency) */
boundeeID: string;
/**
* The Service ID of the bounder that is binding the boundee (the dependent)
*
* NOTE: This will be undefined if the service is bound directly to the container
*/
bounderID: string | undefined
}
| {
type: 'SERVICE_INIT';
/** The Service ID of the service being initialized */
serviceID: string
}
/**
* The dependency injection container, allows for services to be initialized and maintains the dependency trees.
*/
export class Container {
/** Used during the `bind` operation to detect circular dependencies */
private bindStack: string[] = []
/** The map of bound services to their IDs */
protected boundMap = new Map<string, Service<unknown>>()
/** The RxJS observable representing the event stream */
protected event$ = new Subject<ContainerEvent>()
/**
* Returns whether a container has the given service bound
* @param service The service to check for
*/
public hasBound<
T extends typeof Service<any> & { ID: string }
>(service: T): boolean {
return this.boundMap.has(service.ID)
}
/**
* Returns the service bound to the container with the given ID or if not found, undefined.
*
* NOTE: This is an advanced method and should not be used as much as possible.
*
* @param serviceID The ID of the service to get
*/
public getBoundServiceWithID(serviceID: string): Service<unknown> | undefined {
return this.boundMap.get(serviceID)
}
/**
* Binds a service to the container. This is equivalent to marking a service as a dependency.
* @param service The class reference of a service to bind
* @param bounder The class reference of the service that is binding the service (if bound directly to the container, this should be undefined)
*/
public bind<T extends typeof Service<any> & { ID: string }>(
service: T,
bounder: ((typeof Service<T>) & { ID: string }) | undefined = undefined
): InstanceType<T> {
// We need to store the current container in a variable so that we can restore it after the bind operation
const oldCurrentContainer = currentContainer;
currentContainer = this;
// If the service is already bound, return the existing instance
if (this.hasBound(service)) {
this.event$.next({
type: 'SERVICE_BIND',
boundeeID: service.ID,
bounderID: bounder?.ID // Return the bounder ID if it is defined, else assume its the container
})
return this.boundMap.get(service.ID) as InstanceType<T> // Casted as InstanceType<T> because service IDs and types are expected to match
}
// Detect circular dependency and throw error
if (this.bindStack.findIndex((serviceID) => serviceID === service.ID) !== -1) {
const circularServices = `${this.bindStack.join(' -> ')} -> ${service.ID}`
throw new Error(`Circular dependency detected.\nChain: ${circularServices}`)
}
// Push the service ID onto the bind stack to detect circular dependencies
this.bindStack.push(service.ID)
// Initialize the service and emit events
// NOTE: We need to cast the service to any as TypeScript thinks that the service is abstract
const instance: Service<any> = new (service as any)()
this.boundMap.set(service.ID, instance)
this.bindStack.pop()
this.event$.next({
type: 'SERVICE_INIT',
serviceID: service.ID,
})
this.event$.next({
type: 'SERVICE_BIND',
boundeeID: service.ID,
bounderID: bounder?.ID
})
// Restore the current container
currentContainer = oldCurrentContainer;
// We expect the return type to match the service definition
return instance as InstanceType<T>
}
/**
* Returns an iterator of the currently bound service IDs and their instances
*/
public getBoundServices(): IterableIterator<[string, Service<any>]> {
return this.boundMap.entries()
}
/**
* Returns the public container event stream
*/
public getEventStream(): Observable<ContainerEvent> {
return this.event$.asObservable()
}
}

View File

@@ -0,0 +1,2 @@
export * from "./container"
export * from "./service"

View File

@@ -0,0 +1,65 @@
import { Observable, Subject } from 'rxjs'
import { Container, currentContainer } from './container'
/**
* A Dioc service that can bound to a container and can bind dependency services.
*
* NOTE: Services cannot have a constructor that takes arguments.
*
* @template EventDef The type of events that can be emitted by the service. These will be accessible by event streams
*/
export abstract class Service<EventDef = {}> {
/**
* The internal event stream of the service
*/
private event$ = new Subject<EventDef>()
/** The container the service is bound to */
#container: Container
constructor() {
if (!currentContainer) {
throw new Error(
`Tried to initialize service with no container (ID: ${ (this.constructor as any).ID })`
)
}
this.#container = currentContainer
}
/**
* Binds a dependency service into this service.
* @param service The class reference of the service to bind
*/
protected bind<T extends typeof Service<any> & { ID: string }>(service: T): InstanceType<T> {
if (!currentContainer) {
throw new Error('No currentContainer defined.')
}
return currentContainer.bind(service, this.constructor as typeof Service<any> & { ID: string })
}
/**
* Returns the container the service is bound to
*/
protected getContainer(): Container {
return this.#container
}
/**
* Emits an event on the service's event stream
* @param event The event to emit
*/
protected emit(event: EventDef) {
this.event$.next(event)
}
/**
* Returns the event stream of the service
*/
public getEventStream(): Observable<EventDef> {
return this.event$.asObservable()
}
}

View File

@@ -0,0 +1,33 @@
import { Container, Service } from "./main";
/**
* A container that can be used for writing tests, contains additional methods
* for binding suitable for writing tests. (see `bindMock`).
*/
export class TestContainer extends Container {
/**
* Binds a mock service to the container.
*
* @param service
* @param mock
*/
public bindMock<
T extends typeof Service<any> & { ID: string },
U extends Partial<InstanceType<T>>
>(service: T, mock: U): U {
if (this.boundMap.has(service.ID)) {
throw new Error(`Service '${service.ID}' already bound to container. Did you already call bindMock on this ?`)
}
this.boundMap.set(service.ID, mock as any)
this.event$.next({
type: "SERVICE_BIND",
boundeeID: service.ID,
bounderID: undefined,
})
return mock
}
}

34
packages/dioc/lib/vue.ts Normal file
View File

@@ -0,0 +1,34 @@
import { Plugin, inject } from "vue"
import { Container } from "./container"
import { Service } from "./service"
const VUE_CONTAINER_KEY = Symbol()
// TODO: Some Vue version issue with plugin generics is breaking type checking
/**
* The Vue Dioc Plugin, this allows the composables to work and access the container
*
* NOTE: Make sure you add `vue` as dependency to be able to use this plugin (duh)
*/
export const diocPlugin: Plugin = {
install(app, { container }) {
app.provide(VUE_CONTAINER_KEY, container)
}
}
/**
* A composable that binds a service to a Vue Component
*
* @param service The class reference of the service to bind
*/
export function useService<
T extends typeof Service<any> & { ID: string }
>(service: T): InstanceType<T> {
const container = inject(VUE_CONTAINER_KEY) as Container | undefined | null
if (!container) {
throw new Error("Container not found, did you forget to install the dioc plugin?")
}
return container.bind(service)
}

View File

@@ -0,0 +1,54 @@
{
"name": "dioc",
"private": true,
"version": "0.1.0",
"type": "module",
"files": [
"dist",
"index.d.ts"
],
"main": "./dist/counter.umd.cjs",
"module": "./dist/counter.js",
"types": "./index.d.ts",
"exports": {
".": {
"types": "./dist/main.d.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.js"
},
"./vue": {
"types": "./dist/vue.d.ts",
"require": "./dist/vue.cjs",
"import": "./dist/vue.js"
},
"./testing": {
"types": "./dist/testing.d.ts",
"require": "./dist/testing.cjs",
"import": "./dist/testing.js"
}
},
"scripts": {
"dev": "vite",
"build": "vite build && tsc --emitDeclarationOnly",
"prepare": "pnpm run build",
"test": "vitest run",
"do-test": "pnpm run test",
"test:watch": "vitest"
},
"devDependencies": {
"typescript": "^4.9.4",
"vite": "^4.0.4",
"vitest": "^0.29.3"
},
"dependencies": {
"rxjs": "^7.8.1"
},
"peerDependencies": {
"vue": "^3.2.25"
},
"peerDependenciesMeta": {
"vue": {
"optional": true
}
}
}

View File

@@ -0,0 +1,262 @@
import { it, expect, describe, vi } from "vitest"
import { Service } from "../lib/service"
import { Container, currentContainer, ContainerEvent } from "../lib/container"
class TestServiceA extends Service {
public static ID = "TestServiceA"
}
class TestServiceB extends Service {
public static ID = "TestServiceB"
// Marked public to allow for testing
public readonly serviceA = this.bind(TestServiceA)
}
describe("Container", () => {
describe("getBoundServiceWithID", () => {
it("returns the service instance if it is bound to the container", () => {
const container = new Container()
const service = container.bind(TestServiceA)
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBe(service)
})
it("returns undefined if the service is not bound to the container", () => {
const container = new Container()
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBeUndefined()
})
})
describe("bind", () => {
it("correctly binds the service to it", () => {
const container = new Container()
const service = container.bind(TestServiceA)
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
expect(service.getContainer()).toBe(container)
})
it("after bind, the current container is set back to its previous value", () => {
const originalValue = currentContainer
const container = new Container()
container.bind(TestServiceA)
expect(currentContainer).toBe(originalValue)
})
it("dependent services are registered in the same container", () => {
const container = new Container()
const serviceB = container.bind(TestServiceB)
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
expect(serviceB.serviceA.getContainer()).toBe(container)
})
it("binding an already initialized service returns the initialized instance (services are singletons)", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
const serviceA2 = container.bind(TestServiceA)
expect(serviceA).toBe(serviceA2)
})
it("binding a service which is a dependency of another service returns the same instance created from the dependency resolution (services are singletons)", () => {
const container = new Container()
const serviceB = container.bind(TestServiceB)
const serviceA = container.bind(TestServiceA)
expect(serviceB.serviceA).toBe(serviceA)
})
it("binding an initialized service as a dependency returns the same instance", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
const serviceB = container.bind(TestServiceB)
expect(serviceB.serviceA).toBe(serviceA)
})
it("container emits an init event when an uninitialized service is initialized via bind and event only called once", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_INIT" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_INIT") {
serviceFunc(ev)
}
})
const instance = container.bind(TestServiceA)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_INIT",
serviceID: TestServiceA.ID,
})
})
it("the bind event emitted has an undefined bounderID when the service is bound directly to the container", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_BIND" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_BIND") {
serviceFunc(ev)
}
})
container.bind(TestServiceA)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: undefined,
})
})
it("the bind event emitted has the correct bounderID when the service is bound to another service", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_BIND" }],
void
>()
container.getEventStream().subscribe((ev) => {
// We only care about the bind event of TestServiceA
if (ev.type === "SERVICE_BIND" && ev.boundeeID === TestServiceA.ID) {
serviceFunc(ev)
}
})
container.bind(TestServiceB)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: TestServiceB.ID,
})
})
})
describe("hasBound", () => {
it("returns true if the given service is bound to the container", () => {
const container = new Container()
container.bind(TestServiceA)
expect(container.hasBound(TestServiceA)).toEqual(true)
})
it("returns false if the given service is not bound to the container", () => {
const container = new Container()
expect(container.hasBound(TestServiceA)).toEqual(false)
})
it("returns true when the service is bound because it is a dependency of another service", () => {
const container = new Container()
container.bind(TestServiceB)
expect(container.hasBound(TestServiceA)).toEqual(true)
})
})
describe("getEventStream", () => {
it("returns an observable which emits events correctly when services are initialized", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_INIT" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_INIT") {
serviceFunc(ev)
}
})
container.bind(TestServiceB)
expect(serviceFunc).toHaveBeenCalledTimes(2)
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
type: "SERVICE_INIT",
serviceID: TestServiceA.ID,
})
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
type: "SERVICE_INIT",
serviceID: TestServiceB.ID,
})
})
it("returns an observable which emits events correctly when services are bound", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_BIND" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_BIND") {
serviceFunc(ev)
}
})
container.bind(TestServiceB)
expect(serviceFunc).toHaveBeenCalledTimes(2)
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: TestServiceB.ID,
})
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceB.ID,
bounderID: undefined,
})
})
})
describe("getBoundServices", () => {
it("returns an iterator over all services bound to the container in the format [service id, service instance]", () => {
const container = new Container()
const instanceB = container.bind(TestServiceB)
const instanceA = instanceB.serviceA
expect(Array.from(container.getBoundServices())).toEqual([
[TestServiceA.ID, instanceA],
[TestServiceB.ID, instanceB],
])
})
it("returns an empty iterator if no services are bound", () => {
const container = new Container()
expect(Array.from(container.getBoundServices())).toEqual([])
})
})
})

View File

@@ -0,0 +1,66 @@
import { describe, expect, it, vi } from "vitest"
import { Service, Container } from "../lib/main"
class TestServiceA extends Service {
public static ID = "TestServiceA"
}
class TestServiceB extends Service<"test"> {
public static ID = "TestServiceB"
// Marked public to allow for testing
public readonly serviceA = this.bind(TestServiceA)
public emitTestEvent() {
this.emit("test")
}
}
describe("Service", () => {
describe("constructor", () => {
it("throws an error if the service is initialized without a container", () => {
expect(() => new TestServiceA()).toThrowError(
"Tried to initialize service with no container (ID: TestServiceA)"
)
})
})
describe("bind", () => {
it("correctly binds the dependency service using the container", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
const serviceB = container.bind(TestServiceB)
expect(serviceB.serviceA).toBe(serviceA)
})
})
describe("getContainer", () => {
it("returns the container the service is bound to", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
// @ts-expect-error getContainer is a protected member, we are just using it to help with testing
expect(serviceA.getContainer()).toBe(container)
})
})
describe("getEventStream", () => {
it("returns the valid event stream of the service", () => {
const container = new Container()
const serviceB = container.bind(TestServiceB)
const serviceFunc = vi.fn()
serviceB.getEventStream().subscribe(serviceFunc)
serviceB.emitTestEvent()
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith("test")
})
})
})

View File

@@ -0,0 +1,92 @@
import { describe, expect, it, vi } from "vitest"
import { TestContainer } from "../lib/testing"
import { Service } from "../lib/service"
import { ContainerEvent } from "../lib/container"
class TestServiceA extends Service {
public static ID = "TestServiceA"
public test() {
return "real"
}
}
class TestServiceB extends Service {
public static ID = "TestServiceB"
// declared public to help with testing
public readonly serviceA = this.bind(TestServiceA)
public test() {
return this.serviceA.test()
}
}
describe("TestContainer", () => {
describe("bindMock", () => {
it("returns the fake service defined", () => {
const container = new TestContainer()
const fakeService = {
test: () => "fake",
}
const result = container.bindMock(TestServiceA, fakeService)
expect(result).toBe(fakeService)
})
it("new services bound to the container get the mock service", () => {
const container = new TestContainer()
const fakeServiceA = {
test: () => "fake",
}
container.bindMock(TestServiceA, fakeServiceA)
const serviceB = container.bind(TestServiceB)
expect(serviceB.serviceA).toBe(fakeServiceA)
})
it("container emits SERVICE_BIND event", () => {
const container = new TestContainer()
const fakeServiceA = {
test: () => "fake",
}
const serviceFunc = vi.fn<[ContainerEvent, void]>()
container.getEventStream().subscribe((ev) => {
serviceFunc(ev)
})
container.bindMock(TestServiceA, fakeServiceA)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: undefined,
})
})
it("throws if service already bound", () => {
const container = new TestContainer()
const fakeServiceA = {
test: () => "fake",
}
container.bindMock(TestServiceA, fakeServiceA)
expect(() => {
container.bindMock(TestServiceA, fakeServiceA)
}).toThrowError(
"Service 'TestServiceA' already bound to container. Did you already call bindMock on this ?"
)
})
})
})

2
packages/dioc/testing.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export { default } from "./dist/testing.d.ts"
export * from "./dist/testing.d.ts"

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"declaration": true,
"sourceMap": true,
"outDir": "dist",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true
},
"include": ["lib"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
export default defineConfig({
build: {
lib: {
entry: {
index: './lib/main.ts',
vue: './lib/vue.ts',
testing: './lib/testing.ts',
},
},
rollupOptions: {
external: ['vue'],
}
},
})

View File

@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
}
})

2
packages/dioc/vue.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export { default } from "./dist/vue.d.ts"
export * from "./dist/vue.d.ts"

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2023.4.0",
"version": "2023.8.0",
"description": "",
"author": "",
"private": true,
@@ -33,7 +33,7 @@
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.2.1",
"@nestjs/throttler": "^4.0.0",
"@prisma/client": "^4.7.1",
"@prisma/client": "^4.16.2",
"apollo-server-express": "^3.11.1",
"apollo-server-plugin-base": "^3.7.1",
"argon2": "^0.30.3",
@@ -57,7 +57,7 @@
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-microsoft": "^1.0.0",
"prisma": "^4.7.1",
"prisma": "^4.16.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.6.0"

View File

@@ -5,7 +5,7 @@ datasource db {
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x"]
binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"]
}
model Team {

View File

@@ -411,6 +411,23 @@ export class AdminResolver {
return deletedTeam.right;
}
@Mutation(() => Boolean, {
description: 'Revoke a team Invite by Invite ID',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async revokeTeamInviteByAdmin(
@Args({
name: 'inviteID',
description: 'Team Invite ID',
type: () => ID,
})
inviteID: string,
): Promise<boolean> {
const invite = await this.adminService.revokeTeamInviteByID(inviteID);
if (E.isLeft(invite)) throwErr(invite.left);
return true;
}
/* Subscriptions */
@Subscription(() => InvitedUser, {

View File

@@ -11,6 +11,7 @@ import {
INVALID_EMAIL,
ONLY_ONE_ADMIN_ACCOUNT,
TEAM_INVITE_ALREADY_MEMBER,
TEAM_INVITE_NO_INVITE_FOUND,
USER_ALREADY_INVITED,
USER_IS_ADMIN,
USER_NOT_FOUND,
@@ -181,7 +182,7 @@ export class AdminService {
* @returns an array team invitations
*/
async pendingInvitationCountInTeam(teamID: string) {
const invitations = await this.teamInvitationService.getAllTeamInvitations(
const invitations = await this.teamInvitationService.getTeamInvitations(
teamID,
);
@@ -236,11 +237,11 @@ export class AdminService {
const user = await this.userService.findUserByEmail(userEmail);
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
const isUserAlreadyMember = await this.teamService.getTeamMemberTE(
const teamMember = await this.teamService.getTeamMemberTE(
teamID,
user.value.uid,
)();
if (E.left(isUserAlreadyMember)) {
if (E.isLeft(teamMember)) {
const addedUser = await this.teamService.addMemberToTeamWithEmail(
teamID,
userEmail,
@@ -248,6 +249,18 @@ export class AdminService {
);
if (E.isLeft(addedUser)) return E.left(addedUser.left);
const userInvitation =
await this.teamInvitationService.getTeamInviteByEmailAndTeamID(
userEmail,
teamID,
);
if (E.isRight(userInvitation)) {
await this.teamInvitationService.revokeInvitation(
userInvitation.right.id,
);
}
return E.right(addedUser.right);
}
@@ -404,4 +417,19 @@ export class AdminService {
if (E.isLeft(team)) return E.left(team.left);
return E.right(team.right);
}
/**
* Revoke a team invite by ID
* @param inviteID Team Invite ID
* @returns an Either of boolean or error
*/
async revokeTeamInviteByID(inviteID: string) {
const teamInvite = await this.teamInvitationService.revokeInvitation(
inviteID,
);
if (E.isLeft(teamInvite)) return E.left(teamInvite.left);
return E.right(teamInvite.right);
}
}

View File

@@ -0,0 +1,9 @@
import { Controller, Get } from '@nestjs/common';
@Controller('ping')
export class AppController {
@Get()
ping(): string {
return 'Success';
}
}

View File

@@ -19,6 +19,7 @@ import { UserCollectionModule } from './user-collection/user-collection.module';
import { ShortcodeModule } from './shortcode/shortcode.module';
import { COOKIES_NOT_FOUND } from './errors';
import { ThrottlerModule } from '@nestjs/throttler';
import { AppController } from './app.controller';
@Module({
imports: [
@@ -81,5 +82,6 @@ import { ThrottlerModule } from '@nestjs/throttler';
ShortcodeModule,
],
providers: [GQLComplexityPlugin],
controllers: [AppController],
})
export class AppModule {}

View File

@@ -2,9 +2,9 @@ import {
Body,
Controller,
Get,
InternalServerErrorException,
Post,
Query,
Req,
Request,
Res,
UseGuards,
@@ -19,12 +19,18 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { GqlUser } from 'src/decorators/gql-user.decorator';
import { AuthUser } from 'src/types/AuthUser';
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
import { authCookieHandler, throwHTTPErr } from './helper';
import {
AuthProvider,
authCookieHandler,
authProviderCheck,
throwHTTPErr,
} from './helper';
import { GoogleSSOGuard } from './guards/google-sso.guard';
import { GithubSSOGuard } from './guards/github-sso.guard';
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
import { SkipThrottle } from '@nestjs/throttler';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'auth', version: '1' })
@@ -39,6 +45,9 @@ export class AuthController {
@Body() authData: SignInMagicDto,
@Query('origin') origin: string,
) {
if (!authProviderCheck(AuthProvider.EMAIL))
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
const deviceIdToken = await this.authService.signInMagicLink(
authData.email,
origin,

View File

@@ -11,6 +11,7 @@ import { RTJwtStrategy } from './strategies/rt-jwt.strategy';
import { GoogleStrategy } from './strategies/google.strategy';
import { GithubStrategy } from './strategies/github.strategy';
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
import { AuthProvider, authProviderCheck } from './helper';
@Module({
imports: [
@@ -26,9 +27,9 @@ import { MicrosoftStrategy } from './strategies/microsoft.strategy';
AuthService,
JwtStrategy,
RTJwtStrategy,
GoogleStrategy,
GithubStrategy,
MicrosoftStrategy,
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
],
controllers: [AuthController],
})

View File

@@ -228,11 +228,11 @@ export class AuthService {
url = process.env.VITE_BASE_URL;
}
await this.mailerService.sendAuthEmail(email, {
await this.mailerService.sendEmail(email, {
template: 'code-your-own',
variables: {
inviteeEmail: email,
magicLink: `${url}/magic-link?token=${generatedTokens.token}`,
magicLink: `${url}/enter?token=${generatedTokens.token}`,
},
});

View File

@@ -1,8 +1,20 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
@Injectable()
export class GithubSSOGuard extends AuthGuard('github') {
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
if (!authProviderCheck(AuthProvider.GITHUB))
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
return super.canActivate(context);
}
getAuthenticateOptions(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();

View File

@@ -1,8 +1,20 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
@Injectable()
export class GoogleSSOGuard extends AuthGuard('google') {
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
if (!authProviderCheck(AuthProvider.GOOGLE))
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
return super.canActivate(context);
}
getAuthenticateOptions(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();

View File

@@ -1,8 +1,26 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
@Injectable()
export class MicrosoftSSOGuard extends AuthGuard('microsoft') {
export class MicrosoftSSOGuard
extends AuthGuard('microsoft')
implements CanActivate
{
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
if (!authProviderCheck(AuthProvider.MICROSOFT))
throwHTTPErr({
message: AUTH_PROVIDER_NOT_SPECIFIED,
statusCode: 404,
});
return super.canActivate(context);
}
getAuthenticateOptions(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();

View File

@@ -1,10 +1,11 @@
import { ForbiddenException, HttpException, HttpStatus } from '@nestjs/common';
import { HttpException, HttpStatus } from '@nestjs/common';
import { DateTime } from 'luxon';
import { AuthError } from 'src/types/AuthError';
import { AuthTokens } from 'src/types/AuthTokens';
import { Response } from 'express';
import * as cookie from 'cookie';
import { COOKIES_NOT_FOUND } from 'src/errors';
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
import { throwErr } from 'src/utils';
enum AuthTokenType {
ACCESS_TOKEN = 'access_token',
@@ -16,6 +17,13 @@ export enum Origin {
APP = 'app',
}
export enum AuthProvider {
GOOGLE = 'GOOGLE',
GITHUB = 'GITHUB',
MICROSOFT = 'MICROSOFT',
EMAIL = 'EMAIL',
}
/**
* This function allows throw to be used as an expression
* @param errMessage Message present in the error message
@@ -63,7 +71,7 @@ export const authCookieHandler = (
});
if (!redirect) {
res.status(HttpStatus.OK).send();
return res.status(HttpStatus.OK).send();
}
// check to see if redirectUrl is a whitelisted url
@@ -72,7 +80,7 @@ export const authCookieHandler = (
// if it is not redirect by default to REDIRECT_URL
redirectUrl = process.env.REDIRECT_URL;
res.status(HttpStatus.OK).redirect(redirectUrl);
return res.status(HttpStatus.OK).redirect(redirectUrl);
};
/**
@@ -97,3 +105,25 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
refresh_token: cookies[AuthTokenType.REFRESH_TOKEN],
};
};
/**
* Check to see if given auth provider is present in the VITE_ALLOWED_AUTH_PROVIDERS env variable
*
* @param provider Provider we want to check the presence of
* @returns Boolean if provider specified is present or not
*/
export function authProviderCheck(provider: string) {
if (!provider) {
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
}
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
provider.trim().toUpperCase(),
)
: [];
if (!envVariables.includes(provider.toUpperCase())) return false;
return true;
}

View File

@@ -17,7 +17,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
scope: [process.env.MICROSOFT_SCOPE],
passReqToCallback: true,
tenant: process.env.MICROSOFT_TENANT,
store: true,
});
}

View File

@@ -23,7 +23,31 @@ export const AUTH_FAIL = 'auth/fail';
export const JSON_INVALID = 'json_invalid';
/**
* Tried to delete an user data document from fb firestore but failed.
* Auth Provider not specified
* (Auth)
*/
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
/**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
*/
export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS =
'"VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file';
/**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file
*/
export const ENV_EMPTY_AUTH_PROVIDERS =
'"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file';
/**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
*/
export const ENV_NOT_SUPPORT_AUTH_PROVIDERS =
'"VITE_ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
/**
* Tried to delete a user data document from fb firestore but failed.
* (FirebaseService)
*/
export const USER_FB_DOCUMENT_DELETION_FAILED =
@@ -231,7 +255,7 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
/**
* Tried to perform action on a request that doesn't accept their member role level
* Tried to perform an action on a request that doesn't accept their member role level
* (GqlRequestTeamMemberGuard)
*/
export const TEAM_REQ_NOT_REQUIRED_ROLE = 'team_req/not_required_role';
@@ -262,7 +286,7 @@ export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
export const SENDER_EMAIL_INVALID = 'mailer/sender_email_invalid' as const;
/**
* Tried to perform action on a request when the user is not even member of the team
* Tried to perform an action on a request when the user is not even a member of the team
* (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard)
*/
export const TEAM_REQ_NOT_MEMBER = 'team_req/not_member';
@@ -307,11 +331,18 @@ export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
/**
* Invalid or non-existent TEAM ENVIRONMMENT ID
* Invalid or non-existent TEAM ENVIRONMENT ID
* (TeamEnvironmentsService)
*/
export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
/**
* Invalid TEAM ENVIRONMENT name
* (TeamEnvironmentsService)
*/
export const TEAM_ENVIRONMENT_SHORT_NAME =
'team_environment/short_name' as const;
/**
* The user is not a member of the team of the given environment
* (GqlTeamEnvTeamGuard)
@@ -340,7 +371,7 @@ export const USER_SETTINGS_NULL_SETTINGS =
'user_settings/null_settings' as const;
/*
* Global environment doesnt exists for the user
* Global environment doesn't exist for the user
* (UserEnvironmentsService)
*/
export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS =

View File

@@ -5,7 +5,6 @@ import {
UserMagicLinkMailDescription,
} from './MailDescriptions';
import { throwErr } from 'src/utils';
import * as TE from 'fp-ts/TaskEither';
import { EMAIL_FAILED } from 'src/errors';
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
@@ -35,33 +34,14 @@ export class MailerService {
/**
* Sends an email to the given email address given a mail description
* @param to The email address to be sent to (NOTE: this is not validated)
* @param to Receiver's email id
* @param mailDesc Definition of what email to be sent
* @returns Response if email was send successfully or not
*/
sendMail(
async sendEmail(
to: string,
mailDesc: MailDescription | UserMagicLinkMailDescription,
) {
return TE.tryCatch(
async () => {
await this.nestMailerService.sendMail({
to,
template: mailDesc.template,
subject: this.resolveSubjectForMailDesc(mailDesc),
context: mailDesc.variables,
});
},
() => EMAIL_FAILED,
);
}
/**
*
* @param to Receiver's email id
* @param mailDesc Details of email to be sent for Magic-Link auth
* @returns Response if email was send successfully or not
*/
async sendAuthEmail(to: string, mailDesc: UserMagicLinkMailDescription) {
try {
await this.nestMailerService.sendMail({
to,

View File

@@ -5,11 +5,13 @@ import * as cookieParser from 'cookie-parser';
import { VersioningType } from '@nestjs/common';
import * as session from 'express-session';
import { emitGQLSchemaFile } from './gql-schema';
import { checkEnvironmentAuthProvider } from './utils';
async function bootstrap() {
console.log(`Running in production: ${process.env.PRODUCTION}`);
console.log(`Port: ${process.env.PORT}`);
console.log(`Database: ${process.env.DATABASE_URL}`);
checkEnvironmentAuthProvider();
const app = await NestFactory.create(AppModule);

View File

@@ -150,7 +150,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
orderBy: {
createdOn: 'desc',
},
skip: 1,
skip: args.cursor ? 1 : 0,
take: args.take,
cursor: args.cursor ? { id: args.cursor } : undefined,
});

View File

@@ -1,15 +1,5 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import * as TE from 'fp-ts/TaskEither';
import * as O from 'fp-ts/Option';
import * as S from 'fp-ts/string';
import { pipe } from 'fp-ts/function';
import {
getAnnotatedRequiredRoles,
getGqlArg,
getUserFromGQLContext,
throwErr,
} from 'src/utils';
import { TeamEnvironmentsService } from './team-environments.service';
import {
BUG_AUTH_NO_USER_CTX,
@@ -19,6 +9,10 @@ import {
TEAM_ENVIRONMENT_NOT_FOUND,
} from 'src/errors';
import { TeamService } from 'src/team/team.service';
import { GqlExecutionContext } from '@nestjs/graphql';
import * as E from 'fp-ts/Either';
import { TeamMemberRole } from '@prisma/client';
import { throwErr } from 'src/utils';
/**
* A guard which checks whether the caller of a GQL Operation
@@ -33,50 +27,31 @@ export class GqlTeamEnvTeamGuard implements CanActivate {
private readonly teamService: TeamService,
) {}
canActivate(context: ExecutionContext): Promise<boolean> {
return pipe(
TE.Do,
async canActivate(context: ExecutionContext): Promise<boolean> {
const requireRoles = this.reflector.get<TeamMemberRole[]>(
'requiresTeamRole',
context.getHandler(),
);
if (!requireRoles) throw new Error(BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES);
TE.bindW('requiredRoles', () =>
pipe(
getAnnotatedRequiredRoles(this.reflector, context),
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES),
),
),
const gqlExecCtx = GqlExecutionContext.create(context);
TE.bindW('user', () =>
pipe(
getUserFromGQLContext(context),
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
),
),
const { user } = gqlExecCtx.getContext().req;
if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX);
TE.bindW('envID', () =>
pipe(
getGqlArg('id', context),
O.fromPredicate(S.isString),
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_ENV_ID),
),
),
const { id } = gqlExecCtx.getArgs<{ id: string }>();
if (!id) throwErr(BUG_TEAM_ENV_GUARD_NO_ENV_ID);
TE.bindW('membership', ({ envID, user }) =>
pipe(
this.teamEnvironmentService.getTeamEnvironment(envID),
TE.fromTaskOption(() => TEAM_ENVIRONMENT_NOT_FOUND),
TE.chainW((env) =>
pipe(
this.teamService.getTeamMemberTE(env.teamID, user.uid),
TE.mapLeft(() => TEAM_ENVIRONMENT_NOT_TEAM_MEMBER),
),
),
),
),
const teamEnvironment =
await this.teamEnvironmentService.getTeamEnvironment(id);
if (E.isLeft(teamEnvironment)) throwErr(TEAM_ENVIRONMENT_NOT_FOUND);
TE.map(({ membership, requiredRoles }) =>
requiredRoles.includes(membership.role),
),
const member = await this.teamService.getTeamMember(
teamEnvironment.right.teamID,
user.uid,
);
if (!member) throwErr(TEAM_ENVIRONMENT_NOT_TEAM_MEMBER);
TE.getOrElse(throwErr),
)();
return requireRoles.includes(member.role);
}
}

View File

@@ -0,0 +1,41 @@
import { ArgsType, Field, ID } from '@nestjs/graphql';
@ArgsType()
export class CreateTeamEnvironmentArgs {
@Field({
name: 'name',
description: 'Name of the Team Environment',
})
name: string;
@Field(() => ID, {
name: 'teamID',
description: 'ID of the Team',
})
teamID: string;
@Field({
name: 'variables',
description: 'JSON string of the variables object',
})
variables: string;
}
@ArgsType()
export class UpdateTeamEnvironmentArgs {
@Field(() => ID, {
name: 'id',
description: 'ID of the Team Environment',
})
id: string;
@Field({
name: 'name',
description: 'Name of the Team Environment',
})
name: string;
@Field({
name: 'variables',
description: 'JSON string of the variables object',
})
variables: string;
}

View File

@@ -13,6 +13,11 @@ import { throwErr } from 'src/utils';
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
import { TeamEnvironment } from './team-environments.model';
import { TeamEnvironmentsService } from './team-environments.service';
import * as E from 'fp-ts/Either';
import {
CreateTeamEnvironmentArgs,
UpdateTeamEnvironmentArgs,
} from './input-type.args';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => 'TeamEnvironment')
@@ -29,29 +34,18 @@ export class TeamEnvironmentsResolver {
})
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
createTeamEnvironment(
@Args({
name: 'name',
description: 'Name of the Team Environment',
})
name: string,
@Args({
name: 'teamID',
description: 'ID of the Team',
type: () => ID,
})
teamID: string,
@Args({
name: 'variables',
description: 'JSON string of the variables object',
})
variables: string,
async createTeamEnvironment(
@Args() args: CreateTeamEnvironmentArgs,
): Promise<TeamEnvironment> {
return this.teamEnvironmentsService.createTeamEnvironment(
name,
teamID,
variables,
)();
const teamEnvironment =
await this.teamEnvironmentsService.createTeamEnvironment(
args.name,
args.teamID,
args.variables,
);
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
return teamEnvironment.right;
}
@Mutation(() => Boolean, {
@@ -59,7 +53,7 @@ export class TeamEnvironmentsResolver {
})
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
deleteTeamEnvironment(
async deleteTeamEnvironment(
@Args({
name: 'id',
description: 'ID of the Team Environment',
@@ -67,10 +61,12 @@ export class TeamEnvironmentsResolver {
})
id: string,
): Promise<boolean> {
return pipe(
this.teamEnvironmentsService.deleteTeamEnvironment(id),
TE.getOrElse(throwErr),
)();
const isDeleted = await this.teamEnvironmentsService.deleteTeamEnvironment(
id,
);
if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
return isDeleted.right;
}
@Mutation(() => TeamEnvironment, {
@@ -79,28 +75,19 @@ export class TeamEnvironmentsResolver {
})
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
updateTeamEnvironment(
@Args({
name: 'id',
description: 'ID of the Team Environment',
type: () => ID,
})
id: string,
@Args({
name: 'name',
description: 'Name of the Team Environment',
})
name: string,
@Args({
name: 'variables',
description: 'JSON string of the variables object',
})
variables: string,
async updateTeamEnvironment(
@Args()
args: UpdateTeamEnvironmentArgs,
): Promise<TeamEnvironment> {
return pipe(
this.teamEnvironmentsService.updateTeamEnvironment(id, name, variables),
TE.getOrElse(throwErr),
)();
const updatedTeamEnvironment =
await this.teamEnvironmentsService.updateTeamEnvironment(
args.id,
args.name,
args.variables,
);
if (E.isLeft(updatedTeamEnvironment)) throwErr(updatedTeamEnvironment.left);
return updatedTeamEnvironment.right;
}
@Mutation(() => TeamEnvironment, {
@@ -108,7 +95,7 @@ export class TeamEnvironmentsResolver {
})
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
deleteAllVariablesFromTeamEnvironment(
async deleteAllVariablesFromTeamEnvironment(
@Args({
name: 'id',
description: 'ID of the Team Environment',
@@ -116,10 +103,13 @@ export class TeamEnvironmentsResolver {
})
id: string,
): Promise<TeamEnvironment> {
return pipe(
this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(id),
TE.getOrElse(throwErr),
)();
const teamEnvironment =
await this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
id,
);
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
return teamEnvironment.right;
}
@Mutation(() => TeamEnvironment, {
@@ -127,7 +117,7 @@ export class TeamEnvironmentsResolver {
})
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
createDuplicateEnvironment(
async createDuplicateEnvironment(
@Args({
name: 'id',
description: 'ID of the Team Environment',
@@ -135,10 +125,12 @@ export class TeamEnvironmentsResolver {
})
id: string,
): Promise<TeamEnvironment> {
return pipe(
this.teamEnvironmentsService.createDuplicateEnvironment(id),
TE.getOrElse(throwErr),
)();
const res = await this.teamEnvironmentsService.createDuplicateEnvironment(
id,
);
if (E.isLeft(res)) throwErr(res.left);
return res.right;
}
/* Subscriptions */

View File

@@ -2,7 +2,11 @@ import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from 'src/prisma/prisma.service';
import { TeamEnvironment } from './team-environments.model';
import { TeamEnvironmentsService } from './team-environments.service';
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
import {
JSON_INVALID,
TEAM_ENVIRONMENT_NOT_FOUND,
TEAM_ENVIRONMENT_SHORT_NAME,
} from 'src/errors';
const mockPrisma = mockDeep<PrismaService>();
@@ -31,125 +35,81 @@ beforeEach(() => {
describe('TeamEnvironmentsService', () => {
describe('getTeamEnvironment', () => {
test('queries the db with the id', async () => {
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
await teamEnvironmentsService.getTeamEnvironment('123')();
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: {
id: '123',
},
}),
test('should successfully return a TeamEnvironment with valid ID', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
teamEnvironment,
);
});
test('requests prisma to reject the query promise if not found', async () => {
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
await teamEnvironmentsService.getTeamEnvironment('123')();
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
rejectOnNotFound: true,
}),
const result = await teamEnvironmentsService.getTeamEnvironment(
teamEnvironment.id,
);
expect(result).toEqualRight(teamEnvironment);
});
test('should return a Some of the correct environment if exists', async () => {
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce(
'RejectOnNotFound',
);
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
expect(result).toEqualSome(teamEnvironment);
});
test('should return a None if the environment does not exist', async () => {
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
expect(result).toBeNone();
const result = await teamEnvironmentsService.getTeamEnvironment(
teamEnvironment.id,
);
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
});
});
describe('createTeamEnvironment', () => {
test('should create and return a new team environment given a valid name,variable and team ID', async () => {
test('should successfully create and return a new team environment given valid inputs', async () => {
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
const result = await teamEnvironmentsService.createTeamEnvironment(
teamEnvironment.name,
teamEnvironment.teamID,
JSON.stringify(teamEnvironment.variables),
)();
);
expect(result).toEqual(<TeamEnvironment>{
id: teamEnvironment.id,
name: teamEnvironment.name,
teamID: teamEnvironment.teamID,
expect(result).toEqualRight({
...teamEnvironment,
variables: JSON.stringify(teamEnvironment.variables),
});
});
test('should reject if given team ID is invalid', async () => {
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
const result = await teamEnvironmentsService.createTeamEnvironment(
'12',
teamEnvironment.teamID,
JSON.stringify(teamEnvironment.variables),
);
await expect(
teamEnvironmentsService.createTeamEnvironment(
teamEnvironment.name,
'invalidteamid',
JSON.stringify(teamEnvironment.variables),
),
).rejects.toBeDefined();
});
test('should reject if provided team environment name is not a string', async () => {
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
await expect(
teamEnvironmentsService.createTeamEnvironment(
null as any,
teamEnvironment.teamID,
JSON.stringify(teamEnvironment.variables),
),
).rejects.toBeDefined();
});
test('should reject if provided variable is not a string', async () => {
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
await expect(
teamEnvironmentsService.createTeamEnvironment(
teamEnvironment.name,
teamEnvironment.teamID,
null as any,
),
).rejects.toBeDefined();
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
});
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
mockPrisma.teamEnvironment.create.mockResolvedValueOnce(teamEnvironment);
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
const result = await teamEnvironmentsService.createTeamEnvironment(
teamEnvironment.name,
teamEnvironment.teamID,
JSON.stringify(teamEnvironment.variables),
)();
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_environment/${teamEnvironment.teamID}/created`,
result,
{
...teamEnvironment,
variables: JSON.stringify(teamEnvironment.variables),
},
);
});
});
describe('deleteTeamEnvironment', () => {
test('should resolve to true given a valid team environment ID', async () => {
test('should successfully delete a TeamEnvironment with a valid ID', async () => {
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
const result = await teamEnvironmentsService.deleteTeamEnvironment(
teamEnvironment.id,
)();
);
expect(result).toEqualRight(true);
});
@@ -159,7 +119,7 @@ describe('TeamEnvironmentsService', () => {
const result = await teamEnvironmentsService.deleteTeamEnvironment(
'invalidid',
)();
);
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
});
@@ -169,7 +129,7 @@ describe('TeamEnvironmentsService', () => {
const result = await teamEnvironmentsService.deleteTeamEnvironment(
teamEnvironment.id,
)();
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_environment/${teamEnvironment.teamID}/deleted`,
@@ -182,7 +142,7 @@ describe('TeamEnvironmentsService', () => {
});
describe('updateVariablesInTeamEnvironment', () => {
test('should add new variable to a team environment', async () => {
test('should successfully add new variable to a team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
...teamEnvironment,
variables: [{ key: 'value' }],
@@ -192,7 +152,7 @@ describe('TeamEnvironmentsService', () => {
teamEnvironment.id,
teamEnvironment.name,
JSON.stringify([{ key: 'value' }]),
)();
);
expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment,
@@ -200,7 +160,7 @@ describe('TeamEnvironmentsService', () => {
});
});
test('should add new variable to already existing list of variables in a team environment', async () => {
test('should successfully add new variable to already existing list of variables in a team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
...teamEnvironment,
variables: [{ key: 'value' }, { key_2: 'value_2' }],
@@ -210,7 +170,7 @@ describe('TeamEnvironmentsService', () => {
teamEnvironment.id,
teamEnvironment.name,
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
)();
);
expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment,
@@ -218,7 +178,7 @@ describe('TeamEnvironmentsService', () => {
});
});
test('should edit existing variables in a team environment', async () => {
test('should successfully edit existing variables in a team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
...teamEnvironment,
variables: [{ key: '1234' }],
@@ -228,7 +188,7 @@ describe('TeamEnvironmentsService', () => {
teamEnvironment.id,
teamEnvironment.name,
JSON.stringify([{ key: '1234' }]),
)();
);
expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment,
@@ -236,22 +196,7 @@ describe('TeamEnvironmentsService', () => {
});
});
test('should delete existing variable in a team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
const result = await teamEnvironmentsService.updateTeamEnvironment(
teamEnvironment.id,
teamEnvironment.name,
JSON.stringify([{}]),
)();
expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment,
variables: JSON.stringify([{}]),
});
});
test('should edit name of an existing team environment', async () => {
test('should successfully edit name of an existing team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
...teamEnvironment,
variables: [{ key: '123' }],
@@ -261,7 +206,7 @@ describe('TeamEnvironmentsService', () => {
teamEnvironment.id,
teamEnvironment.name,
JSON.stringify([{ key: '123' }]),
)();
);
expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment,
@@ -269,14 +214,24 @@ describe('TeamEnvironmentsService', () => {
});
});
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
const result = await teamEnvironmentsService.updateTeamEnvironment(
teamEnvironment.id,
'12',
JSON.stringify([{ key: 'value' }]),
);
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
});
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
const result = await teamEnvironmentsService.updateTeamEnvironment(
'invalidid',
teamEnvironment.name,
JSON.stringify(teamEnvironment.variables),
)();
);
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
});
@@ -288,7 +243,7 @@ describe('TeamEnvironmentsService', () => {
teamEnvironment.id,
teamEnvironment.name,
JSON.stringify([{ key: 'value' }]),
)();
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_environment/${teamEnvironment.teamID}/updated`,
@@ -301,13 +256,13 @@ describe('TeamEnvironmentsService', () => {
});
describe('deleteAllVariablesFromTeamEnvironment', () => {
test('should delete all variables in a team environment', async () => {
test('should successfully delete all variables in a team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
const result =
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
teamEnvironment.id,
)();
);
expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment,
@@ -315,13 +270,13 @@ describe('TeamEnvironmentsService', () => {
});
});
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
const result =
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
'invalidid',
)();
);
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
});
@@ -332,7 +287,7 @@ describe('TeamEnvironmentsService', () => {
const result =
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
teamEnvironment.id,
)();
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_environment/${teamEnvironment.teamID}/updated`,
@@ -345,33 +300,33 @@ describe('TeamEnvironmentsService', () => {
});
describe('createDuplicateEnvironment', () => {
test('should duplicate an existing team environment', async () => {
test('should successfully duplicate an existing team environment', async () => {
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
teamEnvironment,
);
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
...teamEnvironment,
id: 'newid',
...teamEnvironment,
});
const result = await teamEnvironmentsService.createDuplicateEnvironment(
teamEnvironment.id,
)();
);
expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment,
id: 'newid',
...teamEnvironment,
variables: JSON.stringify(teamEnvironment.variables),
});
});
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
const result = await teamEnvironmentsService.createDuplicateEnvironment(
teamEnvironment.id,
)();
);
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
});
@@ -382,19 +337,19 @@ describe('TeamEnvironmentsService', () => {
);
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
...teamEnvironment,
id: 'newid',
...teamEnvironment,
});
const result = await teamEnvironmentsService.createDuplicateEnvironment(
teamEnvironment.id,
)();
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_environment/${teamEnvironment.teamID}/created`,
{
...teamEnvironment,
id: 'newid',
...teamEnvironment,
variables: JSON.stringify([{}]),
},
);

View File

@@ -1,15 +1,14 @@
import { Injectable } from '@nestjs/common';
import { pipe } from 'fp-ts/function';
import * as T from 'fp-ts/Task';
import * as TO from 'fp-ts/TaskOption';
import * as TE from 'fp-ts/TaskEither';
import * as A from 'fp-ts/Array';
import { Prisma } from '@prisma/client';
import { TeamEnvironment as DBTeamEnvironment, Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { TeamEnvironment } from './team-environments.model';
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
import {
TEAM_ENVIRONMENT_NOT_FOUND,
TEAM_ENVIRONMENT_SHORT_NAME,
} from 'src/errors';
import * as E from 'fp-ts/Either';
import { isValidLength } from 'src/utils';
@Injectable()
export class TeamEnvironmentsService {
constructor(
@@ -17,219 +16,218 @@ export class TeamEnvironmentsService {
private readonly pubsub: PubSubService,
) {}
getTeamEnvironment(id: string) {
return TO.tryCatch(() =>
this.prisma.teamEnvironment.findFirst({
where: { id },
TITLE_LENGTH = 3;
/**
* TeamEnvironments are saved in the DB in the following way
* [{ key: value }, { key: value },....]
*
*/
/**
* Typecast a database TeamEnvironment to a TeamEnvironment model
* @param teamEnvironment database TeamEnvironment
* @returns TeamEnvironment model
*/
private cast(teamEnvironment: DBTeamEnvironment): TeamEnvironment {
return {
id: teamEnvironment.id,
name: teamEnvironment.name,
teamID: teamEnvironment.teamID,
variables: JSON.stringify(teamEnvironment.variables),
};
}
/**
* Get details of a TeamEnvironment.
*
* @param id TeamEnvironment ID
* @returns Either of a TeamEnvironment or error message
*/
async getTeamEnvironment(id: string) {
try {
const teamEnvironment =
await this.prisma.teamEnvironment.findFirstOrThrow({
where: { id },
});
return E.right(teamEnvironment);
} catch (error) {
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
}
/**
* Create a new TeamEnvironment.
*
* @param name name of new TeamEnvironment
* @param teamID teamID of new TeamEnvironment
* @param variables JSONified string of contents of new TeamEnvironment
* @returns Either of a TeamEnvironment or error message
*/
async createTeamEnvironment(name: string, teamID: string, variables: string) {
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
const result = await this.prisma.teamEnvironment.create({
data: {
name: name,
teamID: teamID,
variables: JSON.parse(variables),
},
});
const createdTeamEnvironment = this.cast(result);
this.pubsub.publish(
`team_environment/${createdTeamEnvironment.teamID}/created`,
createdTeamEnvironment,
);
return E.right(createdTeamEnvironment);
}
/**
* Delete a TeamEnvironment.
*
* @param id TeamEnvironment ID
* @returns Either of boolean or error message
*/
async deleteTeamEnvironment(id: string) {
try {
const result = await this.prisma.teamEnvironment.delete({
where: {
id: id,
},
});
const deletedTeamEnvironment = this.cast(result);
this.pubsub.publish(
`team_environment/${deletedTeamEnvironment.teamID}/deleted`,
deletedTeamEnvironment,
);
return E.right(true);
} catch (error) {
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
}
/**
* Update a TeamEnvironment.
*
* @param id TeamEnvironment ID
* @param name TeamEnvironment name
* @param variables JSONified string of contents of new TeamEnvironment
* @returns Either of a TeamEnvironment or error message
*/
async updateTeamEnvironment(id: string, name: string, variables: string) {
try {
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
const result = await this.prisma.teamEnvironment.update({
where: { id: id },
data: {
name,
variables: JSON.parse(variables),
},
});
const updatedTeamEnvironment = this.cast(result);
this.pubsub.publish(
`team_environment/${updatedTeamEnvironment.teamID}/updated`,
updatedTeamEnvironment,
);
return E.right(updatedTeamEnvironment);
} catch (error) {
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
}
/**
* Clear contents of a TeamEnvironment.
*
* @param id TeamEnvironment ID
* @returns Either of a TeamEnvironment or error message
*/
async deleteAllVariablesFromTeamEnvironment(id: string) {
try {
const result = await this.prisma.teamEnvironment.update({
where: { id: id },
data: {
variables: [],
},
});
const teamEnvironment = this.cast(result);
this.pubsub.publish(
`team_environment/${teamEnvironment.teamID}/updated`,
teamEnvironment,
);
return E.right(teamEnvironment);
} catch (error) {
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
}
/**
* Create a duplicate of a existing TeamEnvironment.
*
* @param id TeamEnvironment ID
* @returns Either of a TeamEnvironment or error message
*/
async createDuplicateEnvironment(id: string) {
try {
const environment = await this.prisma.teamEnvironment.findFirst({
where: {
id: id,
},
rejectOnNotFound: true,
}),
);
});
const result = await this.prisma.teamEnvironment.create({
data: {
name: environment.name,
teamID: environment.teamID,
variables: environment.variables as Prisma.JsonArray,
},
});
const duplicatedTeamEnvironment = this.cast(result);
this.pubsub.publish(
`team_environment/${duplicatedTeamEnvironment.teamID}/created`,
duplicatedTeamEnvironment,
);
return E.right(duplicatedTeamEnvironment);
} catch (error) {
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
}
createTeamEnvironment(name: string, teamID: string, variables: string) {
return pipe(
() =>
this.prisma.teamEnvironment.create({
data: {
name: name,
teamID: teamID,
variables: JSON.parse(variables),
},
}),
T.chainFirst(
(environment) => () =>
this.pubsub.publish(
`team_environment/${environment.teamID}/created`,
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
T.map((data) => {
return <TeamEnvironment>{
id: data.id,
name: data.name,
teamID: data.teamID,
variables: JSON.stringify(data.variables),
};
}),
);
}
/**
* Fetch all TeamEnvironments of a team.
*
* @param teamID teamID of new TeamEnvironment
* @returns List of TeamEnvironments
*/
async fetchAllTeamEnvironments(teamID: string) {
const result = await this.prisma.teamEnvironment.findMany({
where: {
teamID: teamID,
},
});
const teamEnvironments = result.map((item) => {
return this.cast(item);
});
deleteTeamEnvironment(id: string) {
return pipe(
TE.tryCatch(
() =>
this.prisma.teamEnvironment.delete({
where: {
id: id,
},
}),
() => TEAM_ENVIRONMENT_NOT_FOUND,
),
TE.chainFirst((environment) =>
TE.fromTask(() =>
this.pubsub.publish(
`team_environment/${environment.teamID}/deleted`,
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
),
TE.map((data) => true),
);
}
updateTeamEnvironment(id: string, name: string, variables: string) {
return pipe(
TE.tryCatch(
() =>
this.prisma.teamEnvironment.update({
where: { id: id },
data: {
name,
variables: JSON.parse(variables),
},
}),
() => TEAM_ENVIRONMENT_NOT_FOUND,
),
TE.chainFirst((environment) =>
TE.fromTask(() =>
this.pubsub.publish(
`team_environment/${environment.teamID}/updated`,
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
),
TE.map(
(environment) =>
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
);
}
deleteAllVariablesFromTeamEnvironment(id: string) {
return pipe(
TE.tryCatch(
() =>
this.prisma.teamEnvironment.update({
where: { id: id },
data: {
variables: [],
},
}),
() => TEAM_ENVIRONMENT_NOT_FOUND,
),
TE.chainFirst((environment) =>
TE.fromTask(() =>
this.pubsub.publish(
`team_environment/${environment.teamID}/updated`,
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
),
TE.map(
(environment) =>
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
);
}
createDuplicateEnvironment(id: string) {
return pipe(
TE.tryCatch(
() =>
this.prisma.teamEnvironment.findFirst({
where: {
id: id,
},
rejectOnNotFound: true,
}),
() => TEAM_ENVIRONMENT_NOT_FOUND,
),
TE.chain((environment) =>
TE.fromTask(() =>
this.prisma.teamEnvironment.create({
data: {
name: environment.name,
teamID: environment.teamID,
variables: environment.variables as Prisma.JsonArray,
},
}),
),
),
TE.chainFirst((environment) =>
TE.fromTask(() =>
this.pubsub.publish(
`team_environment/${environment.teamID}/created`,
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
),
TE.map(
(environment) =>
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
);
}
fetchAllTeamEnvironments(teamID: string) {
return pipe(
() =>
this.prisma.teamEnvironment.findMany({
where: {
teamID: teamID,
},
}),
T.map(
A.map(
(environment) =>
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
);
return teamEnvironments;
}
/**

View File

@@ -11,6 +11,6 @@ export class TeamEnvsTeamResolver {
description: 'Returns all Team Environments for the given Team',
})
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id)();
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id);
}
}

View File

@@ -0,0 +1,20 @@
import { ArgsType, Field, ID } from '@nestjs/graphql';
import { TeamMemberRole } from 'src/team/team.model';
@ArgsType()
export class CreateTeamInvitationArgs {
@Field(() => ID, {
name: 'teamID',
description: 'ID of the Team ID to invite from',
})
teamID: string;
@Field({ name: 'inviteeEmail', description: 'Email of the user to invite' })
inviteeEmail: string;
@Field(() => TeamMemberRole, {
name: 'inviteeRole',
description: 'Role to be given to the user',
})
inviteeRole: TeamMemberRole;
}

View File

@@ -12,15 +12,10 @@ import { TeamInvitation } from './team-invitation.model';
import { TeamInvitationService } from './team-invitation.service';
import { pipe } from 'fp-ts/function';
import * as TE from 'fp-ts/TaskEither';
import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option';
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
import { EmailCodec } from 'src/types/Email';
import {
INVALID_EMAIL,
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
TEAM_INVITE_NO_INVITE_FOUND,
USER_NOT_FOUND,
} from 'src/errors';
import { TEAM_INVITE_NO_INVITE_FOUND, USER_NOT_FOUND } from 'src/errors';
import { GqlUser } from 'src/decorators/gql-user.decorator';
import { User } from 'src/user/user.model';
import { UseGuards } from '@nestjs/common';
@@ -36,6 +31,8 @@ import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler';
import { AuthUser } from 'src/types/AuthUser';
import { CreateTeamInvitationArgs } from './input-type.args';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => TeamInvitation)
@@ -79,8 +76,8 @@ export class TeamInvitationResolver {
'Gets the Team Invitation with the given ID, or null if not exists',
})
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
teamInvitation(
@GqlUser() user: User,
async teamInvitation(
@GqlUser() user: AuthUser,
@Args({
name: 'inviteID',
description: 'ID of the Team Invitation to lookup',
@@ -88,17 +85,11 @@ export class TeamInvitationResolver {
})
inviteID: string,
): Promise<TeamInvitation> {
return pipe(
this.teamInvitationService.getInvitation(inviteID),
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
TE.chainW(
TE.fromPredicate(
(a) => a.inviteeEmail.toLowerCase() === user.email?.toLowerCase(),
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
),
),
TE.getOrElse(throwErr),
)();
const teamInvitation = await this.teamInvitationService.getInvitation(
inviteID,
);
if (O.isNone(teamInvitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
return teamInvitation.value;
}
@Mutation(() => TeamInvitation, {
@@ -106,56 +97,19 @@ export class TeamInvitationResolver {
})
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER)
createTeamInvitation(
@GqlUser()
user: User,
@Args({
name: 'teamID',
description: 'ID of the Team ID to invite from',
type: () => ID,
})
teamID: string,
@Args({
name: 'inviteeEmail',
description: 'Email of the user to invite',
})
inviteeEmail: string,
@Args({
name: 'inviteeRole',
type: () => TeamMemberRole,
description: 'Role to be given to the user',
})
inviteeRole: TeamMemberRole,
async createTeamInvitation(
@GqlUser() user: AuthUser,
@Args() args: CreateTeamInvitationArgs,
): Promise<TeamInvitation> {
return pipe(
TE.Do,
const teamInvitation = await this.teamInvitationService.createInvitation(
user,
args.teamID,
args.inviteeEmail,
args.inviteeRole,
);
// Validate email
TE.bindW('email', () =>
pipe(
EmailCodec.decode(inviteeEmail),
TE.fromEither,
TE.mapLeft(() => INVALID_EMAIL),
),
),
// Validate and get Team
TE.bindW('team', () => this.teamService.getTeamWithIDTE(teamID)),
// Create team
TE.chainW(({ email, team }) =>
this.teamInvitationService.createInvitation(
user,
team,
email,
inviteeRole,
),
),
// If failed, throw err (so the message is passed) else return value
TE.getOrElse(throwErr),
)();
if (E.isLeft(teamInvitation)) throwErr(teamInvitation.left);
return teamInvitation.right;
}
@Mutation(() => Boolean, {
@@ -163,7 +117,7 @@ export class TeamInvitationResolver {
})
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
@RequiresTeamRole(TeamMemberRole.OWNER)
revokeTeamInvitation(
async revokeTeamInvitation(
@Args({
name: 'inviteID',
type: () => ID,
@@ -171,19 +125,19 @@ export class TeamInvitationResolver {
})
inviteID: string,
): Promise<true> {
return pipe(
this.teamInvitationService.revokeInvitation(inviteID),
TE.map(() => true as const),
TE.getOrElse(throwErr),
)();
const isRevoked = await this.teamInvitationService.revokeInvitation(
inviteID,
);
if (E.isLeft(isRevoked)) throwErr(isRevoked.left);
return true;
}
@Mutation(() => TeamMember, {
description: 'Accept an Invitation',
})
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
acceptTeamInvitation(
@GqlUser() user: User,
async acceptTeamInvitation(
@GqlUser() user: AuthUser,
@Args({
name: 'inviteID',
type: () => ID,
@@ -191,10 +145,12 @@ export class TeamInvitationResolver {
})
inviteID: string,
): Promise<TeamMember> {
return pipe(
this.teamInvitationService.acceptInvitation(inviteID, user),
TE.getOrElse(throwErr),
)();
const teamMember = await this.teamInvitationService.acceptInvitation(
inviteID,
user,
);
if (E.isLeft(teamMember)) throwErr(teamMember.left);
return teamMember.right;
}
// Subscriptions

View File

@@ -1,24 +1,25 @@
import { Injectable } from '@nestjs/common';
import * as T from 'fp-ts/Task';
import * as O from 'fp-ts/Option';
import * as TO from 'fp-ts/TaskOption';
import * as TE from 'fp-ts/TaskEither';
import { pipe, flow, constVoid } from 'fp-ts/function';
import * as E from 'fp-ts/Either';
import { PrismaService } from 'src/prisma/prisma.service';
import { Team, TeamMemberRole } from 'src/team/team.model';
import { Email } from 'src/types/Email';
import { User } from 'src/user/user.model';
import { TeamInvitation as DBTeamInvitation } from '@prisma/client';
import { TeamMember, TeamMemberRole } from 'src/team/team.model';
import { TeamService } from 'src/team/team.service';
import {
INVALID_EMAIL,
TEAM_INVALID_ID,
TEAM_INVITE_ALREADY_MEMBER,
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
TEAM_INVITE_MEMBER_HAS_INVITE,
TEAM_INVITE_NO_INVITE_FOUND,
TEAM_MEMBER_NOT_FOUND,
} from 'src/errors';
import { TeamInvitation } from './team-invitation.model';
import { MailerService } from 'src/mailer/mailer.service';
import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { validateEmail } from '../utils';
import { AuthUser } from 'src/types/AuthUser';
@Injectable()
export class TeamInvitationService {
@@ -29,245 +30,221 @@ export class TeamInvitationService {
private readonly mailerService: MailerService,
private readonly pubsub: PubSubService,
) {
this.getInvitation = this.getInvitation.bind(this);
}
) {}
getInvitation(inviteID: string): TO.TaskOption<TeamInvitation> {
return pipe(
() =>
this.prisma.teamInvitation.findUnique({
where: {
id: inviteID,
},
}),
TO.fromTask,
TO.chain(flow(O.fromNullable, TO.fromOption)),
TO.map((x) => x as TeamInvitation),
);
}
getInvitationWithEmail(email: Email, team: Team) {
return pipe(
() =>
this.prisma.teamInvitation.findUnique({
where: {
teamID_inviteeEmail: {
inviteeEmail: email,
teamID: team.id,
},
},
}),
TO.fromTask,
TO.chain(flow(O.fromNullable, TO.fromOption)),
);
}
createInvitation(
creator: User,
team: Team,
inviteeEmail: Email,
inviteeRole: TeamMemberRole,
) {
return pipe(
// Perform all validation checks
TE.sequenceArray([
// creator should be a TeamMember
pipe(
this.teamService.getTeamMemberTE(team.id, creator.uid),
TE.map(constVoid),
),
// Invitee should not be a team member
pipe(
async () => await this.userService.findUserByEmail(inviteeEmail),
TO.foldW(
() => TE.right(undefined), // If no user, short circuit to completion
(user) =>
pipe(
// If user is found, check if team member
this.teamService.getTeamMemberTE(team.id, user.uid),
TE.foldW(
() => TE.right(undefined), // Not team-member, this is good
() => TE.left(TEAM_INVITE_ALREADY_MEMBER), // Is team member, not good
),
),
),
TE.map(constVoid),
),
// Should not have an existing invite
pipe(
this.getInvitationWithEmail(inviteeEmail, team),
TE.fromTaskOption(() => null),
TE.swap,
TE.map(constVoid),
TE.mapLeft(() => TEAM_INVITE_MEMBER_HAS_INVITE),
),
]),
// Create the invitation
TE.chainTaskK(
() => () =>
this.prisma.teamInvitation.create({
data: {
teamID: team.id,
inviteeEmail,
inviteeRole,
creatorUid: creator.uid,
},
}),
),
// Send email, this is a side effect
TE.chainFirstTaskK((invitation) =>
pipe(
this.mailerService.sendMail(inviteeEmail, {
template: 'team-invitation',
variables: {
invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `https://hoppscotch.io/join-team?id=${invitation.id}`,
invite_team_name: team.name,
},
}),
TE.getOrElseW(() => T.of(undefined)), // This value doesn't matter as we don't mind the return value (chainFirst) as long as the task completes
),
),
// Send PubSub topic
TE.chainFirstTaskK((invitation) =>
TE.fromTask(async () => {
const inv: TeamInvitation = {
id: invitation.id,
teamID: invitation.teamID,
creatorUid: invitation.creatorUid,
inviteeEmail: invitation.inviteeEmail,
inviteeRole: TeamMemberRole[invitation.inviteeRole],
};
this.pubsub.publish(`team/${inv.teamID}/invite_added`, inv);
}),
),
// Map to model type
TE.map((x) => x as TeamInvitation),
);
}
revokeInvitation(inviteID: string) {
return pipe(
// Make sure invite exists
this.getInvitation(inviteID),
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
// Delete team invitation
TE.chainTaskK(
() => () =>
this.prisma.teamInvitation.delete({
where: {
id: inviteID,
},
}),
),
// Emit Pubsub Event
TE.chainFirst((invitation) =>
TE.fromTask(() =>
this.pubsub.publish(
`team/${invitation.teamID}/invite_removed`,
invitation.id,
),
),
),
// We are not returning anything
TE.map(constVoid),
);
}
getAllInvitationsInTeam(team: Team) {
return pipe(
() =>
this.prisma.teamInvitation.findMany({
where: {
teamID: team.id,
},
}),
T.map((x) => x as TeamInvitation[]),
);
}
acceptInvitation(inviteID: string, acceptedBy: User) {
return pipe(
TE.Do,
// First get the invitation
TE.bindW('invitation', () =>
pipe(
this.getInvitation(inviteID),
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
),
),
// Validation checks
TE.chainFirstW(({ invitation }) =>
TE.sequenceArray([
// Make sure the invited user is not part of the team
pipe(
this.teamService.getTeamMemberTE(invitation.teamID, acceptedBy.uid),
TE.swap,
TE.bimap(
() => TEAM_INVITE_ALREADY_MEMBER,
constVoid, // The return type is ignored
),
),
// Make sure the invited user and accepting user has the same email
pipe(
undefined,
TE.fromPredicate(
(a) => acceptedBy.email === invitation.inviteeEmail,
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
),
),
]),
),
// Add the team member
// TODO: Somehow bring subscriptions to this ?
TE.bindW('teamMember', ({ invitation }) =>
pipe(
TE.tryCatch(
() =>
this.teamService.addMemberToTeam(
invitation.teamID,
acceptedBy.uid,
invitation.inviteeRole,
),
() => TEAM_INVITE_ALREADY_MEMBER, // Can only fail if Team Member already exists, which we checked, but due to async lets assert that here too
),
),
),
TE.chainFirstW(({ invitation }) => this.revokeInvitation(invitation.id)),
TE.map(({ teamMember }) => teamMember),
);
/**
* Cast a DBTeamInvitation to a TeamInvitation
* @param dbTeamInvitation database TeamInvitation
* @returns TeamInvitation model
*/
cast(dbTeamInvitation: DBTeamInvitation): TeamInvitation {
return {
...dbTeamInvitation,
inviteeRole: TeamMemberRole[dbTeamInvitation.inviteeRole],
};
}
/**
* Fetch the count invitations for a given team.
* @param teamID team id
* @returns a count team invitations for a team
* Get the team invite
* @param inviteID invite id
* @returns an Option of team invitation or none
*/
async getAllTeamInvitations(teamID: string) {
const invitations = await this.prisma.teamInvitation.findMany({
async getInvitation(inviteID: string) {
try {
const dbInvitation = await this.prisma.teamInvitation.findUniqueOrThrow({
where: {
id: inviteID,
},
});
return O.some(this.cast(dbInvitation));
} catch (e) {
return O.none;
}
}
/**
* Get the team invite for an invitee with email and teamID.
* @param inviteeEmail invitee email
* @param teamID team id
* @returns an Either of team invitation for the invitee or error
*/
async getTeamInviteByEmailAndTeamID(inviteeEmail: string, teamID: string) {
const isEmailValid = validateEmail(inviteeEmail);
if (!isEmailValid) return E.left(INVALID_EMAIL);
try {
const teamInvite = await this.prisma.teamInvitation.findUniqueOrThrow({
where: {
teamID_inviteeEmail: {
inviteeEmail: inviteeEmail,
teamID: teamID,
},
},
});
return E.right(teamInvite);
} catch (e) {
return E.left(TEAM_INVITE_NO_INVITE_FOUND);
}
}
/**
* Create a team invitation
* @param creator creator of the invitation
* @param teamID team id
* @param inviteeEmail invitee email
* @param inviteeRole invitee role
* @returns an Either of team invitation or error message
*/
async createInvitation(
creator: AuthUser,
teamID: string,
inviteeEmail: string,
inviteeRole: TeamMemberRole,
) {
// validate email
const isEmailValid = validateEmail(inviteeEmail);
if (!isEmailValid) return E.left(INVALID_EMAIL);
// team ID should valid
const team = await this.teamService.getTeamWithID(teamID);
if (!team) return E.left(TEAM_INVALID_ID);
// invitation creator should be a TeamMember
const isTeamMember = await this.teamService.getTeamMember(
team.id,
creator.uid,
);
if (!isTeamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
// Checking to see if the invitee is already part of the team or not
const inviteeUser = await this.userService.findUserByEmail(inviteeEmail);
if (O.isSome(inviteeUser)) {
// invitee should not already a member
const isTeamMember = await this.teamService.getTeamMember(
team.id,
inviteeUser.value.uid,
);
if (isTeamMember) return E.left(TEAM_INVITE_ALREADY_MEMBER);
}
// check invitee already invited earlier or not
const teamInvitation = await this.getTeamInviteByEmailAndTeamID(
inviteeEmail,
team.id,
);
if (E.isRight(teamInvitation)) return E.left(TEAM_INVITE_MEMBER_HAS_INVITE);
// create the invitation
const dbInvitation = await this.prisma.teamInvitation.create({
data: {
teamID: team.id,
inviteeEmail,
inviteeRole,
creatorUid: creator.uid,
},
});
await this.mailerService.sendEmail(inviteeEmail, {
template: 'team-invitation',
variables: {
invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
invite_team_name: team.name,
},
});
const invitation = this.cast(dbInvitation);
this.pubsub.publish(`team/${invitation.teamID}/invite_added`, invitation);
return E.right(invitation);
}
/**
* Revoke a team invitation
* @param inviteID invite id
* @returns an Either of true or error message
*/
async revokeInvitation(inviteID: string) {
// check if the invite exists
const invitation = await this.getInvitation(inviteID);
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
// delete the invite
await this.prisma.teamInvitation.delete({
where: {
id: inviteID,
},
});
this.pubsub.publish(
`team/${invitation.value.teamID}/invite_removed`,
invitation.value.id,
);
return E.right(true);
}
/**
* Accept a team invitation
* @param inviteID invite id
* @param acceptedBy user who accepted the invitation
* @returns an Either of team member or error message
*/
async acceptInvitation(inviteID: string, acceptedBy: AuthUser) {
// check if the invite exists
const invitation = await this.getInvitation(inviteID);
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
// make sure the user is not already a member of the team
const teamMemberInvitee = await this.teamService.getTeamMember(
invitation.value.teamID,
acceptedBy.uid,
);
if (teamMemberInvitee) return E.left(TEAM_INVITE_ALREADY_MEMBER);
// make sure the user is the same as the invitee
if (
acceptedBy.email.toLowerCase() !==
invitation.value.inviteeEmail.toLowerCase()
)
return E.left(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
// add the user to the team
let teamMember: TeamMember;
try {
teamMember = await this.teamService.addMemberToTeam(
invitation.value.teamID,
acceptedBy.uid,
invitation.value.inviteeRole,
);
} catch (e) {
return E.left(TEAM_INVITE_ALREADY_MEMBER);
}
// delete the invite
await this.revokeInvitation(inviteID);
return E.right(teamMember);
}
/**
* Fetch all team invitations for a given team.
* @param teamID team id
* @returns array of team invitations for a team
*/
async getTeamInvitations(teamID: string) {
const dbInvitations = await this.prisma.teamInvitation.findMany({
where: {
teamID: teamID,
},
});
const invitations: TeamInvitation[] = dbInvitations.map((dbInvitation) =>
this.cast(dbInvitation),
);
return invitations;
}
}

View File

@@ -1,21 +1,21 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { pipe } from 'fp-ts/function';
import { TeamService } from 'src/team/team.service';
import { TeamInvitationService } from './team-invitation.service';
import * as O from 'fp-ts/Option';
import * as T from 'fp-ts/Task';
import * as TE from 'fp-ts/TaskEither';
import { GqlExecutionContext } from '@nestjs/graphql';
import {
BUG_AUTH_NO_USER_CTX,
BUG_TEAM_INVITE_NO_INVITE_ID,
TEAM_INVITE_NO_INVITE_FOUND,
TEAM_MEMBER_NOT_FOUND,
TEAM_NOT_REQUIRED_ROLE,
} from 'src/errors';
import { User } from 'src/user/user.model';
import { throwErr } from 'src/utils';
import { TeamMemberRole } from 'src/team/team.model';
/**
* This guard only allows team owner to execute the resolver
*/
@Injectable()
export class TeamInviteTeamOwnerGuard implements CanActivate {
constructor(
@@ -24,48 +24,30 @@ export class TeamInviteTeamOwnerGuard implements CanActivate {
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
return pipe(
TE.Do,
// Get GQL context
const gqlExecCtx = GqlExecutionContext.create(context);
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
// Get user
const { user } = gqlExecCtx.getContext().req;
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
// Get the invite
TE.bindW('invite', ({ gqlCtx }) =>
pipe(
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
TE.chainW((inviteID) =>
pipe(
this.teamInviteService.getInvitation(inviteID),
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
),
),
),
),
// Get the invite
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
TE.bindW('user', ({ gqlCtx }) =>
pipe(
gqlCtx.getContext().req.user,
O.fromNullable,
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
),
),
const invitation = await this.teamInviteService.getInvitation(inviteID);
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
TE.bindW('userMember', ({ invite, user }) =>
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
),
// Fetch team member details of this user
const teamMember = await this.teamService.getTeamMember(
invitation.value.teamID,
user.uid,
);
TE.chainW(
TE.fromPredicate(
({ userMember }) => userMember.role === TeamMemberRole.OWNER,
() => TEAM_NOT_REQUIRED_ROLE,
),
),
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
if (teamMember.role !== TeamMemberRole.OWNER)
throwErr(TEAM_NOT_REQUIRED_ROLE);
TE.fold(
(err) => throwErr(err),
() => T.of(true),
),
)();
return true;
}
}

View File

@@ -1,20 +1,23 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { TeamInvitationService } from './team-invitation.service';
import { pipe, flow } from 'fp-ts/function';
import * as TE from 'fp-ts/TaskEither';
import * as T from 'fp-ts/Task';
import * as O from 'fp-ts/Option';
import { GqlExecutionContext } from '@nestjs/graphql';
import {
BUG_AUTH_NO_USER_CTX,
BUG_TEAM_INVITE_NO_INVITE_ID,
TEAM_INVITE_NOT_VALID_VIEWER,
TEAM_INVITE_NO_INVITE_FOUND,
TEAM_MEMBER_NOT_FOUND,
} from 'src/errors';
import { User } from 'src/user/user.model';
import { throwErr } from 'src/utils';
import { TeamService } from 'src/team/team.service';
/**
* This guard only allows user to execute the resolver
* 1. If user is invitee, allow
* 2. Or else, if user is team member, allow
*
* TLDR: Allow if user is invitee or team member
*/
@Injectable()
export class TeamInviteViewerGuard implements CanActivate {
constructor(
@@ -23,50 +26,32 @@ export class TeamInviteViewerGuard implements CanActivate {
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
return pipe(
TE.Do,
// Get GQL context
const gqlExecCtx = GqlExecutionContext.create(context);
// Get GQL Context
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
// Get user
const { user } = gqlExecCtx.getContext().req;
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
// Get user
TE.bindW('user', ({ gqlCtx }) =>
pipe(
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
),
),
// Get the invite
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
// Get the invite
TE.bindW('invite', ({ gqlCtx }) =>
pipe(
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
TE.chainW(
flow(
this.teamInviteService.getInvitation,
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
),
),
),
),
const invitation = await this.teamInviteService.getInvitation(inviteID);
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
// Check if the user and the invite email match, else if we can resolver the user as a team member
// any better solution ?
TE.chainW(({ user, invite }) =>
user.email?.toLowerCase() === invite.inviteeEmail.toLowerCase()
? TE.of(true)
: pipe(
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
TE.map(() => true),
),
),
// Check if the user and the invite email match, else if user is a team member
if (
user.email?.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
) {
const teamMember = await this.teamService.getTeamMember(
invitation.value.teamID,
user.uid,
);
TE.mapLeft((e) =>
e === 'team/member_not_found' ? TEAM_INVITE_NOT_VALID_VIEWER : e,
),
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
}
TE.fold(throwErr, () => T.of(true)),
)();
return true;
}
}

View File

@@ -1,11 +1,7 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { TeamInvitationService } from './team-invitation.service';
import { pipe, flow } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import * as T from 'fp-ts/Task';
import * as TE from 'fp-ts/TaskEither';
import { GqlExecutionContext } from '@nestjs/graphql';
import { User } from 'src/user/user.model';
import {
BUG_AUTH_NO_USER_CTX,
BUG_TEAM_INVITE_NO_INVITE_ID,
@@ -24,44 +20,26 @@ export class TeamInviteeGuard implements CanActivate {
constructor(private readonly teamInviteService: TeamInvitationService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
return pipe(
TE.Do,
// Get GQL Context
const gqlExecCtx = GqlExecutionContext.create(context);
// Get execution context
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
// Get user
const { user } = gqlExecCtx.getContext().req;
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
// Get user
TE.bindW('user', ({ gqlCtx }) =>
pipe(
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
),
),
// Get the invite
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
// Get invite
TE.bindW('invite', ({ gqlCtx }) =>
pipe(
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
TE.chainW(
flow(
this.teamInviteService.getInvitation,
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
),
),
),
),
const invitation = await this.teamInviteService.getInvitation(inviteID);
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
// Check if the emails match
TE.chainW(
TE.fromPredicate(
({ user, invite }) => user.email === invite.inviteeEmail,
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
),
),
if (
user.email.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
) {
throwErr(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
}
// Fold it to a promise
TE.fold(throwErr, () => T.of(true)),
)();
return true;
}
}

View File

@@ -12,6 +12,6 @@ export class TeamTeamInviteExtResolver {
complexity: 10,
})
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
return this.teamInviteService.getAllInvitationsInTeam(team)();
return this.teamInviteService.getTeamInvitations(team.id);
}
}

View File

@@ -2,7 +2,7 @@ import { HttpStatus } from '@nestjs/common';
/**
** Custom interface to handle errors specific to Auth module
** Since its REST we need to return HTTP status code along with error message
** Since its REST we need to return the HTTP status code along with the error message
*/
export type AuthError = {
message: string;

View File

@@ -24,6 +24,8 @@ beforeEach(() => {
mockPubSub.publish.mockClear();
});
const date = new Date();
describe('UserHistoryService', () => {
describe('fetchUserHistory', () => {
test('Should return a list of users REST history if exists', async () => {
@@ -360,13 +362,15 @@ describe('UserHistoryService', () => {
});
describe('removeRequestFromHistory', () => {
test('Should resolve right and delete request from users history', async () => {
const executedOn = new Date();
mockPrisma.userHistory.delete.mockResolvedValueOnce({
userUid: 'abc',
id: '1',
request: [{}],
responseMetadata: [{}],
reqType: ReqType.REST,
executedOn: new Date(),
executedOn: executedOn,
isStarred: false,
});
@@ -376,7 +380,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST,
executedOn: new Date(),
executedOn: executedOn,
isStarred: false,
};
@@ -384,7 +388,7 @@ describe('UserHistoryService', () => {
await userHistoryService.removeRequestFromHistory('abc', '1'),
).toEqualRight(userHistory);
});
test('Should resolve left and error out when req id is invalid ', async () => {
test('Should resolve left and error out when req id is invalid', async () => {
mockPrisma.userHistory.delete.mockResolvedValueOnce(null);
return expect(
@@ -398,7 +402,7 @@ describe('UserHistoryService', () => {
request: [{}],
responseMetadata: [{}],
reqType: ReqType.REST,
executedOn: new Date(),
executedOn: date,
isStarred: false,
});
@@ -408,7 +412,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST,
executedOn: new Date(),
executedOn: date,
isStarred: false,
};

View File

@@ -9,7 +9,13 @@ import * as E from 'fp-ts/Either';
import * as A from 'fp-ts/Array';
import { TeamMemberRole } from './team/team.model';
import { User } from './user/user.model';
import { JSON_INVALID } from './errors';
import {
ENV_EMPTY_AUTH_PROVIDERS,
ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
ENV_NOT_SUPPORT_AUTH_PROVIDERS,
JSON_INVALID,
} from './errors';
import { AuthProvider } from './auth/helper';
/**
* A workaround to throw an exception in an expression.
@@ -152,3 +158,31 @@ export function isValidLength(title: string, length: number) {
return true;
}
/**
* This function is called by bootstrap() in main.ts
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
* If not, it throws an error.
*/
export function checkEnvironmentAuthProvider() {
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
}
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
}
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
',',
).map((provider) => provider.toLocaleUpperCase());
const supportedAuthProviders = Object.values(AuthProvider).map(
(provider: string) => provider.toLocaleUpperCase(),
);
for (const givenAuthProvider of givenAuthProviders) {
if (!supportedAuthProviders.includes(givenAuthProvider)) {
throw new Error(ENV_NOT_SUPPORT_AUTH_PROVIDERS);
}
}
}

View File

@@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
support@hoppscotch.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,29 +1,19 @@
<div align="center">
<a href="https://hoppscotch.io">
<img
src="https://avatars.githubusercontent.com/u/56705483"
alt="Hoppscotch Logo"
height="64"
/>
</a>
</div>
<div align="center">
# Hoppscotch CLI <font size=2><sup>ALPHA</sup></font>
</div>
A CLI to run Hoppscotch test scripts in CI environments.
A CLI to run Hoppscotch Test Scripts in CI environments.
### **Commands:**
- `hopp test [options] [file]`: testing hoppscotch collection.json file
### **Usage:**
```
```bash
hopp [options or commands] arguments
```
### **Options:**
- `-v`, `--ver`: see the current version of the CLI
- `-h`, `--help`: display help for command
@@ -45,17 +35,21 @@ hopp [options or commands] arguments
- Executes and outputs test-script response.
#### Options:
##### `-e <file_path>` / `--env <file_path>`
- Accepts path to env.json with contents in below format:
```json
{
"ENV1":"value1",
"ENV2":"value2"
}
```
- You can now access those variables using `pw.env.get('<var_name>')`
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
## Install
@@ -75,4 +69,59 @@ npm i -g @hoppscotch/cli
## **Contributing:**
To get started contributing to the repository, please read **[CONTRIBUTING.md](./CONTRIBUTING.md)**
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org).
4. You may merge the Pull Request once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer merge it for you.
## Set Up The Development Environment
1. After cloning the repository, execute the following commands:
```bash
pnpm install
pnpm run build
```
2. In order to test locally, you can use two types of package linking:
1. The 'pnpm exec' way (preferred since it does not hamper your original installation of the CLI):
```bash
pnpm link @hoppscotch/cli
// Then to use or test the CLI:
pnpm exec hopp
// After testing, to remove the package linking:
pnpm rm @hoppscotch/cli
```
2. The 'global' way (warning: this might override the globally installed CLI, if exists):
```bash
sudo pnpm link --global
// Then to use or test the CLI:
hopp
// After testing, to remove the package linking:
sudo pnpm rm --global @hoppscotch/cli
```
3. To use the Typescript watch scripts:
```bash
pnpm run dev
```

View File

@@ -17,7 +17,7 @@ const CLI_BEFORE_ALL_TXT = `hopp: The ${accent(
)}) ${chalk.black.bold.bgYellowBright(" ALPHA ")} \n`;
const CLI_AFTER_ALL_TXT = `\nFor more help, head on to ${accent(
"https://docs.hoppscotch.io/cli"
"https://docs.hoppscotch.io/documentation/clients/cli"
)}`;
program
@@ -59,7 +59,9 @@ program
.description("running hoppscotch collection.json file")
.addHelpText(
"after",
`\nFor help, head on to ${accent("https://docs.hoppscotch.io/cli#test")}`
`\nFor help, head on to ${accent(
"https://docs.hoppscotch.io/documentation/clients/cli#commands"
)}`
)
.action(async (path, options) => await test(path, options)());

View File

@@ -6,7 +6,6 @@ module.exports = {
env: {
browser: true,
node: true,
jest: true,
},
parserOptions: {
sourceType: "module",
@@ -30,8 +29,18 @@ module.exports = {
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
"no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier":
"prettier/prettier": [
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{},
{
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2,
},
],
"vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off",

View File

@@ -1,3 +1,8 @@
module.exports = {
semi: false
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -4,6 +4,7 @@
@apply after:backface-hidden;
@apply selection:bg-accentDark;
@apply selection:text-accentContrast;
@apply overscroll-none;
}
:root {
@@ -165,12 +166,6 @@ a {
@apply truncate;
@apply sm:inline-flex;
}
.env-icon {
@apply transition;
@apply inline-flex;
@apply items-center;
}
}
.tippy-svg-arrow {
@@ -189,10 +184,11 @@ a {
@apply border-solid border-dividerDark;
@apply rounded;
@apply shadow-lg;
@apply max-w-[45vw] #{!important};
.tippy-content {
@apply flex flex-col;
@apply max-h-56;
@apply max-h-[45vh];
@apply items-stretch;
@apply overflow-y-auto;
@apply text-secondary text-body;
@@ -200,6 +196,10 @@ a {
@apply leading-normal;
@apply focus:outline-none;
scroll-behavior: smooth;
& > span {
@apply block #{!important};
}
}
.tippy-svg-arrow {
@@ -215,6 +215,7 @@ a {
[data-v-tippy] {
@apply flex flex-1;
@apply truncate;
}
[interactive] > div {
@@ -325,7 +326,7 @@ pre.ace_editor {
@apply after:font-icon;
@apply after:text-current;
@apply after:right-3;
@apply after:content-["\e313"];
@apply after:content-["\e5cf"];
@apply after:text-lg;
}
@@ -459,6 +460,14 @@ pre.ace_editor {
@apply bg-dividerLight;
}
.splitpanes--horizontal .splitpanes__pane {
@apply transition-none;
}
.splitpanes--vertical .splitpanes__pane {
@apply transition-none;
}
.cm-focused {
@apply select-auto;
@apply outline-none #{!important};
@@ -472,6 +481,10 @@ pre.ace_editor {
}
}
.cm-scroller {
@apply overscroll-y-auto;
}
.cm-editor {
.cm-line::selection {
@apply bg-accentDark #{!important};
@@ -559,3 +572,11 @@ details[open] summary .indicator {
@apply rounded;
@apply border-0;
}
.gql-operation-not-highlight {
@apply opacity-50;
}
.gql-operation-highlight {
@apply opacity-100;
}

View File

@@ -1,8 +1,21 @@
@mixin base-theme {
--font-sans: "Inter", sans-serif;
--font-mono: "Roboto Mono", monospace;
--font-icon: "Material Icons";
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
--font-sans: "Inter Variable", sans-serif;
--font-icon: "Material Symbols Rounded Variable";
--font-mono: "Roboto Mono Variable", monospace;
--font-size-body: 0.75rem;
--font-size-tiny: 0.688rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin dark-theme {
@@ -200,23 +213,26 @@
:root {
@include base-theme;
@include dark-theme;
@include green-theme;
@include dark-editor-theme;
@include green-theme;
}
:root.light {
@include light-theme;
@include light-editor-theme;
color-scheme: light;
}
:root.dark {
@include dark-theme;
@include dark-editor-theme;
color-scheme: dark;
}
:root.black {
@include black-theme;
@include black-editor-theme;
color-scheme: dark;
}
:root[data-accent="blue"] {
@@ -254,63 +270,3 @@
:root[data-accent="yellow"] {
@include yellow-theme;
}
@mixin font-small {
--font-size-body: 0.75rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin font-medium {
--font-size-body: 0.875rem;
--line-height-body: 1.25rem;
--upper-primary-sticky-fold: 4.375rem;
--upper-secondary-sticky-fold: 6.688rem;
--upper-tertiary-sticky-fold: 9rem;
--upper-mobile-primary-sticky-fold: 7.125rem;
--upper-mobile-secondary-sticky-fold: 9.438rem;
--upper-mobile-sticky-fold: 11.75rem;
--upper-mobile-tertiary-sticky-fold: 9rem;
--lower-primary-sticky-fold: 3.25rem;
--lower-secondary-sticky-fold: 5.563rem;
--lower-tertiary-sticky-fold: 7.875rem;
--sidebar-primary-sticky-fold: 2.25rem;
}
@mixin font-large {
--font-size-body: 1rem;
--line-height-body: 1.5rem;
--upper-primary-sticky-fold: 4.625rem;
--upper-secondary-sticky-fold: 7.188rem;
--upper-tertiary-sticky-fold: 9.75rem;
--upper-mobile-primary-sticky-fold: 7.625rem;
--upper-mobile-secondary-sticky-fold: 10.188rem;
--upper-mobile-sticky-fold: 12.75rem;
--upper-mobile-tertiary-sticky-fold: 9.75rem;
--lower-primary-sticky-fold: 3.5rem;
--lower-secondary-sticky-fold: 6.063rem;
--lower-tertiary-sticky-fold: 8.625rem;
--sidebar-primary-sticky-fold: 2.5rem;
}
:root[data-font-size="small"] {
@include font-small;
}
:root[data-font-size="medium"] {
@include font-medium;
}
:root[data-font-size="large"] {
@include font-large;
}

View File

@@ -19,6 +19,7 @@
"edit": "Redigeer",
"filter": "Filter",
"go_back": "Gaan terug",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Etiket",
"learn_more": "Leer meer",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Versameling geskep",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Wysig versameling",
"invalid_name": "Gee 'n geldige naam vir die versameling",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "My versamelings",
"name": "My nuwe versameling",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Nuwe versameling",
"order_changed": "Collection Order Updated",
"renamed": "Versameling hernoem",
"request_in_use": "Request in use",
"save_as": "Stoor as",
@@ -142,6 +147,7 @@
"remove_team": "Weet u seker dat u hierdie span wil uitvee?",
"remove_telemetry": "Weet u seker dat u van Telemetry wil afskakel?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Is u seker dat u hierdie werkruimte wil sinkroniseer?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Zen -modus"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Versamelings",
"confirm": "Bevestig",
"edit_request": "Wysig versoek",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Kopieer skakel",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Duur",
"enter_curl": "Voer cURL in",
"generate_code": "Genereer kode",
@@ -396,8 +405,10 @@
"header_list": "Koplys",
"invalid_name": "Gee 'n naam vir die versoek",
"method": "Metode",
"moved": "Request moved",
"name": "Versoek naam",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Reaksie liggaam",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Opskrifte",
@@ -434,6 +446,7 @@
"status": "Status",
"time": "Tyd",
"title": "Reaksie",
"video": "Video",
"waiting_for_connection": "wag vir verbinding",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Liggaam",
"collections": "Versamelings",
"documentation": "Dokumentasie",
"environments": "Environments",
"headers": "Opskrifte",
"history": "Geskiedenis",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Verlaat span",
"exit_disabled": "Slegs eienaar kan nie die span verlaat nie",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Die e -posformaat is ongeldig",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
@@ -675,6 +690,7 @@
"member_removed": "Gebruiker verwyder",
"member_role_updated": "Gebruikersrolle opgedateer",
"members": "Lede",
"more_members": "+{count} more",
"name_length_insufficient": "Spannaam moet ten minste 6 karakters lank wees",
"name_updated": "Team name updated",
"new": "Nuwe span",
@@ -682,10 +698,13 @@
"new_name": "My nuwe span",
"no_access": "U het nie redigeertoegang tot hierdie versamelings nie",
"no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "Toestemmings",
"same_target_destination": "Same target and destination",
"saved": "Span gered",
"select_a_team": "Select a team",
"title": "Spanne",
@@ -713,5 +732,11 @@
"message": "Boodskap",
"protocols": "Protokolle",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "يحرر",
"filter": "Filter",
"go_back": "عد",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "ملصق",
"learn_more": "اقرأ أكثر",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "تم إنشاء المجموعة",
"different_parent": "Cannot reorder collection with different parent",
"edit": "تحرير المجموعة",
"invalid_name": "الرجاء تقديم اسم صالح للمجموعة",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "مجموعاتي",
"name": "مجموعتي الجديدة",
"name_length_insufficient": "اسم المجموعة يجب ان لايقل على 3 رموز",
"new": "مجموعة جديدة",
"order_changed": "Collection Order Updated",
"renamed": "تمت إعادة تسمية المجموعة",
"request_in_use": "Request in use",
"save_as": "حفظ باسم",
@@ -142,6 +147,7 @@
"remove_team": "هل أنت متأكد أنك تريد حذف هذا الفريق؟",
"remove_telemetry": "هل أنت متأكد أنك تريد الانسحاب من القياس عن بعد؟",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "هل أنت متأكد أنك تريد مزامنة مساحة العمل هذه؟"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "وضع Zen"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "المجموعات",
"confirm": "يتأكد",
"edit_request": "تحرير الطلب",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "نسخ الوصلة",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "مدة",
"enter_curl": "أدخل cURL",
"generate_code": "إنشاء التعليمات البرمجية",
@@ -396,8 +405,10 @@
"header_list": "قائمة الرأس",
"invalid_name": "يرجى تقديم اسم للطلب",
"method": "طريقة",
"moved": "Request moved",
"name": "اسم الطلب",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "هيئة الاستجابة",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "الرؤوس",
@@ -434,6 +446,7 @@
"status": "حالة",
"time": "وقت",
"title": "إجابة",
"video": "Video",
"waiting_for_connection": "في انتظار الاتصال",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "الجسم",
"collections": "المجموعات",
"documentation": "توثيق",
"environments": "Environments",
"headers": "الرؤوس",
"history": "تاريخ",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "البريد الإلكتروني لا يتوافق مع معلومات حسابك. اتصل بمدير الفريق.",
"exit": "فريق الخروج",
"exit_disabled": "فقط المالك لا يمكنه الخروج من الفريق",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "تنسيق البريد الإلكتروني غير صالح",
"invalid_id": "معرف الفريق غير صالح. اتصل بمدير الفريق.",
"invalid_invite_link": "رابط الدعوة غير صالح",
@@ -675,6 +690,7 @@
"member_removed": "تمت إزالة المستخدم",
"member_role_updated": "تم تحديث أدوار المستخدم",
"members": "أعضاء",
"more_members": "+{count} more",
"name_length_insufficient": "يجب أن يتكون اسم الفريق من 6 أحرف على الأقل",
"name_updated": "تم تحديث اسم الفريق",
"new": "فريق جديد",
@@ -682,10 +698,13 @@
"new_name": "فريقي الجديد",
"no_access": "ليس لديك حق التعديل في هذه المجموعات",
"no_invite_found": "لم يتم العثور على الدعوة. اتصل بمدير الفريق",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "دعوات معلقة",
"permissions": "أذونات",
"same_target_destination": "Same target and destination",
"saved": "فريق حفظ",
"select_a_team": "اختر فريق",
"title": "فرق",
@@ -713,5 +732,11 @@
"message": "رسالة",
"protocols": "البروتوكولات",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Editar",
"filter": "Filtrar resposta",
"go_back": "Tornar",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Etiquetar",
"learn_more": "Aprèn més",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Col·lecció creada",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Editar la col·lecció",
"invalid_name": "Proporcioneu un nom vàlid per a la col·lecció",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Les meves col·leccions",
"name": "La meva nova col·lecció",
"name_length_insufficient": "El nom de la col·lecció ha de tenir almenys 3 caràcters",
"new": "Nova col · lecció",
"order_changed": "Collection Order Updated",
"renamed": "S'ha canviat el nom de la col·lecció",
"request_in_use": "Request in use",
"save_as": "Guardar com",
@@ -142,6 +147,7 @@
"remove_team": "Està segur que vol suprimir aquest equip?",
"remove_telemetry": "Està segur que vol desactivar Telemetry?",
"request_change": "Està segur que vol descartar la sol·licitud actual, els canvis no desats es perdran.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Està segur que vol sincronitzar aquest espai de treball?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Mode Zen"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Col·leccions",
"confirm": "Confirmar",
"edit_request": "Sol·licitud d'edició",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Copia l'enllaç",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Durada",
"enter_curl": "Introduïu cURL",
"generate_code": "Generar codi",
@@ -396,8 +405,10 @@
"header_list": "Llista de capçaleres",
"invalid_name": "Proporcioneu un nom per a la sol·licitud",
"method": "Mètode",
"moved": "Request moved",
"name": "Sol·licita el nom",
"new": "Nova sol·licitud",
"order_changed": "Request Order Updated",
"override": "Sobreescriure",
"override_help": "Estableix <kbd>Content-Type</kbd> a les capçaleres (Headers)",
"overriden": "Sobreescrit",
@@ -421,6 +432,7 @@
"view_my_links": "Visualitzar els meus enllaços"
},
"response": {
"audio": "Audio",
"body": "Cos de resposta",
"filter_response_body": "Filtrar el cos de la resposta JSON (utilitza la sintaxi JSONPath)",
"headers": "Capçaleres",
@@ -434,6 +446,7 @@
"status": "Estat",
"time": "Temps",
"title": "Resposta",
"video": "Video",
"waiting_for_connection": "esperant la connexió",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Cos",
"collections": "Col·leccions",
"documentation": "Documentació",
"environments": "Environments",
"headers": "Capçaleres",
"history": "Historial",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "El correu electrònic no coincideix amb les dades del vostre compte. Contacta amb el propietari del teu equip.",
"exit": "Sortir de l'equip",
"exit_disabled": "L'únic propietari no pot sortir de l'equip",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "El format del correu electrònic no és vàlid",
"invalid_id": "Identificador d'equip no vàlid. Contacta amb el propietari del teu equip.",
"invalid_invite_link": "Enllaç d'invitació no vàlid",
@@ -675,6 +690,7 @@
"member_removed": "S'ha eliminat l'usuari",
"member_role_updated": "Rols d'usuari actualitzats",
"members": "Membres",
"more_members": "+{count} more",
"name_length_insufficient": "El nom de l'equip ha de tenir com a mínim 6 caràcters",
"name_updated": "S'ha actualitzat el nom de l'equip",
"new": "Nou equip",
@@ -682,10 +698,13 @@
"new_name": "El meu Nou Equip",
"no_access": "No teniu accés d'edició a aquestes col·leccions",
"no_invite_found": "No s'ha trobat la invitació. Contacta amb el propietari del teu equip.",
"no_request_found": "Request not found.",
"not_found": "No s'ha trobat l'equip. Contacta amb el propietari del teu equip.",
"not_valid_viewer": "No ets un espectador vàlid. Contacta amb el propietari del teu equip.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Invitacions pendents",
"permissions": "Permisos",
"same_target_destination": "Same target and destination",
"saved": "S'ha guardat l'equip",
"select_a_team": "Select a team",
"title": "Equips",
@@ -713,5 +732,11 @@
"message": "Missatge",
"protocols": "Protocols",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "编辑",
"filter": "过滤",
"go_back": "返回",
"go_forward": "前进",
"group_by": "分组方式",
"label": "标签",
"learn_more": "了解更多",
@@ -39,9 +40,9 @@
"start": "开始",
"starting": "正在开始",
"stop": "停止",
"to_close": "关闭",
"to_navigate": "定位",
"to_select": "选择",
"to_close": "关闭",
"to_navigate": "定位",
"to_select": "选择",
"turn_off": "关闭",
"turn_on": "开启",
"undo": "撤消",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "集合已创建",
"different_parent": "不能用不同的父类来重新排序集合",
"edit": "编辑集合",
"invalid_name": "请提供有效的集合名称",
"invalid_root_move": "该集合已经在根级了",
"moved": "移动完成",
"my_collections": "我的集合",
"name": "我的新集合",
"name_length_insufficient": "集合名字至少需要 3 个字符",
"new": "新建集合",
"order_changed": "集合顺序已更新",
"renamed": "集合已更名",
"request_in_use": "请求正在使用中",
"save_as": "另存为",
@@ -142,6 +147,7 @@
"remove_team": "你确定要删除该团队吗?",
"remove_telemetry": "你确定要退出遥测服务吗?",
"request_change": "你确定你要放弃当前的请求,未保存的修改将被丢失。",
"save_unsaved_tab": "你想保存在此标签页中所作的修改吗?",
"sync": "您确定要同步该工作区吗?"
},
"count": {
@@ -171,7 +177,7 @@
"members": "团队为空",
"parameters": "该请求没有任何参数",
"pending_invites": "此团队无待办邀请",
"profile": "登录以查看你的个人档案",
"profile": "登录以查看你的个人资料",
"protocols": "协议为空",
"schema": "连接至 GraphQL 端点",
"shortcodes": "Shortcodes 为空",
@@ -203,7 +209,7 @@
"browser_support_sse": "该浏览器似乎不支持 SSE。",
"check_console_details": "检查控制台日志以获悉详情",
"curl_invalid_format": "cURL 格式不正确",
"danger_zone": "Danger zone",
"danger_zone": "危险区域",
"delete_account": "您的帐号目前为这些团队的拥有者:",
"delete_account_description": "您在删除帐号前必须先将您自己从团队中移除、转移拥有权,或是删除团队。",
"empty_req_name": "空请求名称",
@@ -213,7 +219,7 @@
"incorrect_email": "电子邮箱错误",
"invalid_link": "无效链接",
"invalid_link_description": "你点击的链接无效或已过期。",
"json_parsing_failed": "Invalid JSON",
"json_parsing_failed": "不合法的 JSON",
"json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试",
"network_error": "好像发生了网络错误,请重试。",
"network_fail": "无法发送请求",
@@ -310,13 +316,14 @@
"zen_mode": "ZEN 模式"
},
"modal": {
"close_unsaved_tab": "有未保存的变更",
"collections": "集合",
"confirm": "确认",
"edit_request": "编辑请求",
"import_export": "导入/导出"
},
"mqtt": {
"already_subscribed": "您已经订阅了此主。",
"already_subscribed": "您已经订阅了此主。",
"clean_session": "清除会话",
"clear_input": "清除输入",
"clear_input_on_send": "发送后清除输入",
@@ -348,7 +355,7 @@
"navigation": {
"doc": "文档",
"graphql": "GraphQL",
"profile": "个人档案",
"profile": "个人资料",
"realtime": "实时",
"rest": "REST",
"settings": "设置"
@@ -370,7 +377,7 @@
"owner_description": "所有者可以添加、编辑和删除请求、集合及团队成员。",
"roles": "角色",
"roles_description": "角色用以控制共享集合的访问权限。",
"updated": "档案已更新",
"updated": "已更新",
"viewer": "查看者",
"viewer_description": "查看者只可查看与使用请求。"
},
@@ -389,6 +396,8 @@
"text": "文字"
},
"copy_link": "复制链接",
"different_collection": "不能对来自不同集合的请求进行重新排序",
"duplicated": "重复的请求",
"duration": "持续时间",
"enter_curl": "输入 cURL",
"generate_code": "生成代码",
@@ -396,8 +405,10 @@
"header_list": "请求头列表",
"invalid_name": "请提供请求名称",
"method": "方法",
"moved": "请求移动完成",
"name": "请求名称",
"new": "新请求",
"order_changed": "请求顺序更新完成",
"override": "覆盖",
"override_help": "设置 <kbd>Content-Type</kbd> 头",
"overriden": "覆盖",
@@ -421,6 +432,7 @@
"view_my_links": "查看我的链接"
},
"response": {
"audio": "Audio",
"body": "响应体",
"filter_response_body": "筛选JSON响应本体使用JSONPath语法",
"headers": "响应头",
@@ -434,6 +446,7 @@
"status": "状态",
"time": "时间",
"title": "响应",
"video": "Video",
"waiting_for_connection": "等待连接",
"xml": "XML"
},
@@ -468,10 +481,10 @@
"language": "语言",
"light_mode": "亮色",
"official_proxy_hosting": "官方代理由 Hoppscotch 托管。",
"profile": "个人档案",
"profile_description": "更新你的档案详情",
"profile": "个人资料",
"profile_description": "更新你的资料",
"profile_email": "电子邮箱地址",
"profile_name": "档案名称",
"profile_name": "名称",
"proxy": "网络代理",
"proxy_url": "代理网址",
"proxy_use_toggle": "使用代理中间件发送请求",
@@ -521,7 +534,7 @@
"documentation": "前往文档页面",
"forward": "前往下一页面",
"graphql": "前往 GraphQL 页面",
"profile": "前往个人档案页面",
"profile": "前往个人资料页面",
"realtime": "前往实时页面",
"rest": "前往 REST 页面",
"settings": "前往设置页面",
@@ -563,7 +576,7 @@
},
"socketio": {
"communication": "通讯",
"connection_not_authorized": "此SocketIO连接未使用任何验证。",
"connection_not_authorized": "此 SocketIO 连接未使用任何验证。",
"event_name": "事件名称",
"events": "事件",
"log": "日志",
@@ -603,12 +616,12 @@
"none": "无",
"nothing_found": "没有找到",
"published_error": "将信息:{topic}发布至主题:{message}时发生错误",
"published_message": "已将此信息:{message}发布至主题:{topic}",
"published_message": "已将此信息:{message} 发布至主题:{topic}",
"reconnection_error": "重连失败",
"subscribed_failed": "无法订阅此主{topic}",
"subscribed_success": "成功订阅此主{topic}",
"unsubscribed_failed": "无法取消订阅此主{topic}",
"unsubscribed_success": "成功取消订阅此主{topic}",
"subscribed_failed": "无法订阅此主{topic}",
"subscribed_success": "成功订阅此主{topic}",
"unsubscribed_failed": "无法取消订阅此主{topic}",
"unsubscribed_success": "成功取消订阅此主{topic}",
"waiting_send_request": "等待发送请求"
},
"support": {
@@ -628,6 +641,7 @@
"body": "请求体",
"collections": "集合",
"documentation": "帮助文档",
"environments": "环境",
"headers": "请求头",
"history": "历史记录",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队者。",
"exit": "退出团队",
"exit_disabled": "团队所有者无法退出团队",
"invalid_coll_id": "无效的集合 ID",
"invalid_email_format": "电子邮箱格式无效",
"invalid_id": "无效的团队 ID请联系你的团队者。",
"invalid_invite_link": "无效的邀请链接",
@@ -675,6 +690,7 @@
"member_removed": "用户已移除",
"member_role_updated": "用户角色已更新",
"members": "成员",
"more_members": "+{count} 更多",
"name_length_insufficient": "团队名称至少为 6 个字符",
"name_updated": "团队名称已更新",
"new": "新团队",
@@ -682,10 +698,13 @@
"new_name": "我的新团队",
"no_access": "你没有编辑集合的权限",
"no_invite_found": "未找到邀请。请联系你的团队者。",
"no_request_found": "请求不存在",
"not_found": "没有找到团队,请联系您的团队所有者。",
"not_valid_viewer": "你不是有效的查看者。请联系你的团队者。",
"parent_coll_move": "不能将集合移动到一个子集合",
"pending_invites": "待办邀请",
"permissions": "权限",
"same_target_destination": "目标相同",
"saved": "团队已保存",
"select_a_team": "选择团队",
"title": "团队",
@@ -713,5 +732,11 @@
"message": "信息",
"protocols": "协议",
"url": "URL"
},
"workspace": {
"change": "切换工作空间",
"personal": "我的工作空间",
"team": "团队工作空间",
"title": "工作空间"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Upravit",
"filter": "Filter",
"go_back": "Vrať se",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Označení",
"learn_more": "Další informace",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Kolekce vytvořena",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Upravit sbírku",
"invalid_name": "Uveďte prosím platný název kolekce",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Moje sbírky",
"name": "Moje nová kolekce",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Nová kolekce",
"order_changed": "Collection Order Updated",
"renamed": "Sbírka přejmenována",
"request_in_use": "Request in use",
"save_as": "Uložit jako",
@@ -142,6 +147,7 @@
"remove_team": "Opravdu chcete tento tým smazat?",
"remove_telemetry": "Opravdu se chcete odhlásit z telemetrie?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Opravdu chcete synchronizovat tento pracovní prostor?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Zenový režim"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Sbírky",
"confirm": "Potvrdit",
"edit_request": "Upravit požadavek",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Kopírovat odkaz",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Doba trvání",
"enter_curl": "Zadejte cURL",
"generate_code": "Vygenerujte kód",
@@ -396,8 +405,10 @@
"header_list": "Seznam záhlaví",
"invalid_name": "Uveďte prosím název žádosti",
"method": "Metoda",
"moved": "Request moved",
"name": "Vyžádejte si jméno",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Odpovědní orgán",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Záhlaví",
@@ -434,6 +446,7 @@
"status": "Postavení",
"time": "Čas",
"title": "Odezva",
"video": "Video",
"waiting_for_connection": "čekání na připojení",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Tělo",
"collections": "Sbírky",
"documentation": "Dokumentace",
"environments": "Environments",
"headers": "Záhlaví",
"history": "Dějiny",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Ukončete tým",
"exit_disabled": "Pouze vlastník nemůže opustit tým",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Formát e -mailu je neplatný",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
@@ -675,6 +690,7 @@
"member_removed": "Uživatel odstraněn",
"member_role_updated": "Role uživatelů aktualizovány",
"members": "Členové",
"more_members": "+{count} more",
"name_length_insufficient": "Název týmu by měl mít alespoň 6 znaků",
"name_updated": "Team name updated",
"new": "Nový tým",
@@ -682,10 +698,13 @@
"new_name": "Můj nový tým",
"no_access": "K těmto kolekcím nemáte přístup k úpravám",
"no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "Oprávnění",
"same_target_destination": "Same target and destination",
"saved": "Tým uložen",
"select_a_team": "Select a team",
"title": "Týmy",
@@ -713,5 +732,11 @@
"message": "Zpráva",
"protocols": "Protokoly",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Redigere",
"filter": "Filter",
"go_back": "Gå tilbage",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Etiket",
"learn_more": "Lær mere",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Samlingen er oprettet",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Rediger samling",
"invalid_name": "Angiv et gyldigt navn til samlingen",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Mine samlinger",
"name": "Min nye samling",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Ny kollektion",
"order_changed": "Collection Order Updated",
"renamed": "Samling omdøbt",
"request_in_use": "Request in use",
"save_as": "Gem som",
@@ -142,6 +147,7 @@
"remove_team": "Er du sikker på, at du vil slette dette hold?",
"remove_telemetry": "Er du sikker på, at du vil fravælge telemetri?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Er du sikker på, at du vil synkronisere dette arbejdsområde?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Zen -tilstand"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Samlinger",
"confirm": "Bekræfte",
"edit_request": "Rediger anmodning",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Kopier link",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Varighed",
"enter_curl": "Indtast cURL",
"generate_code": "Generer kode",
@@ -396,8 +405,10 @@
"header_list": "Overskriftsliste",
"invalid_name": "Angiv et navn på anmodningen",
"method": "Metode",
"moved": "Request moved",
"name": "Anmodningsnavn",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Svarorgan",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Overskrifter",
@@ -434,6 +446,7 @@
"status": "Status",
"time": "Tid",
"title": "Respons",
"video": "Video",
"waiting_for_connection": "venter på forbindelse",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Legeme",
"collections": "Samlinger",
"documentation": "Dokumentation",
"environments": "Environments",
"headers": "Overskrifter",
"history": "Historie",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Afslut Team",
"exit_disabled": "Kun ejeren kan ikke forlade teamet",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "E -mailformatet er ugyldigt",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
@@ -675,6 +690,7 @@
"member_removed": "Bruger fjernet",
"member_role_updated": "Brugerroller opdateret",
"members": "Medlemmer",
"more_members": "+{count} more",
"name_length_insufficient": "Holdnavnet bør mindst være 6 tegn langt",
"name_updated": "Team name updated",
"new": "Nyt team",
@@ -682,10 +698,13 @@
"new_name": "Mit nye hold",
"no_access": "Du har ikke redigeringsadgang til disse samlinger",
"no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "Tilladelser",
"same_target_destination": "Same target and destination",
"saved": "Hold reddet",
"select_a_team": "Select a team",
"title": "Hold",
@@ -713,5 +732,11 @@
"message": "Besked",
"protocols": "Protokoller",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Bearbeiten",
"filter": "Filter",
"go_back": "Zurück",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Etikett",
"learn_more": "Mehr erfahren",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Sammlung erstellt",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Sammlung bearbeiten",
"invalid_name": "Bitte gib einen gültigen Namen für die Sammlung an",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Meine Sammlungen",
"name": "Meine neue Sammlung",
"name_length_insufficient": "Sammlungsname soll mindestens 3 Zeichen lang sein",
"new": "Neue Sammlung",
"order_changed": "Collection Order Updated",
"renamed": "Sammlung umbenannt",
"request_in_use": "Anfrage wird ausgeführt",
"save_as": "Speichern als",
@@ -142,6 +147,7 @@
"remove_team": "Möchtest Du dieses Team wirklich löschen?",
"remove_telemetry": "Möchtest Du die Telemetrie wirklich deaktivieren?",
"request_change": "Möchtest Du diese Anfrage verwerfen? Ungespeicherte Änderungen gehen verloren.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Möchtest Du diesen Arbeitsbereich wirklich synchronisieren?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Zen-Modus"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Sammlungen",
"confirm": "Aktion bestätigen",
"edit_request": "Anfrage bearbeiten",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Link kopieren",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Dauer",
"enter_curl": "cURL eingeben",
"generate_code": "Code generieren",
@@ -396,8 +405,10 @@
"header_list": "Header-Liste",
"invalid_name": "Bitte gib einen Namen für die Anfrage an",
"method": "Methode",
"moved": "Request moved",
"name": "Anfragename",
"new": "Neue Anfrage",
"order_changed": "Request Order Updated",
"override": "Überschreiben",
"override_help": "Setze <kbd>Content-Type</kbd> in Headers",
"overriden": "Überschrieben",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Antworttext",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Header",
@@ -434,6 +446,7 @@
"status": "Status",
"time": "Zeit",
"title": "Antwort",
"video": "Video",
"waiting_for_connection": "auf Verbindung warten",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Anfragekörper",
"collections": "Sammlungen",
"documentation": "Dokumentation",
"environments": "Environments",
"headers": "Header",
"history": "Verlauf",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "E-Mail-Adresse stimmt nicht mit Deinen Kontodaten überein, bitte kontaktiere den Teameigentümer.",
"exit": "Team verlassen",
"exit_disabled": "Eigentümer können das Team nicht verlassen",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "E-Mail-Format ist ungültig",
"invalid_id": "Ungültige Team-ID, bitte kontaktiere den Teameigentümer.",
"invalid_invite_link": "Ungültiger Einladungslink.",
@@ -675,6 +690,7 @@
"member_removed": "Benutzer entfernt",
"member_role_updated": "Benutzerrollen aktualisiert",
"members": "Mitglieder",
"more_members": "+{count} more",
"name_length_insufficient": "Der Teamname sollte mindestens 6 Zeichen lang sein",
"name_updated": "Teamname aktualisiert",
"new": "Neues Team",
@@ -682,10 +698,13 @@
"new_name": "Mein neues Team",
"no_access": "Du hast keinen Bearbeitungszugriff auf diese Sammlungen",
"no_invite_found": "Einladung nicht gefunden, bitte kontaktiere den Teameigentümer.",
"no_request_found": "Request not found.",
"not_found": "Team wurde nicht gefunde, bitte kontaktiere den Teameigentümer.",
"not_valid_viewer": "Du hast nicht die richtige Berechtigung als Gast, bitte kontaktiere den Teameigentümer.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Wartende Einladungen",
"permissions": "Berechtigungen",
"same_target_destination": "Same target and destination",
"saved": "Team gespeichert",
"select_a_team": "Team auswählen",
"title": "Team",
@@ -713,5 +732,11 @@
"message": "Nachricht",
"protocols": "Protokolle",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Επεξεργασία",
"filter": "Filter",
"go_back": "Πήγαινε πίσω",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Επιγραφή",
"learn_more": "Μάθε περισσότερα",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Η συλλογή δημιουργήθηκε",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Επεξεργασία Συλλογής",
"invalid_name": "Καταχωρίστε ένα έγκυρο όνομα για τη συλλογή",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Οι Συλλογές μου",
"name": "Η νέα μου συλλογή",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Νέα συλλογή",
"order_changed": "Collection Order Updated",
"renamed": "Η συλλογή μετονομάστηκε",
"request_in_use": "Request in use",
"save_as": "Αποθήκευση ως",
@@ -142,6 +147,7 @@
"remove_team": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την ομάδα;",
"remove_telemetry": "Είστε βέβαιοι ότι θέλετε να εξαιρεθείτε από την τηλεμετρία;",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Είστε βέβαιοι ότι θέλετε να συγχρονίσετε αυτόν τον χώρο εργασίας;"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Λειτουργία Zen"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Συλλογές",
"confirm": "Επιβεβαιώνω",
"edit_request": "Αίτημα Επεξεργασίας",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Αντιγραφή συνδέσμου",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Διάρκεια",
"enter_curl": "Εισαγάγετε cURL",
"generate_code": "Δημιουργία κώδικα",
@@ -396,8 +405,10 @@
"header_list": "Λίστα κεφαλίδων",
"invalid_name": "Καταχωρίστε ένα όνομα για το αίτημα",
"method": "Μέθοδος",
"moved": "Request moved",
"name": "Αίτημα ονόματος",
"new": "Νέο Αίτημα",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "Προβολή των links μου"
},
"response": {
"audio": "Audio",
"body": "Σώμα απόκρισης",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Κεφαλίδες",
@@ -434,6 +446,7 @@
"status": "Κατάσταση",
"time": "χρόνος",
"title": "Απάντηση",
"video": "Video",
"waiting_for_connection": "περιμένοντας τη σύνδεση",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Σώμα",
"collections": "Συλλογές",
"documentation": "Τεκμηρίωση",
"environments": "Environments",
"headers": "Κεφαλίδες",
"history": "Ιστορία",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Το Email δεν ταιριάζει με τις λεπτομέριες του προφιλ σας. Επικοινωνήστε με τον Ιδιοκτήτη της Ομάδας.",
"exit": "Έξοδος από την ομάδα",
"exit_disabled": "Μόνο ο ιδιοκτήτης δεν μπορεί να αποχωρήσει από την ομάδα",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Η μορφή ηλεκτρονικού ταχυδρομείου δεν είναι έγκυρη",
"invalid_id": "Μή εγκυρο αναγνωριστικό ομάδας. Επικοινωνήστε με τον Ιδιοκτήτη της Ομάδας.",
"invalid_invite_link": "Μη έγκυρος σύνδεσμος πρόσκλησης",
@@ -675,6 +690,7 @@
"member_removed": "Ο χρήστης καταργήθηκε",
"member_role_updated": "Οι ρόλοι των χρηστών ενημερώθηκαν",
"members": "Μέλη",
"more_members": "+{count} more",
"name_length_insufficient": "Το όνομα της ομάδας πρέπει να έχει τουλάχιστον 6 χαρακτήρες",
"name_updated": "Το όνομα ομάδας ανανεώθηκε",
"new": "Νέα Ομάδα",
@@ -682,10 +698,13 @@
"new_name": "Η νέα μου ομάδα",
"no_access": "Δεν έχετε πρόσβαση επεξεργασίας σε αυτές τις συλλογές",
"no_invite_found": "Δέν βρέθηκε πρόσκληση. Επικοινωνήστε με τον Ιδιοκτήτη της Ομάδας.",
"no_request_found": "Request not found.",
"not_found": "Η ομάδα δεν βρέθηκε. Επικοινωνήστε με τον Ιδιοκτήτη της Ομάδας.",
"not_valid_viewer": "Δεν είστε έγκυρος viewer. Επικοινωνήστε με τον Ιδιοκτήτη της Ομάδας.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Εκκρεμης προσκλήσεις",
"permissions": "Άδειες",
"same_target_destination": "Same target and destination",
"saved": "Η ομάδα σώθηκε",
"select_a_team": "Επιλογή ομάδας",
"title": "Της ομάδας",
@@ -713,5 +732,11 @@
"message": "Μήνυμα",
"protocols": "Πρωτόκολλα",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -4,6 +4,7 @@
"cancel": "Cancel",
"choose_file": "Choose a file",
"clear": "Clear",
"clear_history": "Clear All History",
"clear_all": "Clear all",
"close": "Close",
"connect": "Connect",
@@ -30,6 +31,7 @@
"open_workspace": "Open workspace",
"paste": "Paste",
"prettify": "Prettify",
"rename": "Rename",
"remove": "Remove",
"restore": "Restore",
"save": "Save",
@@ -67,6 +69,8 @@
"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",
"social_links": "Social links",
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
"join_discord_community": "Join our Discord community",
"keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch",
@@ -131,6 +135,7 @@
"renamed": "Collection renamed",
"request_in_use": "Request in use",
"save_as": "Save as",
"save_to_collection": "Save to Collection",
"select": "Select a Collection",
"select_location": "Select location",
"select_team": "Select a team",
@@ -138,7 +143,6 @@
},
"confirm": {
"exit_team": "Are you sure you want to leave this team?",
"save_unsaved_tab": "Do you want to save changes made in this tab ?",
"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?",
@@ -148,8 +152,16 @@
"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.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"close_unsaved_tab": "Are you sure you want to close this tab?",
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
},
"context_menu": {
"set_environment_variable": "Set as variable",
"add_parameters": "Add to parameters",
"open_request_in_new_tab": "Open request in new tab"
},
"count": {
"header": "Header {count}",
"message": "Message {count}",
@@ -192,17 +204,31 @@
"create_new": "Create new environment",
"created": "Environment created",
"deleted": "Environment deletion",
"duplicated": "Environment duplicated",
"edit": "Edit Environment",
"global": "Global",
"empty_variables": "No variables",
"global_variables": "Global variables",
"invalid_name": "Please provide a name for the environment",
"list": "Environment variables",
"my_environments": "My Environments",
"name": "Name",
"nested_overflow": "nested environment variables are limited to 10 levels",
"new": "New Environment",
"no_active_environment": "No active environment",
"no_environment": "No environment",
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
"quick_peek": "Environment Quick Peek",
"replace_with_variable": "Replace with variable",
"scope": "Scope",
"select": "Select environment",
"set": "Set environment",
"set_as_environment": "Set as environment",
"team_environments": "Team Environments",
"title": "Environments",
"updated": "Environment updated",
"value": "Value",
"variable": "Variable",
"variable_list": "Variable List"
},
"error": {
@@ -226,6 +252,7 @@
"no_duration": "No duration",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"proxy_error": "Proxy error",
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script"
@@ -253,6 +280,10 @@
"graphql": {
"mutations": "Mutations",
"schema": "Schema",
"switch_connection": "Switch connection",
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
"subscriptions": "Subscriptions"
},
"group": {
@@ -282,6 +313,30 @@
"preview": "Hide Preview",
"sidebar": "Collapse sidebar"
},
"inspections": {
"title": "Inspector",
"description": "Inspect possible errors",
"environment": {
"add_environment": "Add to Environment",
"not_found": "Environment variable “{environment}” not found."
},
"header": {
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
},
"response": {
"401_error": "Please check your authentication credentials.",
"404_error": "Please check your request URL and method type.",
"network_error": "Please check your network connection.",
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
"default_error": "Please check your request."
},
"url": {
"extension_not_installed": "Extension not installed.",
"extention_not_enabled": "Extension not enabled.",
"extention_enable_action": "Enable Browser Extension",
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list."
}
},
"import": {
"collections": "Import collections",
"curl": "Import cURL",
@@ -316,9 +371,9 @@
"zen_mode": "Zen mode"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Collections",
"confirm": "Confirm",
"close_unsaved_tab": "Close Unsaved Tab ?",
"edit_request": "Edit Request",
"import_export": "Import / Export"
},
@@ -418,8 +473,10 @@
"payload": "Payload",
"query": "Query",
"raw_body": "Raw Request Body",
"rename": "Rename Request",
"renamed": "Request renamed",
"run": "Run",
"stop": "Stop",
"save": "Save",
"save_as": "Save as",
"saved": "Request saved",
@@ -432,6 +489,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Response Body",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Headers",
@@ -445,6 +503,7 @@
"status": "Status",
"time": "Time",
"title": "Response",
"video": "Video",
"waiting_for_connection": "waiting for connection",
"xml": "XML"
},
@@ -457,9 +516,9 @@
"account_name_description": "This is your display name.",
"background": "Background",
"black_mode": "Black",
"dark_mode": "Dark",
"change_font_size": "Change font size",
"choose_language": "Choose language",
"dark_mode": "Dark",
"delete_account": "Delete account",
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
"expand_navigation": "Expand navigation",
@@ -523,6 +582,10 @@
"show_all": "Keyboard shortcuts",
"title": "General"
},
"others": {
"title": "Others",
"prettify": "Prettify Editor's Content"
},
"miscellaneous": {
"invite": "Invite people to Hoppscotch",
"title": "Miscellaneous"
@@ -543,6 +606,9 @@
"delete_method": "Select DELETE method",
"get_method": "Select GET method",
"head_method": "Select HEAD method",
"rename": "Rename Request",
"import_curl": "Import cURL",
"show_code": "Generate code snippet",
"method": "Method",
"next_method": "Select Next method",
"post_method": "Select POST method",
@@ -551,6 +617,7 @@
"reset_request": "Reset Request",
"save_to_collections": "Save to Collections",
"send_request": "Send Request",
"save_request": "Save Request",
"title": "Request"
},
"response": {
@@ -559,10 +626,10 @@
"title": "Response"
},
"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",
"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"
}
},
@@ -580,6 +647,90 @@
"log": "Log",
"url": "URL"
},
"spotlight": {
"general": {
"help_menu": "Help and support",
"chat": "Chat with support",
"open_docs": "Read Documentation",
"open_keybindings": "Keyboard shortcuts",
"open_github": "Open GitHub repository",
"social": "Social",
"title": "General"
},
"miscellaneous": {
"invite": "Invite your friends to Hoppscotch",
"title": "Miscellaneous"
},
"request": {
"switch_to": "Switch to",
"select_method": "Select method",
"save_as_new": "Save as new request",
"tab_parameters": "Parameters tab",
"tab_body": "Body tab",
"tab_headers": "Headers tab",
"tab_authorization": "Authorization tab",
"tab_pre_request_script": "Pre-request script tab",
"tab_tests": "Tests tab",
"tab_query": "Query tab",
"tab_variables": "Variables tab"
},
"graphql": {
"connect": "Connect to server",
"disconnect": "Disconnect from server"
},
"response": {
"copy": "Copy response",
"download": "Download response as file",
"title": "Response"
},
"environments": {
"new": "Create new environment",
"new_variable": "Create a new environment variable",
"edit": "Edit current environment",
"delete": "Delete current environment",
"duplicate": "Duplicate current environment",
"edit_global": "Edit global environment",
"duplicate_global": "Duplicate global environment",
"title": "Environments"
},
"workspace": {
"new": "Create new team",
"edit": "Edit current team",
"delete": "Delete current team",
"invite": "Invite people to team",
"switch_to_personal": "Switch to your personal workspace",
"title": "Teams"
},
"tab": {
"duplicate": "Duplicate current tab",
"close_current": "Close current tab",
"close_others": "Close all other tabs",
"new_tab": "Open a new tab",
"title": "Tabs"
},
"section": {
"user": "User",
"theme": "Theme",
"interface": "Interface",
"interceptor": "Interceptor"
},
"change_language": "Change Language",
"settings": {
"theme": {
"black": "Black",
"dark": "Dark",
"light": "Light",
"system": "System preference"
},
"font": {
"size_sm": "Small",
"size_md": "Medium",
"size_lg": "Large"
},
"change_interceptor": "Change Interceptor",
"change_language": "Change Language"
}
},
"sse": {
"event_type": "Event type",
"log": "Log",
@@ -637,8 +788,11 @@
"tab": {
"authorization": "Authorization",
"body": "Body",
"close": "Close Tab",
"close_others": "Close other Tabs",
"collections": "Collections",
"documentation": "Documentation",
"duplicate": "Duplicate Tab",
"environments": "Environments",
"headers": "Headers",
"history": "History",
@@ -664,9 +818,9 @@
"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_coll_id": "Invalid collection ID",
"invalid_email_format": "Email format is invalid",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_coll_id": "Invalid collection ID",
"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",

View File

@@ -1,43 +1,44 @@
{
"action": {
"autoscroll": "Autoscroll",
"autoscroll": "Desplazamiento automático",
"cancel": "Cancelar",
"choose_file": "Seleccionar archivo",
"clear": "Limpiar",
"clear_all": "Limpiar todo",
"close": "Cerrar",
"connect": "Conectar",
"connecting": "Connecting",
"connecting": "Conectando",
"copy": "Copiar",
"delete": "Borrar",
"disconnect": "Desconectar",
"dismiss": "Descartar",
"dont_save": "Don't save",
"dont_save": "No guardar",
"download_file": "Descargar archivo",
"drag_to_reorder": "Arrastrar para reordenar",
"duplicate": "Duplicar",
"edit": "Editar",
"filter": "Filter",
"filter": "Filtrar",
"go_back": "Volver",
"group_by": "Group by",
"go_forward": "Adelante",
"group_by": "Agrupar por",
"label": "Etiqueta",
"learn_more": "Aprender más",
"less": "Menos",
"more": "Más",
"new": "Nuevo",
"no": "No",
"open_workspace": "Open workspace",
"open_workspace": "Abrir espacio de trabajo",
"paste": "Pegar",
"prettify": "Embellecer",
"remove": "Eliminar",
"restore": "Restaurar",
"save": "Guardar",
"scroll_to_bottom": "Scroll to bottom",
"scroll_to_top": "Scroll to top",
"scroll_to_bottom": "Desplazar hacia abajo",
"scroll_to_top": "Desplazar hacia arriba",
"search": "Buscar",
"send": "Enviar",
"start": "Comenzar",
"starting": "Starting",
"starting": "Iniciando",
"stop": "Detener",
"to_close": "para cerrar",
"to_navigate": "para navegar",
@@ -55,16 +56,16 @@
"chat_with_us": "Habla con nosotros",
"contact_us": "Contáctanos",
"copy": "Copiar",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
"copy_user_id": "Copiar token de autenticación de usuario",
"developer_option": "Opciones para desarrolladores",
"developer_option_description": "Herramientas para desarrolladores que ayudan en el desarrollo y mantenimiento de Hoppscotch.",
"discord": "Discord",
"documentation": "Documentación",
"github": "GitHub",
"help": "Ayuda y comentarios",
"home": "Inicio",
"invite": "Invitar",
"invite_description": "En Hoppscotch, diseñamos una interfaz simple e intuitiva para crear y administrar sus APIs. Hoppscotch es una herramienta que le ayuda a crear, probar, documentar y compartir sus APIs.",
"invite_description": "En Hoppscotch, diseñamos una interfaz simple e intuitiva para crear y administrar tus APIs. Hoppscotch es una herramienta que le ayuda a crear, probar, documentar y compartir tus APIs.",
"invite_your_friends": "Invita a tus amigos",
"join_discord_community": "Únete a nuestra comunidad Discord",
"keyboard_shortcuts": "Atajos de teclado",
@@ -78,7 +79,7 @@
"shortcuts": "Atajos",
"spotlight": "Destacar",
"status": "Estado",
"status_description": "Check the status of the website",
"status_description": "Comprobar el estado del sitio web",
"terms_and_privacy": "Términos y privacidad",
"twitter": "Twitter",
"type_a_command_search": "Escribe un comando o buscar algo…",
@@ -117,14 +118,18 @@
},
"collection": {
"created": "Colección creada",
"different_parent": "No se puede reordenar la colección con un padre diferente",
"edit": "Editar colección",
"invalid_name": "Proporciona un nombre válido para la colección.",
"invalid_root_move": "La colección ya está en la raíz",
"moved": "Movido con éxito",
"my_collections": "Mis colecciones",
"name": "Mi nueva colección",
"name_length_insufficient": "El nombre de la colección debe tener al menos 3 caracteres",
"new": "Nueva colección",
"order_changed": "Orden de colección actualizada",
"renamed": "Colección renombrada",
"request_in_use": "Petición en uso",
"request_in_use": "Solicitud en uso",
"save_as": "Guardar como",
"select": "Seleccionar colección",
"select_location": "Seleccionar ubicación",
@@ -133,16 +138,17 @@
},
"confirm": {
"exit_team": "¿Estás seguro de que quieres dejar este equipo?",
"logout": "¿Está seguro de que desea cerrar la sesión?",
"remove_collection": "¿Está seguro de que desea eliminar esta colección de forma permanente?",
"remove_environment": "¿Está seguro de que desea eliminar este entorno de forma permanente?",
"remove_folder": "¿Está seguro de que desea eliminar esta carpeta de forma permanente?",
"remove_history": "¿Está seguro de que desea eliminar todo el historial de forma permanente?",
"remove_request": "¿Está seguro de que desea eliminar esta petición de forma permanente?",
"remove_team": "¿Está seguro de que desea eliminar este equipo?",
"remove_telemetry": "¿Está seguro de que desea darse de baja de la telemetría?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"sync": "¿Está seguro de que desea sincronizar este espacio de trabajo?"
"logout": "¿Estás seguro de que deseas cerrar la sesión?",
"remove_collection": "¿Estás seguro de que deseas eliminar esta colección de forma permanente?",
"remove_environment": "¿Estás seguro de que deseas eliminar este entorno de forma permanente?",
"remove_folder": "¿Estás seguro de que deseas eliminar esta carpeta de forma permanente?",
"remove_history": "¿Estás seguro de que deseas eliminar todo el historial de forma permanente?",
"remove_request": "¿Estás seguro de que deseas eliminar esta solicitud de forma permanente?",
"remove_team": "¿Estás seguro de que deseas eliminar este equipo?",
"remove_telemetry": "¿Estás seguro de que deseas darse de baja de la telemetría?",
"request_change": "¿Estás seguro de que deseas descartar la solicitud actual, los cambios no guardados se perderán.",
"save_unsaved_tab": "¿Deseas guardar los cambios realizados en esta pestaña?",
"sync": "¿Estás seguro de que deseas sincronizar este espacio de trabajo?"
},
"count": {
"header": "Encabezado {count}",
@@ -157,28 +163,28 @@
"generate_message": "Importar cualquier colección de Hoppscotch para generar documentación de la API sobre la marcha."
},
"empty": {
"authorization": "Esta petición no utiliza ninguna autorización.",
"body": "Esta petición no tiene cuerpo",
"authorization": "Esta solicitud no utiliza ninguna autorización.",
"body": "Esta solicitud no tiene cuerpo",
"collection": "La colección está vacía",
"collections": "Las colecciones están vacías",
"documentation": "Conectarse a un punto final de GraphQL para ver la documentación",
"endpoint": "El punto final no puede estar vacío",
"environments": "Los entornos están vacíos",
"folder": "La carpeta está vacía",
"headers": "Esta petición no tiene encabezados",
"headers": "Esta solicitud no tiene encabezados",
"history": "El historial está vacío",
"invites": "La lista de invitados está vacía",
"members": "El equipo está vacío",
"parameters": "Esta petición no tiene ningún parámetro",
"parameters": "Esta solicitud no tiene ningún parámetro",
"pending_invites": "No hay invitaciones pendientes para este equipo",
"profile": "Iniciar sesión para ver tu perfil",
"protocols": "Los protocolos están vacíos",
"schema": "Conectarse a un punto final de GraphQL",
"shortcodes": "Los shortcodes están vacíos",
"shortcodes": "Aún no se han creado Shortcodes",
"subscription": "Subscriptions are empty",
"team_name": "Nombre del equipo vacío",
"teams": "Los equipos están vacíos",
"tests": "No hay pruebas para esta petición"
"tests": "No hay pruebas para esta solicitud"
},
"environment": {
"add_to_global": "Añadir a Global",
@@ -188,38 +194,38 @@
"deleted": "Eliminar el entorno",
"edit": "Editar entorno",
"invalid_name": "Proporciona un nombre válido para el entorno.",
"my_environments": "My Environments",
"my_environments": "Mis entornos",
"nested_overflow": "las variables de entorno anidadas están limitadas a 10 niveles",
"new": "Nuevo entorno",
"no_environment": "Sin entorno",
"no_environment_description": "No se ha seleccionado ningún entorno. Elije qué hacer con las siguientes variables.",
"select": "Seleccionar entorno",
"team_environments": "Team Environments",
"team_environments": "Entornos de trabajo en equipo",
"title": "Entornos",
"updated": "Actualización del entorno",
"updated": "Entorno actualizado",
"variable_list": "Lista de variables"
},
"error": {
"browser_support_sse": "Este navegador no parece ser compatible con los eventos enviados por el servidor.",
"check_console_details": "Consulta el registro de la consola para obtener más detalles.",
"curl_invalid_format": "cURL no está formateado correctamente",
"danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these teams:",
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
"empty_req_name": "Nombre de petición vacío",
"danger_zone": "Zona de peligro",
"delete_account": "Tu cuenta es actualmente propietaria en estos equipos:",
"delete_account_description": "Para poder eliminar tu cuenta, debes darte de baja, transferir la propiedad o eliminar estos equipos.",
"empty_req_name": "Nombre de solicitud vacío",
"f12_details": "(F12 para más detalles)",
"gql_prettify_invalid_query": "No se puede aplicar embellecedor a una consulta no válida, resuelve los errores de sintaxis de la consulta y vuelve a intentarlo",
"incomplete_config_urls": "URLs de configuración incompletas",
"incorrect_email": "Correo electrónico incorrecto",
"invalid_link": "Enlace no válido",
"invalid_link_description": "El enlace que has pulsado no es válido o ha caducado.",
"json_parsing_failed": "Invalid JSON",
"json_parsing_failed": "JSON no válido",
"json_prettify_invalid_body": "No se puede aplicar embellecedor a un cuerpo inválido, resuelve errores de sintaxis json y vuelve a intentarlo",
"network_error": "Parece que hay un error de red. Por favor, inténtalo de nuevo.",
"network_fail": "No se pudo enviar la petición",
"network_fail": "No se pudo enviar la solicitud",
"no_duration": "Sin duración",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"no_results_found": "No se han encontrado coincidencias",
"page_not_found": "No se ha podido encontrar esta página",
"script_fail": "No se pudo ejecutar el script de solicitud previa",
"something_went_wrong": "Algo salió mal",
"test_script_fail": "No se ha podido ejecutar la secuencia de comandos posterior a la solicitud"
@@ -250,7 +256,7 @@
"subscriptions": "Suscripciones"
},
"group": {
"time": "Time",
"time": "Tiempo",
"url": "URL"
},
"header": {
@@ -259,19 +265,19 @@
"save_workspace": "Guardar mi espacio de trabajo"
},
"helpers": {
"authorization": "El encabezado de autorización se generará automáticamente cuando se envía la petición.",
"authorization": "El encabezado de autorización se generará automáticamente cuando se envía la solicitud.",
"generate_documentation_first": "Generar la documentación primero",
"network_fail": "No se puede acceder a la API. Comprueba tu conexión de red y vuelve a intentarlo.",
"offline": "Parece estar desconectado. Es posible que los datos de este espacio de trabajo no estén actualizados.",
"offline_short": "Pareces estar desconectado.",
"post_request_tests": "Los scripts de prueba están escritos en JavaScript y se ejecutan después de recibir la respuesta.",
"pre_request_script": "Los scripts previos a la petición están escritos en JavaScript y se ejecutan antes de que se envíe la petición.",
"pre_request_script": "Los scripts previos a la solicitud están escritos en JavaScript y se ejecutan antes de que se envíe la solicitud.",
"script_fail": "Parece que hay un problema técnico en el script de solicitud previa. Comprueba el error a continuación y corrige el script en consecuencia.",
"test_script_fail": "Parece que hay un error con el script de prueba. Por favor, corrige los errores y ejecute las pruebas de nuevo",
"tests": "Escribir un script de prueba para automatizar la depuración."
},
"hide": {
"collection": "Collapse Collection Panel",
"collection": "Colapsar el panel de colecciones",
"more": "Ocultar más",
"preview": "Ocultar vista previa",
"sidebar": "Ocultar barra lateral"
@@ -302,39 +308,40 @@
"title": "Importar"
},
"layout": {
"collapse_collection": "Collapse or Expand Collections",
"collapse_sidebar": "Collapse or Expand the sidebar",
"collapse_collection": "Contraer o expandir colecciones",
"collapse_sidebar": "Contraer o expandir la barra lateral",
"column": "Disposición vertical",
"name": "Layout",
"name": "Diseño",
"row": "Disposición horizontal",
"zen_mode": "Modo zen"
},
"modal": {
"close_unsaved_tab": "Tienes cambios sin guardar",
"collections": "Colecciones",
"confirm": "Confirmar",
"edit_request": "Editar petición",
"edit_request": "Editar solicitud",
"import_export": "Importación y exportación"
},
"mqtt": {
"already_subscribed": "You are already subscribed to this topic.",
"clean_session": "Clean Session",
"clear_input": "Clear input",
"clear_input_on_send": "Clear input on send",
"client_id": "Client ID",
"color": "Pick a color",
"already_subscribed": "Ya estás suscrito a este tema.",
"clean_session": "Borrar sesión",
"clear_input": "Borrar entrada",
"clear_input_on_send": "Borrar entrada al enviar",
"client_id": "Identificación del cliente",
"color": "Elige un color",
"communication": "Comunicación",
"connection_config": "Connection Config",
"connection_not_authorized": "This MQTT connection does not use any authentication.",
"invalid_topic": "Please provide a topic for the subscription",
"keep_alive": "Keep Alive",
"connection_config": "Configuración de conexión",
"connection_not_authorized": "Esta conexión MQTT no utiliza ninguna autenticación.",
"invalid_topic": "Indica un tema para la suscripción",
"keep_alive": "Mantenerse vivo",
"log": "Registro",
"lw_message": "Last-Will Message",
"lw_qos": "Last-Will QoS",
"lw_retain": "Last-Will Retain",
"lw_topic": "Last-Will Topic",
"lw_message": "Mensaje de última voluntad",
"lw_qos": "QoS de última voluntad",
"lw_retain": "Última voluntad",
"lw_topic": "Tema de última voluntad",
"message": "Mensaje",
"new": "New Subscription",
"not_connected": "Please start a MQTT connection first.",
"new": "Nueva suscripción",
"not_connected": "Por favor, inicia primero una conexión MQTT.",
"publish": "Publicar",
"qos": "QoS",
"ssl": "SSL",
@@ -346,7 +353,7 @@
"url": "URL"
},
"navigation": {
"doc": "Docs",
"doc": "Documentación",
"graphql": "GraphQL",
"profile": "Perfil",
"realtime": "Tiempo real",
@@ -356,7 +363,7 @@
"preRequest": {
"javascript_code": "Código JavaScript",
"learn": "Leer documentación",
"script": "Script previo a la petición",
"script": "Script previo a la solicitud",
"snippets": "Fragmentos"
},
"profile": {
@@ -378,51 +385,56 @@
"star": "Eliminar estrella"
},
"request": {
"added": "Petición agregada",
"added": "Solicitud agregada",
"authorization": "Autorización",
"body": "Cuerpo de la petición",
"body": "Cuerpo de la solicitud",
"choose_language": "Seleccionar lenguaje",
"content_type": "Tipo de contenido",
"content_type_titles": {
"others": "Others",
"structured": "Structured",
"text": "Text"
"others": "Otros",
"structured": "Estructurado",
"text": "Texto"
},
"copy_link": "Copiar enlace",
"different_collection": "No se pueden reordenar solicitudes de diferentes colecciones",
"duplicated": "Solicitud duplicada",
"duration": "Duración",
"enter_curl": "Ingrese cURL",
"generate_code": "Generar código",
"generated_code": "Código generado",
"header_list": "Lista de encabezados",
"invalid_name": "Proporciona un nombre para la petición.",
"invalid_name": "Proporciona un nombre para la solicitud.",
"method": "Método",
"name": "Nombre de petición",
"new": "New Request",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
"moved": "Request moved",
"name": "Nombre de solicitud",
"new": "Nueva solicitud",
"order_changed": "Orden de solicitudes actualizadas",
"override": "Anular",
"override_help": "Establecer <kbd>Content-Type</kbd> en las cabeceras",
"overriden": "Anulado",
"parameter_list": "Parámetros de consulta",
"parameters": "Parámetros",
"path": "Ruta",
"payload": "Carga útil",
"query": "Consulta",
"raw_body": "Cuerpo de petición sin procesar",
"renamed": "Petición renombrada",
"raw_body": "Cuerpo de solicitud sin procesar",
"renamed": "Solicitud renombrada",
"run": "Ejecutar",
"save": "Guardar",
"save_as": "Guardar como",
"saved": "Petición guardada",
"saved": "Solicitud guardada",
"share": "Compartir",
"share_description": "Share Hoppscotch with your friends",
"title": "Petición",
"type": "Tipo de petición",
"share_description": "Comparte Hoppscotch con tus amigos",
"title": "Solicitud",
"type": "Tipo de solicitud",
"url": "URL",
"variables": "Variables",
"view_my_links": "Ver mis enlaces"
},
"response": {
"audio": "Audio",
"body": "Cuerpo de respuesta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)",
"headers": "Encabezados",
"html": "HTML",
"image": "Imagen",
@@ -434,13 +446,14 @@
"status": "Estado",
"time": "Tiempo",
"title": "Respuesta",
"video": "Video",
"waiting_for_connection": "esperando la conexión",
"xml": "XML"
},
"settings": {
"accent_color": "Color de acentuación",
"account": "Cuenta",
"account_deleted": "Your account has been deleted",
"account_deleted": "Tu cuenta ha sido eliminada",
"account_description": "Personaliza la configuración de tu cuenta.",
"account_email_description": "Tu dirección de correo electrónico principal.",
"account_name_description": "Este es tu nombre para mostrar.",
@@ -449,8 +462,8 @@
"change_font_size": "Cambiar tamaño de fuente",
"choose_language": "Elegir idioma",
"dark_mode": "Oscuro",
"delete_account": "Delete account",
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
"delete_account": "Eliminar cuenta",
"delete_account_description": "Una vez que elimines tu cuenta, todos tus datos se borrarán permanentemente. Esta acción no se puede deshacer.",
"expand_navigation": "Expandir la navegación",
"experiments": "Experimentos",
"experiments_notice": "Esta es una colección de experimentos en los que estamos trabajando que podrían resultar útiles, divertidos, ambos o ninguno. No son definitivos y es posible que no sean estables, por lo que si sucede algo demasiado extraño, no se asuste. Solo apaga la maldita cosa. Fuera de bromas,",
@@ -469,7 +482,7 @@
"light_mode": "Luz",
"official_proxy_hosting": "El proxy oficial está alojado en Hoppscotch.",
"profile": "Perfil",
"profile_description": "Update your profile details",
"profile_description": "Actualiza los datos de tu perfil",
"profile_email": "Correo electrónico",
"profile_name": "Nombre de perfil",
"proxy": "Proxy",
@@ -477,8 +490,8 @@
"proxy_use_toggle": "Utilizar el middleware de proxy para enviar peticiones",
"read_the": "Leer el",
"reset_default": "Restablecer a los predeterminados",
"short_codes": "Short codes",
"short_codes_description": "Short codes which were created by you.",
"short_codes": "Shortcodes",
"short_codes_description": "Shortcodes creados por ti.",
"sidebar_on_left": "Barra lateral a la izquierda",
"sync": "Sincronizar",
"sync_collections": "Colecciones",
@@ -492,15 +505,15 @@
"theme_description": "Personaliza el tema de tu aplicación.",
"use_experimental_url_bar": "Utilizar la barra de URL experimental con resaltado de entorno",
"user": "Usuario",
"verified_email": "Verified email",
"verified_email": "Correo electrónico verificado",
"verify_email": "Verificar correo electrónico"
},
"shortcodes": {
"actions": "Actions",
"created_on": "Created on",
"deleted": "Shortcode deleted",
"method": "Method",
"not_found": "Shortcode not found",
"actions": "Acciones",
"created_on": "Creado el",
"deleted": "Código corto eliminado",
"method": "Método",
"not_found": "Shortcode no encontrado",
"short_code": "Short code",
"url": "URL"
},
@@ -528,7 +541,7 @@
"title": "Navegación"
},
"request": {
"copy_request_link": "Copiar enlace de petición",
"copy_request_link": "Copiar enlace de solicitud",
"delete_method": "Seleccionar método DELETE",
"get_method": "Seleccionar método GET",
"head_method": "Seleccionar método HEAD",
@@ -537,10 +550,10 @@
"post_method": "Seleccionar método POST",
"previous_method": "Seleccionar método anterior",
"put_method": "Seleccionar método PUT",
"reset_request": "Petición de reinicio",
"reset_request": "Solicitud de reinicio",
"save_to_collections": "Guardar en colecciones",
"send_request": "Enviar petición",
"title": "Petición"
"send_request": "Enviar solicitud",
"title": "Solicitud"
},
"response": {
"copy": "Copiar la respuesta al portapapeles",
@@ -582,8 +595,8 @@
"connected_to": "Conectado a {name}",
"connecting_to": "Conectando con {name}...",
"connection_error": "Failed to connect",
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"connection_failed": "Error de conexión",
"connection_lost": "Conexión perdida",
"copied_to_clipboard": "Copiado al portapapeles",
"deleted": "Eliminado",
"deprecated": "OBSOLETO",
@@ -598,18 +611,18 @@
"history_deleted": "Historial eliminado",
"linewrap": "Envolver líneas",
"loading": "Cargando...",
"message_received": "Message: {message} arrived on topic: {topic}",
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
"message_received": "Mensaje: {mensaje} llegó sobre el tema: {topic}",
"mqtt_subscription_failed": "Algo ha ido mal al suscribirse al tema: {topic}",
"none": "Ninguno",
"nothing_found": "Nada encontrado para",
"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": "Esperando para enviar petición"
"published_error": "Algo ha ido mal al publicar el mensaje: {topic} al tema: {message}",
"published_message": "Mensaje publicado: {mensaje} al tema: {topic}",
"reconnection_error": "Fallo en la reconexión",
"subscribed_failed": "Error al suscribirse al tema: {topic}",
"subscribed_success": "Suscrito con éxito al tema: {topic}",
"unsubscribed_failed": "Error al darse de baja del tema: {topic}",
"unsubscribed_success": "Se ha cancelado la suscripción al tema: {topic}",
"waiting_send_request": "Esperando para enviar solicitud"
},
"support": {
"changelog": "Leer más sobre los últimos lanzamientos",
@@ -628,11 +641,12 @@
"body": "Cuerpo",
"collections": "Colecciones",
"documentation": "Documentación",
"environments": "Environments",
"headers": "Encabezados",
"history": "Historial",
"mqtt": "MQTT",
"parameters": "Parámetros",
"pre_request_script": "Script previo a la petición",
"pre_request_script": "Script previo a la solicitud",
"queries": "Consultas",
"query": "Consulta",
"schema": "Esquema",
@@ -652,8 +666,9 @@
"email_do_not_match": "El correo electrónico no coincide con los datos de tu cuenta. Ponte en contacto con el propietario de tu equipo.",
"exit": "Salir del equipo",
"exit_disabled": "Solo el propietario puede salir del equipo",
"invalid_coll_id": "Identificador de colección no válido",
"invalid_email_format": "El formato de correo electrónico no es válido",
"invalid_id": "ID de equipo inválido. Ponte en contacto con el propietario de tu equipo.",
"invalid_id": "Identificador de equipo inválido. Ponte en contacto con el propietario de tu equipo.",
"invalid_invite_link": "Enlace de invitación inválido",
"invalid_invite_link_description": "El enlace que has seguido no es válido. Ponte en contacto con el propietario de tu equipo.",
"invalid_member_permission": "Proporcionar un permiso válido al miembro del equipo",
@@ -670,11 +685,12 @@
"login_to_continue": "Iniciar sesión para continuar",
"login_to_continue_description": "Tienes que estar conectado para unirte a un equipo.",
"logout_and_try_again": "Cerrar la sesión e iniciar sesión con otra cuenta",
"member_has_invite": "Este ID de correo electrónico ya tiene una invitación. Ponte en contacto con el propietario de tu equipo.",
"member_has_invite": "Este Identificador de correo electrónico ya tiene una invitación. Ponte en contacto con el propietario de tu equipo.",
"member_not_found": "Miembro no encontrado. Ponte en contacto con el propietario de tu equipo.",
"member_removed": "Usuario eliminado",
"member_role_updated": "Funciones de usuario actualizadas",
"members": "Miembros",
"more_members": "+{count} more",
"name_length_insufficient": "El nombre del equipo debe tener al menos 6 caracteres",
"name_updated": "Nombre de equipo actualizado",
"new": "Nuevo equipo",
@@ -682,20 +698,23 @@
"new_name": "Mi nuevo equipo",
"no_access": "No tienes acceso de edición a estas colecciones.",
"no_invite_found": "No se ha encontrado la invitación. Ponte en contacto con el propietario de tu equipo.",
"no_request_found": "Solicitud no encontrada.",
"not_found": "Equipo no encontrado. Ponte en contacto con el propietario de tu equipo.",
"not_valid_viewer": "No eres un espectador válido. Ponte en contacto con el propietario de tu equipo.",
"parent_coll_move": "No se puede mover la colección a una colección hija",
"pending_invites": "Invitaciones pendientes",
"permissions": "Permisos",
"same_target_destination": "Same target and destination",
"saved": "Equipo guardado",
"select_a_team": "Seleccionar un equipo",
"title": "Equipos",
"we_sent_invite_link": "¡Hemos enviado un enlace de invitación a todos los invitados!",
"we_sent_invite_link_description": "Pide a todos los invitados que revisen su bandeja de entrada. Haz clic en el enlace para unirse al equipo."
"we_sent_invite_link_description": "Pide a todos los invitados que revisen tu bandeja de entrada. Haz clic en el enlace para unirse al equipo."
},
"team_environment": {
"deleted": "Environment Deleted",
"duplicate": "Environment Duplicated",
"not_found": "Environment not found."
"deleted": "Entorno eliminado",
"duplicate": "Entorno duplicado",
"not_found": "Entorno no encontrado."
},
"test": {
"failed": "prueba fallida",
@@ -713,5 +732,11 @@
"message": "Mensaje",
"protocols": "Protocolos",
"url": "URL"
},
"workspace": {
"change": "Cambiar el espacio de trabajo",
"personal": "Mi espacio de trabajo",
"team": "Espacio de trabajo en equipo",
"title": "Espacios de trabajo"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Muokata",
"filter": "Filter",
"go_back": "Mene takaisin",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Etiketti",
"learn_more": "Lue lisää",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Kokoelma luotu",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Muokkaa kokoelmaa",
"invalid_name": "Anna kokoelmalle kelvollinen nimi",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Omat kokoelmat",
"name": "Uusi kokoelmani",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Uusi kokoelma",
"order_changed": "Collection Order Updated",
"renamed": "Kokoelma nimetty uudelleen",
"request_in_use": "Request in use",
"save_as": "Tallenna nimellä",
@@ -142,6 +147,7 @@
"remove_team": "Haluatko varmasti poistaa tämän ryhmän?",
"remove_telemetry": "Haluatko varmasti poistaa telemetrian käytöstä?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Haluatko varmasti synkronoida tämän työtilan?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Zen -tila"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Kokoelmat",
"confirm": "Vahvistaa",
"edit_request": "Muokkaa pyyntöä",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Kopioi linkki",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Kesto",
"enter_curl": "Kirjoita cURL",
"generate_code": "Luo koodi",
@@ -396,8 +405,10 @@
"header_list": "Otsikkoluettelo",
"invalid_name": "Anna pyynnölle nimi",
"method": "Menetelmä",
"moved": "Request moved",
"name": "Pyynnön nimi",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Vastauselin",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Otsikot",
@@ -434,6 +446,7 @@
"status": "Tila",
"time": "Aika",
"title": "Vastaus",
"video": "Video",
"waiting_for_connection": "yhteyttä odotellessa",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Runko",
"collections": "Kokoelmat",
"documentation": "Dokumentointi",
"environments": "Environments",
"headers": "Otsikot",
"history": "Historia",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Poistu tiimistä",
"exit_disabled": "Vain omistaja ei voi poistua tiimistä",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Sähköpostin muoto on virheellinen",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
@@ -675,6 +690,7 @@
"member_removed": "Käyttäjä poistettu",
"member_role_updated": "Käyttäjäroolit päivitetty",
"members": "Jäsenet",
"more_members": "+{count} more",
"name_length_insufficient": "Joukkueen nimen tulee olla vähintään 6 merkkiä pitkä",
"name_updated": "Team name updated",
"new": "Uusi tiimi",
@@ -682,10 +698,13 @@
"new_name": "Uusi tiimini",
"no_access": "Sinulla ei ole muokkausoikeuksia näihin kokoelmiin",
"no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "Käyttöoikeudet",
"same_target_destination": "Same target and destination",
"saved": "Joukkue tallennettu",
"select_a_team": "Select a team",
"title": "Joukkueet",
@@ -713,5 +732,11 @@
"message": "Viesti",
"protocols": "Pöytäkirjat",
"url": "URL -osoite"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Éditer",
"filter": "Filter",
"go_back": "Retour",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Étiqueter",
"learn_more": "En savoir plus",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Collection créée",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Modifier la collection",
"invalid_name": "Veuillez fournir un nom valide pour la collection",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Mes collections",
"name": "Ma nouvelle collection",
"name_length_insufficient": "Le nom de la collection doit comporter au moins 3 caractères",
"new": "Nouvelle collection",
"order_changed": "Collection Order Updated",
"renamed": "Collection renommée",
"request_in_use": "Demande en cours d'utilisation",
"save_as": "Enregistrer sous",
@@ -142,6 +147,7 @@
"remove_team": "Voulez-vous vraiment supprimer cette équipe ?",
"remove_telemetry": "Êtes-vous sûr de vouloir désactiver la télémétrie ?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Voulez-vous vraiment synchroniser cet espace de travail ?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Mode Zen"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Collections",
"confirm": "Confirmer",
"edit_request": "Modifier la requête",
@@ -389,6 +396,8 @@
"text": "Texte"
},
"copy_link": "Copier le lien",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Durée",
"enter_curl": "Entrer cURL",
"generate_code": "Générer le code",
@@ -396,8 +405,10 @@
"header_list": "Liste des en-têtes",
"invalid_name": "Veuillez fournir un nom pour la requête",
"method": "Méthode",
"moved": "Request moved",
"name": "Nom de la requête",
"new": "Nouvelle requête",
"order_changed": "Request Order Updated",
"override": "Remplacer",
"override_help": "Set <xmp>Content-Type</xmp> in Headers",
"overriden": "Remplacé",
@@ -421,6 +432,7 @@
"view_my_links": "Voir mes liens"
},
"response": {
"audio": "Audio",
"body": "Corps de réponse",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "En-têtes",
@@ -434,6 +446,7 @@
"status": "Statut",
"time": "Temps",
"title": "Réponse",
"video": "Video",
"waiting_for_connection": "En attente de connexion",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Corps",
"collections": "Collections",
"documentation": "Documentation",
"environments": "Environments",
"headers": "En-têtes",
"history": "Histoire",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "L'email ne correspond pas aux détails de votre compte. Contactez le propriétaire de votre équipe.",
"exit": "Quitter l'équipe",
"exit_disabled": "Seul le propriétaire ne peut pas quitter l'équipe",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Le format de l'e-mail n'est pas valide",
"invalid_id": "L'email ne correspond pas aux détails de votre compte. Contactez le propriétaire de votre équipe.",
"invalid_invite_link": "Lien d'invitation invalide",
@@ -675,6 +690,7 @@
"member_removed": "Utilisateur supprimé",
"member_role_updated": "Rôles des utilisateurs mis à jour",
"members": "Membres",
"more_members": "+{count} more",
"name_length_insufficient": "Le nom de l'équipe doit comporter au moins 6 caractères",
"name_updated": "Nom de l'équipe mis à jour",
"new": "Nouvelle équipe",
@@ -682,10 +698,13 @@
"new_name": "Ma nouvelle équipe",
"no_access": "Vous n'avez pas l'accès en modification à ces collections",
"no_invite_found": "Invitation non trouvée. Contactez le propriétaire de votre équipe.",
"no_request_found": "Request not found.",
"not_found": "Équipe non trouvée. Contactez le propriétaire de votre équipe.",
"not_valid_viewer": "Vous n'êtes pas un visionneur valide. Contactez le propriétaire de votre équipe.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Invitations en attente",
"permissions": "Autorisations",
"same_target_destination": "Same target and destination",
"saved": "Équipe enregistrée",
"select_a_team": "Choisir une équipe",
"title": "Équipes",
@@ -713,5 +732,11 @@
"message": "Message",
"protocols": "Protocoles",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "לַעֲרוֹך",
"filter": "Filter",
"go_back": "תחזור",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "תווית",
"learn_more": "למד עוד",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "אוסף נוצר",
"different_parent": "Cannot reorder collection with different parent",
"edit": "ערוך אוסף",
"invalid_name": "אנא ספק שם תקף לאוסף",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "האוספים שלי",
"name": "האוסף החדש שלי",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "קולקציה חדשה",
"order_changed": "Collection Order Updated",
"renamed": "שם האוסף שונה",
"request_in_use": "Request in use",
"save_as": "שמור כ",
@@ -142,6 +147,7 @@
"remove_team": "האם אתה בטוח שברצונך למחוק את הצוות הזה?",
"remove_telemetry": "האם אתה בטוח שברצונך לבטל את הסכמתך לטלמטריה?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "האם אתה בטוח שברצונך לסנכרן את סביבת העבודה הזו?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "מצב זן"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "אוספים",
"confirm": "לְאַשֵׁר",
"edit_request": "ערוך בקשה",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "העתק קישור",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "מֶשֶׁך",
"enter_curl": "הזן cURL",
"generate_code": "צור קוד",
@@ -396,8 +405,10 @@
"header_list": "רשימת כותרות",
"invalid_name": "אנא ספק שם לבקשה",
"method": "שיטה",
"moved": "Request moved",
"name": "שם הבקשה",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "גוף תגובה",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "כותרות",
@@ -434,6 +446,7 @@
"status": "סטָטוּס",
"time": "זְמַן",
"title": "תְגוּבָה",
"video": "Video",
"waiting_for_connection": "מחכה לחיבור",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "גוּף",
"collections": "אוספים",
"documentation": "תיעוד",
"environments": "Environments",
"headers": "כותרות",
"history": "הִיסטוֹרִיָה",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "יציאה מצוות",
"exit_disabled": "רק הבעלים אינו יכול לצאת מהצוות",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "פורמט הדוא\"ל אינו חוקי",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
@@ -675,6 +690,7 @@
"member_removed": "המשתמש הוסר",
"member_role_updated": "תפקידי משתמשים עודכנו",
"members": "חברים",
"more_members": "+{count} more",
"name_length_insufficient": "שם הקבוצה צריך לכלול לפחות 6 תווים",
"name_updated": "Team name updated",
"new": "קבוצה חדשה",
@@ -682,10 +698,13 @@
"new_name": "הצוות החדש שלי",
"no_access": "אין לך גישת עריכה לאוספים אלה",
"no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "הרשאות",
"same_target_destination": "Same target and destination",
"saved": "הקבוצה ניצלה",
"select_a_team": "Select a team",
"title": "צוותים",
@@ -713,5 +732,11 @@
"message": "הוֹדָעָה",
"protocols": "פרוטוקולים",
"url": "כתובת URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "संपादित करें",
"filter": "फ़िल्टर प्रतिक्रिया",
"go_back": "वापस जाओ",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "लेबल",
"learn_more": "और अधिक जानें",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "संग्रह बनाया गया",
"different_parent": "Cannot reorder collection with different parent",
"edit": "संग्रह संपादित करें",
"invalid_name": "कृपया संग्रह के लिए एक नाम प्रदान करें",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "मेरे संग्रह",
"name": "मेरा नया संग्रह",
"name_length_insufficient": "संग्रह का नाम कम से कम 3 वर्णों का होना चाहिए",
"new": "नया संग्रह",
"order_changed": "Collection Order Updated",
"renamed": "संग्रह का नाम बदला गया",
"request_in_use": "रिक्वेस्ट इन यूज़",
"save_as": "इस रूप में सेव करें",
@@ -142,6 +147,7 @@
"remove_team": "क्या आप वाकई इस टीम को हटाना चाहते हैं?",
"remove_telemetry": "क्या आप वाकई टेलीमेट्री से ऑप्ट-आउट करना चाहते हैं?",
"request_change": "क्या आप वाकई वर्तमान अनुरोध को छोड़ना चाहते हैं, सहेजे नहीं गए परिवर्तन खो जाएंगे।",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "क्या आप अपने कार्यक्षेत्र को क्लाउड से पुनर्स्थापित करना चाहेंगे? यह आपकी स्थानीय प्रगति को त्याग देगा।"
},
"count": {
@@ -311,6 +317,7 @@
"zen_mode": "ज़ेन मोड"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "संग्रह",
"confirm": "पुष्टि करें",
"edit_request": "अनुरोध संपादित करें",
@@ -390,6 +397,8 @@
"text": "मूलपाठ"
},
"copy_link": "प्रतिरूप जोड़ना",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "अवधि",
"enter_curl": "कर्ल दर्ज करें",
"generate_code": "उत्पन्न कोड",
@@ -397,8 +406,10 @@
"header_list": "हेडर सूची",
"invalid_name": "कृपया अनुरोध के लिए एक नाम प्रदान करें",
"method": "तरीका",
"moved": "Request moved",
"name": "अनुरोध नाम",
"new": "नई विनती",
"order_changed": "Request Order Updated",
"override": "अवहेलना",
"override_help": "हेडर में <kbd> सामग्री-प्रकार </kbd> सेट करें",
"overriden": "ओवरराइड",
@@ -422,6 +433,7 @@
"view_my_links": "मेरे लिंक देखें"
},
"response": {
"audio": "Audio",
"body": "प्रतिक्रिया निकाय",
"filter_response_body": "फ़िल्टर JSON रिस्पांस बॉडी (JSONPATH सिंटैक्स का उपयोग करता है)",
"headers": "हेडर",
@@ -435,6 +447,7 @@
"status": "दर्जा",
"time": "समय",
"title": "जवाब",
"video": "Video",
"waiting_for_connection": "जुडने के लिए इंतजार",
"xml": "एक्सएमएल"
},
@@ -629,6 +642,7 @@
"body": "शरीर",
"collections": "संग्रह",
"documentation": "प्रलेखन",
"environments": "Environments",
"headers": "हेडर",
"history": "इतिहास",
"mqtt": "MQTT",
@@ -653,6 +667,7 @@
"email_do_not_match": "ईमेल आपके खाते के विवरण के साथ मेल नहीं खाता है। अपनी टीम के मालिक से संपर्क करें।",
"exit": "निकास टीम",
"exit_disabled": "केवल मालिक टीम से बाहर नहीं निकल सकते",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "ईमेल प्रारूप अमान्य है",
"invalid_id": "अमान्य टीम आईडी। अपनी टीम के मालिक से संपर्क करें।",
"invalid_invite_link": "अमान्य आमंत्रित लिंक",
@@ -676,6 +691,7 @@
"member_removed": "उपयोगकर्ता हटा दिया",
"member_role_updated": "उपयोगकर्ता भूमिकाएँ अद्यतन की गईं",
"members": "सदस्यों",
"more_members": "+{count} more",
"name_length_insufficient": "टीम का नाम कम से कम 6 अक्षर लंबा होना चाहिए",
"name_updated": "टीम का नाम अपडेट किया गया",
"new": "नई टीम",
@@ -683,10 +699,13 @@
"new_name": "मेरी नई टीम",
"no_access": "आपके पास इन संग्रहों तक पहुंच नहीं है",
"no_invite_found": "निमंत्रण नहीं मिला। अपनी टीम के मालिक से संपर्क करें।",
"no_request_found": "Request not found.",
"not_found": "टीम नहीं मिली। अपनी टीम के मालिक से संपर्क करें।",
"not_valid_viewer": "आप एक वैध दर्शक नहीं हैं। अपनी टीम के मालिक से संपर्क करें।",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "लंबित आमंत्रण",
"permissions": "अनुमतियां",
"same_target_destination": "Same target and destination",
"saved": "टीम बचाया",
"select_a_team": "एक टीम का चयन करें",
"title": "टीमों",
@@ -714,5 +733,11 @@
"message": "संदेश",
"protocols": "प्रोटोकॉल",
"url": "यूआरएल"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -5,28 +5,29 @@
"choose_file": "Válasszon egy fájlt",
"clear": "Törlés",
"clear_all": "Összes törlése",
"close": "Close",
"close": "Bezárás",
"connect": "Kapcsolódás",
"connecting": "Connecting",
"connecting": "Kapcsolódás",
"copy": "Másolás",
"delete": "Törlés",
"disconnect": "Leválasztás",
"dismiss": "Eltüntetés",
"dont_save": "Ne mentse",
"download_file": "Fájl letöltése",
"drag_to_reorder": "Drag to reorder",
"drag_to_reorder": "Húzza az átrendezéshez",
"duplicate": "Kettőzés",
"edit": "Szerkesztés",
"filter": "Filter",
"filter": "Szűrő",
"go_back": "Vissza",
"group_by": "Group by",
"go_forward": "Előre",
"group_by": "Csoportosítás",
"label": "Címke",
"learn_more": "Tudjon meg többet",
"less": "Kevesebb",
"more": "Több",
"new": "Új",
"no": "Nem",
"open_workspace": "Open workspace",
"open_workspace": "Munkaterület megnyitása",
"paste": "Beillesztés",
"prettify": "Csinosítás",
"remove": "Eltávolítás",
@@ -37,7 +38,7 @@
"search": "Keresés",
"send": "Küldés",
"start": "Indítás",
"starting": "Starting",
"starting": "Indítás",
"stop": "Leállítás",
"to_close": "a bezáráshoz",
"to_navigate": "a navigáláshoz",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Gyűjtemény létrehozva",
"different_parent": "Nem lehet átrendezni a különböző szülővel rendelkező gyűjteményt",
"edit": "Gyűjtemény szerkesztése",
"invalid_name": "Adjon nevet a gyűjteménynek",
"invalid_root_move": "A gyűjtemény már a gyökérben van",
"moved": "Sikeresen áthelyezve",
"my_collections": "Saját gyűjtemények",
"name": "Saját új gyűjtemény",
"name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie",
"new": "Új gyűjtemény",
"order_changed": "Gyűjtemény sorrendje frissítve",
"renamed": "Gyűjtemény átnevezve",
"request_in_use": "A kérés használatban",
"save_as": "Mentés másként",
@@ -142,6 +147,7 @@
"remove_team": "Biztosan törölni szeretné ezt a csapatot?",
"remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?",
"request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.",
"save_unsaved_tab": "Szeretné menteni az ezen a lapon elvégzett változtatásokat?",
"sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát."
},
"count": {
@@ -174,8 +180,8 @@
"profile": "Jelentkezzen be a profilja megtekintéséhez",
"protocols": "A protokollok üresek",
"schema": "Kapcsolódjon egy GraphQL-végponthoz a séma megtekintéséhez",
"shortcodes": "Shortcodes are empty",
"subscription": "Subscriptions are empty",
"shortcodes": "A rövid kódok üresek",
"subscription": "A feliratkozások üresek",
"team_name": "A csapat neve üres",
"teams": "Ön nem tartozik semmilyen csapathoz",
"tests": "Nincsenek tesztek ehhez a kéréshez"
@@ -188,13 +194,13 @@
"deleted": "Környezet törlése",
"edit": "Környezet szerkesztése",
"invalid_name": "Adjon nevet a környezetnek",
"my_environments": "My Environments",
"my_environments": "Saját környezetek",
"nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva",
"new": "Új környezet",
"no_environment": "Nincs környezet",
"no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.",
"select": "Környezet kiválasztása",
"team_environments": "Team Environments",
"team_environments": "Csapatkörnyezetek",
"title": "Környezetek",
"updated": "Környezet frissítve",
"variable_list": "Változólista"
@@ -203,9 +209,9 @@
"browser_support_sse": "Úgy tűnik, hogy ez a böngésző nem támogatja a kiszolgáló által küldött eseményeket.",
"check_console_details": "Nézze meg a konzolnaplót a részletekért.",
"curl_invalid_format": "A cURL nincs megfelelően formázva",
"danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these teams:",
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
"danger_zone": "Veszélyes zóna",
"delete_account": "Az Ön fiókja jelenleg tulajdonos ezekben a csapatokban:",
"delete_account_description": "El kell távolítani magát, át kell adnia a tulajdonjogot vagy törölnie kell ezeket a csapatokat, mielőtt törölhetné a fiókját.",
"empty_req_name": "Üres kérésnév",
"f12_details": "(F12 a részletekért)",
"gql_prettify_invalid_query": "Nem sikerült csinosítani egy érvénytelen lekérdezést, oldja meg a lekérdezés szintaktikai hibáit, és próbálja újra",
@@ -213,13 +219,13 @@
"incorrect_email": "Hibás e-mail",
"invalid_link": "Érvénytelen hivatkozás",
"invalid_link_description": "A kattintott hivatkozás érvénytelen vagy lejárt.",
"json_parsing_failed": "Invalid JSON",
"json_parsing_failed": "Érvénytelen JSON",
"json_prettify_invalid_body": "Nem sikerült csinosítani egy érvénytelen törzset, oldja meg a JSON szintaktikai hibáit, és próbálja újra",
"network_error": "Úgy tűnik, hogy hálózati hiba van. Próbálja újra.",
"network_fail": "Nem sikerült elküldeni a kérést",
"no_duration": "Nincs időtartam",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"no_results_found": "Nincs találat",
"page_not_found": "Ez az oldal nem található",
"script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt",
"something_went_wrong": "Valami elromlott",
"test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt"
@@ -232,9 +238,9 @@
"title": "Exportálás"
},
"filter": {
"all": "All",
"none": "None",
"starred": "Starred"
"all": "Összes",
"none": "Nincs",
"starred": "Csillagozott"
},
"folder": {
"created": "Mappa létrehozva",
@@ -250,7 +256,7 @@
"subscriptions": "Feliratkozások"
},
"group": {
"time": "Time",
"time": "Idő",
"url": "URL"
},
"header": {
@@ -310,31 +316,32 @@
"zen_mode": "Zen mód"
},
"modal": {
"close_unsaved_tab": "Elmentetlen változtatásai vannak",
"collections": "Gyűjtemények",
"confirm": "Megerősítés",
"edit_request": "Kérés szerkesztése",
"import_export": "Importálás és exportálás"
},
"mqtt": {
"already_subscribed": "You are already subscribed to this topic.",
"clean_session": "Clean Session",
"clear_input": "Clear input",
"clear_input_on_send": "Clear input on send",
"client_id": "Client ID",
"color": "Pick a color",
"already_subscribed": "Ön már feliratkozott erre a témára.",
"clean_session": "Munkamenet törlése",
"clear_input": "Bevitel törlése",
"clear_input_on_send": "Bevitel törlése küldéskor",
"client_id": "Ügyfél-azonosító",
"color": "Válasszon színt",
"communication": "Kommunikáció",
"connection_config": "Connection Config",
"connection_not_authorized": "This MQTT connection does not use any authentication.",
"invalid_topic": "Please provide a topic for the subscription",
"keep_alive": "Keep Alive",
"connection_config": "Kapcsolat beállításai",
"connection_not_authorized": "Ez az MQTT-kapcsolat nem használ semmilyen hitelesítést.",
"invalid_topic": "Adjon témát a feliratkozáshoz",
"keep_alive": "Életben tartás",
"log": "Napló",
"lw_message": "Last-Will Message",
"lw_qos": "Last-Will QoS",
"lw_retain": "Last-Will Retain",
"lw_topic": "Last-Will Topic",
"lw_message": "Utolsó kívánság üzenet",
"lw_qos": "Utolsó kívánság QoS",
"lw_retain": "Utolsó kívánság megtartás",
"lw_topic": "Utolsó kívánság téma",
"message": "Üzenet",
"new": "New Subscription",
"not_connected": "Please start a MQTT connection first.",
"new": "Új feliratkozás",
"not_connected": "Először indítson egy MQTT-kapcsolatot.",
"publish": "Közzététel",
"qos": "QoS",
"ssl": "SSL",
@@ -361,7 +368,7 @@
},
"profile": {
"app_settings": "Alkalmazás beállításai",
"default_hopp_displayname": "Unnamed User",
"default_hopp_displayname": "Névtelen felhasználó",
"editor": "Szerkesztő",
"editor_description": "A szerkesztők hozzáadhatnak, szerkeszthetnek és törölhetnek kéréseket.",
"email_verification_mail": "Egy ellenőrző e-mail el lett küldve az e-mail-címére. Kattintson a hivatkozásra az e-mail-címe ellenőrzéséhez.",
@@ -384,22 +391,26 @@
"choose_language": "Nyelv kiválasztása",
"content_type": "Tartalom típusa",
"content_type_titles": {
"others": "Others",
"structured": "Structured",
"text": "Text"
"others": "Egyebek",
"structured": "Szerkesztett",
"text": "Szöveg"
},
"copy_link": "Hivatkozás másolása",
"different_collection": "Nem lehet átrendezni a különböző gyűjteményekből érkező kéréseket",
"duplicated": "Kérés megkettőzve",
"duration": "Időtartam",
"enter_curl": "cURL megadása",
"enter_curl": "cURL-parancs megadása",
"generate_code": "Kód előállítása",
"generated_code": "Előállított kód",
"header_list": "Fejléclista",
"invalid_name": "Adjon nevet a kérésnek",
"method": "Módszer",
"moved": "Kérés áthelyezve",
"name": "Kérés neve",
"new": "Új kérés",
"order_changed": "Kérés sorrendje frissítve",
"override": "Felülbírálás",
"override_help": "A <kbd>Content-Type</kbd> beállítása a fejlécekben",
"override_help": "<kbd>Content-Type</kbd> beállítása a fejlécekben",
"overriden": "Felülbírálva",
"parameter_list": "Lekérdezési paraméterek",
"parameters": "Paraméterek",
@@ -418,11 +429,12 @@
"type": "Kérés típusa",
"url": "URL",
"variables": "Változók",
"view_my_links": "View my links"
"view_my_links": "Saját hivatkozások megtekintése"
},
"response": {
"audio": "Hang",
"body": "Válasz törzse",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"filter_response_body": "JSON-válasz törzsének szűrése (JSONPath szintaxist használ)",
"headers": "Fejlécek",
"html": "HTML",
"image": "Kép",
@@ -434,13 +446,14 @@
"status": "Állapot",
"time": "Idő",
"title": "Válasz",
"video": "Videó",
"waiting_for_connection": "várakozás kapcsolódásra",
"xml": "XML"
},
"settings": {
"accent_color": "Kiemelőszín",
"account": "Fiók",
"account_deleted": "Your account has been deleted",
"account_deleted": "A fiókja törölve lett",
"account_description": "A fiókbeállítások személyre szabása.",
"account_email_description": "Az Ön elsődleges e-mail-címe.",
"account_name_description": "Ez a megjelenített neve.",
@@ -449,8 +462,8 @@
"change_font_size": "Betűméret megváltoztatása",
"choose_language": "Nyelv kiválasztása",
"dark_mode": "Sötét",
"delete_account": "Delete account",
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
"delete_account": "Fiók törlése",
"delete_account_description": "Ha törli a fiókját, akkor az összes adata véglegesen törlésre kerül. Ezt a műveletet nem lehet visszavonni.",
"expand_navigation": "Navigáció kinyitása",
"experiments": "Kísérletek",
"experiments_notice": "Ez olyan kísérletek gyűjteménye, amelyeken dolgozunk, és amelyek hasznosak, szórakoztatóak lehetnek, mindkettő, vagy egyik sem. Ezek nem véglegesek és nem stabilak, ezért ha valami túl furcsa dolog történik, ne essen pánikba. Egyszerűen kapcsolja ki a hibás dolgot. Viccet félretéve, ",
@@ -477,8 +490,8 @@
"proxy_use_toggle": "A proxy középprogram használata a kérések küldéséhez",
"read_the": "Olvassa el:",
"reset_default": "Visszaállítás az alapértelmezettre",
"short_codes": "Short codes",
"short_codes_description": "Short codes which were created by you.",
"short_codes": "Rövid kódok",
"short_codes_description": "Az Ön által létrehozott rövid kódok.",
"sidebar_on_left": "Oldalsáv a bal oldalon",
"sync": "Szinkronizálás",
"sync_collections": "Gyűjtemények",
@@ -492,16 +505,16 @@
"theme_description": "Az alkalmazás témájának személyre szabása.",
"use_experimental_url_bar": "Kísérleti URL-sáv használata a környezet kiemelésével",
"user": "Felhasználó",
"verified_email": "Verified email",
"verified_email": "Ellenőrzött e-mail-cím",
"verify_email": "E-mail-cím ellenőrzése"
},
"shortcodes": {
"actions": "Actions",
"created_on": "Created on",
"deleted": "Shortcode deleted",
"method": "Method",
"not_found": "Shortcode not found",
"short_code": "Short code",
"actions": "Műveletek",
"created_on": "Létrehozva",
"deleted": "Rövid kód törölve",
"method": "Módszer",
"not_found": "A rövid kód nem található",
"short_code": "Rövid kód",
"url": "URL"
},
"shortcut": {
@@ -543,9 +556,9 @@
"title": "Kérés"
},
"response": {
"copy": "Copy response to clipboard",
"download": "Download response as file",
"title": "Response"
"copy": "Válasz másolása a vágólapra",
"download": "Válasz letöltés fájlként",
"title": "Válasz"
},
"theme": {
"black": "Téma átváltása fekete módra",
@@ -563,8 +576,8 @@
},
"socketio": {
"communication": "Kommunikáció",
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
"event_name": "Esemény neve",
"connection_not_authorized": "Ez a SocketIO-kapcsolat nem használ semmilyen hitelesítést.",
"event_name": "Esemény vagy téma neve",
"events": "Események",
"log": "Napló",
"url": "URL"
@@ -581,9 +594,9 @@
"connected": "Kapcsolódva",
"connected_to": "Kapcsolódva ehhez: {name}",
"connecting_to": "Kapcsolódás ehhez: {name}…",
"connection_error": "Failed to connect",
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"connection_error": "Nem sikerült kapcsolódni",
"connection_failed": "A kapcsolódás sikertelen",
"connection_lost": "A kapcsolat elveszett",
"copied_to_clipboard": "Vágólapra másolva",
"deleted": "Törölve",
"deprecated": "ELAVULT",
@@ -598,17 +611,17 @@
"history_deleted": "Előzmények törölve",
"linewrap": "Sorok tördelése",
"loading": "Betöltés…",
"message_received": "Message: {message} arrived on topic: {topic}",
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
"message_received": "Üzenet: {message} érkezett ehhez a témához: {topic}",
"mqtt_subscription_failed": "Valami elromlott a következő témára való feliratkozás során: {topic}",
"none": "Nincs",
"nothing_found": "Semmi sem található ehhez:",
"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}",
"published_error": "Valami elromlott a következő üzenet közzététele során: {topic}, ehhez a témához: {message}",
"published_message": "Közzétett üzenet: {message}, ehhez a témához: {topic}",
"reconnection_error": "Nem sikerült újrakapcsolódni",
"subscribed_failed": "Nem sikerült feliratkozni erre a témára: {topic}",
"subscribed_success": "Sikeresen feliratkozott erre a témára: {topic}",
"unsubscribed_failed": "Nem sikerült leiratkozni erről a témáról: {topic}",
"unsubscribed_success": "Sikeresen leiratkozott erről a témáról: {topic}",
"waiting_send_request": "Várakozás a kérés elküldésére"
},
"support": {
@@ -628,6 +641,7 @@
"body": "Törzs",
"collections": "Gyűjtemények",
"documentation": "Dokumentáció",
"environments": "Környezetek",
"headers": "Fejlécek",
"history": "Előzmények",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Az e-mail-cím nem egyezik a fiókja részleteivel. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"exit": "Kilépés a csapatból",
"exit_disabled": "Csak a tulajdonos nem léphet ki a csapatból",
"invalid_coll_id": "Érvénytelen gyűjteményazonosító",
"invalid_email_format": "Az e-mail formátuma érvénytelen",
"invalid_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"invalid_invite_link": "Érvénytelen meghívási hivatkozás",
@@ -675,6 +690,7 @@
"member_removed": "Felhasználó eltávolítva",
"member_role_updated": "Felhasználói szerepek frissítve",
"members": "Tagok",
"more_members": "+{count} további",
"name_length_insufficient": "A csapat nevének legalább 6 karakter hosszúságúnak kell lennie",
"name_updated": "Csapatnév frissítve",
"new": "Új csapat",
@@ -682,10 +698,13 @@
"new_name": "Saját új csapat",
"no_access": "Nincs szerkesztési jogosultsága ezekhez a gyűjteményekhez",
"no_invite_found": "A meghívás nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"no_request_found": "A kérés nem található.",
"not_found": "A csapat nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"not_valid_viewer": "Ön nem érvényes megtekintő. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"parent_coll_move": "Nem lehet áthelyezni a gyűjteményt egy gyermekgyűjteménybe",
"pending_invites": "Függőben lévő meghívások",
"permissions": "Jogosultságok",
"same_target_destination": "Ugyanaz a cél és célhely",
"saved": "Csapat elmentve",
"select_a_team": "Csapat kiválasztása",
"title": "Csapatok",
@@ -693,9 +712,9 @@
"we_sent_invite_link_description": "Kérje meg az összes meghívottat, hogy nézzék meg a beérkező leveleiket. Kattintsanak a hivatkozásra a csapathoz való csatlakozáshoz."
},
"team_environment": {
"deleted": "Environment Deleted",
"duplicate": "Environment Duplicated",
"not_found": "Environment not found."
"deleted": "Környezet törölve",
"duplicate": "Környezet megkettőzve",
"not_found": "A környezet nem található."
},
"test": {
"failed": "teszt sikertelen",
@@ -713,5 +732,11 @@
"message": "Üzenet",
"protocols": "Protokollok",
"url": "URL"
},
"workspace": {
"change": "Munkaterület váltása",
"personal": "Saját munkaterület",
"team": "Csapat-munkaterület",
"title": "Munkaterületek"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Edit",
"filter": "Tanggapan filter",
"go_back": "Kembali",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Label",
"learn_more": "Pelajari lebih lanjut",
@@ -117,18 +118,22 @@
},
"collection": {
"created": "Koleksi dibuat",
"different_parent": "Tidak dapat mengubah urutan koleksi dengan induk yang berbeda",
"edit": "Mengubah Koleksi",
"invalid_name": "Berikan nama untuk Koleksi",
"invalid_root_move": "Koleksi sudah berada di akar direktori",
"moved": "Berhasil Dipindahkan",
"my_collections": "Koleksi Saya",
"name": "Koleksi Baru Saya",
"name_length_insufficient": "Nama koleksi harus minimal 3 karakter",
"new": "Koleksi baru",
"order_changed": "Pembaruan Urutan Koleksi",
"renamed": "Koleksi berganti nama",
"request_in_use": "Permintaan sedang digunakan",
"save_as": "Simpan Sebagai",
"select": "Pilih Koleksi",
"select_location": "Pilih lokasi",
"select_team": "Pilih team",
"select_team": "Pilih tim",
"team_collections": "Koleksi Tim"
},
"confirm": {
@@ -142,6 +147,7 @@
"remove_team": "Apakah Anda yakin ingin menghapus tim ini?",
"remove_telemetry": "Apakah Anda yakin ingin menyisih dari Telemetri?",
"request_change": "Apakah Anda yakin ingin membuang permintaan saat ini, perubahan yang belum disimpan akan hilang.",
"save_unsaved_tab": "Apakah Anda ingin menyimpan perubahan yang dibuat di tab ini?",
"sync": "Apakah Anda ingin memulihkan ruang kerja Anda dari cloud? Ini akan membuang kemajuan lokal Anda."
},
"count": {
@@ -174,8 +180,8 @@
"profile": "Masuk untuk melihat profil Anda",
"protocols": "Protokol kosong",
"schema": "Hubungkan ke endpoint GraphQL untuk melihat skema",
"shortcodes": "Shortcodes are empty",
"subscription": "Subscriptions are empty",
"shortcodes": "Shortcodes kosong",
"subscription": "Langganan kosong",
"team_name": "Nama team kosong",
"teams": "Kamu bukan di team manapun",
"tests": "Tidak ada tes untuk permintaan ini"
@@ -183,19 +189,19 @@
"environment": {
"add_to_global": "Tambahkan ke Global",
"added": "Tambahan Environment",
"create_new": "Membuat baru environment",
"create_new": "Membuat environment baru",
"created": "Environment dibuat",
"deleted": "Environment dihapus",
"edit": "Sunting Environment",
"invalid_name": "Tolong beri nama untuk environment",
"my_environments": "My Environments",
"nested_overflow": "variabel environment bersarang dibatasi hingga 10 level",
"my_environments": "Environment Saya",
"nested_overflow": "Variabel environment bersarang dibatasi hingga 10 level",
"new": "Environment Baru",
"no_environment": "No environment",
"no_environment_description": "Tidak ada environment yang dipilih. Pilih apa yang harus dilakukan dengan variabel berikut.",
"select": "Pilih environment",
"team_environments": "Team Environments",
"title": "Environments",
"team_environments": "Environment Tim",
"title": "Environment",
"updated": "Environment diperbarui",
"variable_list": "Daftar Variable"
},
@@ -204,8 +210,8 @@
"check_console_details": "Periksa console log untuk detailnya.",
"curl_invalid_format": "cURL tidak diformat dengan benar",
"danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these teams:",
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
"delete_account": "Akun Anda saat ini merupakan pemilik dalam tim-tim ini:",
"delete_account_description": "Anda harus menghapus diri Anda dari tim-tim ini, mentransfer kepemilikan, atau menghapus tim-tim ini sebelum Anda dapat menghapus akun Anda.",
"empty_req_name": "Nama Permintaan Kosong",
"f12_details": "(F12 untuk detailnya)",
"gql_prettify_invalid_query": "Tidak dapat prettify kueri yang tidak valid, menyelesaikan kesalahan sintaksis kueri, dan coba lagi",
@@ -288,7 +294,7 @@
"from_json_description": "Impor dari Hoppscotch berkas koleksi",
"from_my_collections": "Impor dari Koleksi Saya",
"from_my_collections_description": "Impor dari Berkas Koleksi Saya",
"from_openapi": "Import dari OpenAPI",
"from_openapi": "Impor dari OpenAPI",
"from_openapi_description": "Impor dari OpenAPI syarat berkas (YML/JSON)",
"from_postman": "Impor dari Postman",
"from_postman_description": "Impor dari Koleksi Postman",
@@ -310,22 +316,23 @@
"zen_mode": "Zen mode"
},
"modal": {
"close_unsaved_tab": "Anda memiliki perubahan yang belum disimpan",
"collections": "Koleksi",
"confirm": "Mengonfirmasi",
"edit_request": "Edit Request",
"import_export": "Impor / Ekspor"
},
"mqtt": {
"already_subscribed": "You are already subscribed to this topic.",
"clean_session": "Clean Session",
"clear_input": "Clear input",
"clear_input_on_send": "Clear input on send",
"already_subscribed": "Anda sudah berlangganan topik ini.",
"clean_session": "Sesi Bersih",
"clear_input": "Hapus input",
"clear_input_on_send": "Hapus input saat mengirim",
"client_id": "Client ID",
"color": "Pick a color",
"color": "Pilih warna",
"communication": "Komunikasi",
"connection_config": "Connection Config",
"connection_not_authorized": "This MQTT connection does not use any authentication.",
"invalid_topic": "Please provide a topic for the subscription",
"connection_config": "Konfigurasi Koneksi",
"connection_not_authorized": "Koneksi MQTT ini tidak menggunakan otentikasi",
"invalid_topic": "Harap berikan topik untuk langganan",
"keep_alive": "Keep Alive",
"log": "Log",
"lw_message": "Last-Will Message",
@@ -333,8 +340,8 @@
"lw_retain": "Last-Will Retain",
"lw_topic": "Last-Will Topic",
"message": "Pesan",
"new": "New Subscription",
"not_connected": "Please start a MQTT connection first.",
"new": "Langganan Baru",
"not_connected": "Mulai koneksi MQTT terlebih dahulu",
"publish": "Menerbitkan",
"qos": "QoS",
"ssl": "SSL",
@@ -389,15 +396,19 @@
"text": "Text"
},
"copy_link": "Salin tautan",
"different_collection": "Tidak dapat mengubah urutan permintaan dari koleksi yang berbeda",
"duplicated": "Request duplicated",
"duration": "Durasi",
"enter_curl": "Masukkan cURL",
"generate_code": "Generate code",
"generated_code": "Generated code",
"generate_code": "Hasilkan kode",
"generated_code": "Hasilkan kode",
"header_list": "Daftar Header",
"invalid_name": "Harap berikan nama untuk request",
"method": "Method",
"moved": "Request moved",
"name": "Request nama",
"new": "Request baru",
"order_changed": "Urutan Request Diperbarui",
"override": "Membatalkan",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Diganti",
@@ -421,6 +432,7 @@
"view_my_links": "Lihat tautan saya"
},
"response": {
"audio": "Audio",
"body": "Response Body",
"filter_response_body": "Filter body respons JSON (menggunakan sintaks JSONPath)",
"headers": "Headers",
@@ -434,13 +446,14 @@
"status": "Status",
"time": "Waktu",
"title": "Response",
"video": "Video",
"waiting_for_connection": "Menunggu koneksi",
"xml": "XML"
},
"settings": {
"accent_color": "Accent color",
"account": "Akun",
"account_deleted": "Your account has been deleted",
"account_deleted": "Akun Anda telah dihapus",
"account_description": "Sesuaikan pengaturan akun Anda.",
"account_email_description": "Alamat surel utama Anda.",
"account_name_description": "Ini adalah nama tampilan Anda.",
@@ -596,7 +609,7 @@
"file_imported": "File diimpor",
"finished_in": "Selesai dalam {duration} ms",
"history_deleted": "Riwayat dihapus",
"linewrap": "Wrap lines",
"linewrap": "Bungkus baris",
"loading": "Memuat...",
"message_received": "Pesan: {message} tiba di topik: {topic}",
"mqtt_subscription_failed": "Terjadi masalah saat berlangganan topik: {topic}",
@@ -628,6 +641,7 @@
"body": "Body",
"collections": "Collections",
"documentation": "Dokumentasi",
"environments": "Environments",
"headers": "Headers",
"history": "Riwayat",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Surel tidak cocok dengan detail akun Anda. Hubungi pemilik tim Anda.",
"exit": "Keluar dari Tim",
"exit_disabled": "Hanya pemilik yang tidak dapat keluar dari tim",
"invalid_coll_id": "ID koleksi tidak valid",
"invalid_email_format": "Format surel tidak valid",
"invalid_id": "ID tim tidak valid. Hubungi pemilik tim Anda.",
"invalid_invite_link": "Tautan undangan tidak valid",
@@ -675,6 +690,7 @@
"member_removed": "Pengguna dihapus",
"member_role_updated": "Peran pengguna diperbarui",
"members": "Anggota",
"more_members": "+{count} lebih",
"name_length_insufficient": "Nama tim harus setidaknya 6 karakter",
"name_updated": "Nama tim diperbarui",
"new": "Tim Baru",
@@ -682,10 +698,13 @@
"new_name": "Tim baru saya",
"no_access": "Anda tidak memiliki akses edit ke collections ini",
"no_invite_found": "Undangan tidak ditemukan. Hubungi pemilik tim Anda.",
"no_request_found": "Request tidak ditemukan.",
"not_found": "Tim tidak ditemukan. Hubungi pemilik tim Anda.",
"not_valid_viewer": "Anda bukan penonton yang valid. Hubungi pemilik tim Anda.",
"parent_coll_move": "Tidak dapat memindahkan koleksi ke dalam koleksi anak",
"pending_invites": "Undangan tertunda",
"permissions": "Izin",
"same_target_destination": "Sama tujuan dan destinasi",
"saved": "Tim disimpan",
"select_a_team": "Pilih tim",
"title": "tim",
@@ -693,9 +712,9 @@
"we_sent_invite_link_description": "Minta semua undangan untuk memeriksa kotak masuk mereka. Klik tautan untuk bergabung dengan tim."
},
"team_environment": {
"deleted": "Environment Deleted",
"duplicate": "Environment Duplicated",
"not_found": "Environment not found."
"deleted": "Environment dihapus",
"duplicate": "Environment diduplikasi",
"not_found": "Environment tidak ditemukan."
},
"test": {
"failed": "Tes gagal",
@@ -713,5 +732,11 @@
"message": "Pesan",
"protocols": "Protokol",
"url": "URL"
},
"workspace": {
"change": "Beralih workspace",
"personal": "Workspace Saya",
"team": "Workspace Tim",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Modifica",
"filter": "Filter",
"go_back": "Torna indietro",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Etichetta",
"learn_more": "Per saperne di più",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Raccolta creata",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Modifica raccolta",
"invalid_name": "Si prega di fornire un nome valido per la raccolta",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Le Mie Raccolte",
"name": "La mia nuova raccolta",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Nuova raccolta",
"order_changed": "Collection Order Updated",
"renamed": "Raccolta rinominata",
"request_in_use": "Request in use",
"save_as": "Salva come",
@@ -142,6 +147,7 @@
"remove_team": "Sei sicuro di voler eliminare questo team?",
"remove_telemetry": "Sei sicuro di voler disattivare la telemetria?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Vuoi ripristinare il tuo spazio di lavoro con quello del cloud? Questo annullerà le tue modifiche fatte in locale."
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Modalità zen"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Raccolte",
"confirm": "Conferma",
"edit_request": "Modifica richiesta",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Copia collegamento",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Durata",
"enter_curl": "Inserisci cURL",
"generate_code": "Genera codice",
@@ -396,8 +405,10 @@
"header_list": "Elenco intestazioni",
"invalid_name": "Si prega di fornire un nome per la richiesta",
"method": "Metodo",
"moved": "Request moved",
"name": "Nome richiesta",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Corpo della risposta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Intestazioni",
@@ -434,6 +446,7 @@
"status": "Stato",
"time": "Tempo impiegato",
"title": "Risposta",
"video": "Video",
"waiting_for_connection": "In attesa di connessione",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Corpo",
"collections": "Raccolte",
"documentation": "Documentazione",
"environments": "Environments",
"headers": "Intestazioni",
"history": "Cronologia",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "L'indirizzo email non corrisponde a quella del tuo account. Contatta il proprietario del team.",
"exit": "Lascia il team",
"exit_disabled": "Solo il proprietario non può lasciare il team",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Il formato dell'email non è valido",
"invalid_id": "L'ID del team non è valido. Contatta il proprietario del team.",
"invalid_invite_link": "Il link di invito non è valido",
@@ -675,6 +690,7 @@
"member_removed": "Utente rimosso",
"member_role_updated": "Ruoli dell'utente aggiornati",
"members": "Membri",
"more_members": "+{count} more",
"name_length_insufficient": "Il nome del team deve essere lungo almeno 6 caratteri",
"name_updated": "Nome del team aggiornato",
"new": "Nuovo team",
@@ -682,10 +698,13 @@
"new_name": "Il mio nuovo team",
"no_access": "Non sei autorizzato a modificare queste raccolte",
"no_invite_found": "Invito non trovato. Contatta il proprietario del team.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "Non sei autorizzato a visualizzare. Contatta il proprietario del team.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Inviti pendenti",
"permissions": "Permessi",
"same_target_destination": "Same target and destination",
"saved": "Team salvato",
"select_a_team": "Seleziona un team",
"title": "Team",
@@ -713,5 +732,11 @@
"message": "Messaggio",
"protocols": "Protocolli",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "編集",
"filter": "フィルター",
"go_back": "戻る",
"go_forward": "Go forward",
"group_by": "グループ化",
"label": "ラベル",
"learn_more": "もっと詳しく",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "コレクションが作成されました",
"different_parent": "Cannot reorder collection with different parent",
"edit": "コレクションの編集",
"invalid_name": "コレクション名を入力してください",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "マイコレクション",
"name": "新しいマイコレクション",
"name_length_insufficient": "コレクション名は3文字以上である必要があります",
"new": "新しいコレクション",
"order_changed": "Collection Order Updated",
"renamed": "コレクション名が変更されました",
"request_in_use": "使用中のリクエスト",
"save_as": "名前を付けて保存",
@@ -142,6 +147,7 @@
"remove_team": "このチームを削除してもよろしいですか?",
"remove_telemetry": "テレメトリをオプトアウトしてもよろしいですか?",
"request_change": "現在のリクエストを削除してもよろしいですか?保存されていない変更は削除されます。",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "クラウドからワークスペースを復元しますか?この場合、ローカルの進行状況は破棄されます。"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "禅モード"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "コレクション",
"confirm": "確認",
"edit_request": "リクエストの編集",
@@ -389,6 +396,8 @@
"text": "テキスト"
},
"copy_link": "リンクをコピー",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "間隔",
"enter_curl": "cURLコマンドを入力してください",
"generate_code": "コードを生成",
@@ -396,8 +405,10 @@
"header_list": "ヘッダーリスト",
"invalid_name": "リクエスト名を入力してください",
"method": "メソッド",
"moved": "Request moved",
"name": "リクエスト名",
"new": "新しいリクエスト",
"order_changed": "Request Order Updated",
"override": "上書き",
"override_help": "リクエストの <kbd>Content-Type</kbd> ヘッダを上書き",
"overriden": "上書きされました",
@@ -421,6 +432,7 @@
"view_my_links": "自分のリンクを見る"
},
"response": {
"audio": "Audio",
"body": "レスポンスボディ",
"filter_response_body": "JSONレスポンスボディをフィルタ (JSONPathシンタックスを使用)",
"headers": "ヘッダー",
@@ -434,6 +446,7 @@
"status": "ステータス",
"time": "時間",
"title": "レスポンス",
"video": "Video",
"waiting_for_connection": "接続を待っています",
"xml": "XML"
},
@@ -455,8 +468,8 @@
"experiments": "試験的な機能",
"experiments_notice": "これらは試験的に実装している機能で、役に立つかもしれないし、楽しいかもしれないし、両方かもしれないし、はたまたどちらでもないかもしれません。これらは未完成で、安定したものではありません。何か問題がありましたら、こちらより報告をお願いします。→",
"extension_ver_not_reported": "未報告",
"extensions": "拡張機能",
"extension_version": "ブラウザ拡張機能のバージョン",
"extensions": "拡張機能",
"extensions_use_toggle": "ブラウザ拡張機能を使用してリクエストを送信する(利用可能な場合)",
"follow": "フォローする",
"font_size": "フォントサイズ",
@@ -581,6 +594,9 @@
"connected": "接続済み",
"connected_to": "{名前}に接続しました",
"connecting_to": "{名前}に接続しています...",
"connection_error": "Failed to connect",
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"copied_to_clipboard": "クリップボードにコピーしました",
"deleted": "削除されました",
"deprecated": "非推奨",
@@ -625,6 +641,7 @@
"body": "ボディ",
"collections": "コレクション",
"documentation": "ドキュメント",
"environments": "Environments",
"headers": "ヘッダー",
"history": "履歴",
"mqtt": "MQTT",
@@ -649,6 +666,7 @@
"email_do_not_match": "メールアドレスがアカウント情報と一致しません。チームの管理者に連絡してください。",
"exit": "チームから退出",
"exit_disabled": "管理者はチームから退出できません",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "メールアドレスの形式が無効です",
"invalid_id": "チームIDが無効です。チームの管理者に連絡してください。",
"invalid_invite_link": "招待リンクが無効です",
@@ -672,6 +690,7 @@
"member_removed": "ユーザーが削除されました",
"member_role_updated": "ユーザーロールが更新されました",
"members": "メンバー",
"more_members": "+{count} more",
"name_length_insufficient": "チーム名は6文字以上である必要があります",
"name_updated": "チーム名が更新されました",
"new": "新しいチーム",
@@ -679,10 +698,13 @@
"new_name": "私の新しいチーム",
"no_access": "これらのコレクションを編集することはできません",
"no_invite_found": "招待が見つかりません。チームの管理者に連絡してください。",
"no_request_found": "Request not found.",
"not_found": "チームが見つかりません。チームの管理者に連絡してください。",
"not_valid_viewer": "あなたは有効な閲覧者ではありません。チームの管理者に連絡してください。",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "招待の保留",
"permissions": "権限",
"same_target_destination": "Same target and destination",
"saved": "チームが保存されました",
"select_a_team": "チームを選択",
"title": "チーム",
@@ -710,5 +732,11 @@
"message": "メッセージ",
"protocols": "プロトコル",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "편집",
"filter": "Filter",
"go_back": "돌아가기",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "이름",
"learn_more": "더 알아보기",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "모음집 생성됨",
"different_parent": "Cannot reorder collection with different parent",
"edit": "모음집 편집",
"invalid_name": "모음집 이름을 바르게 입력하세요.",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "내 모음집",
"name": "내 새 모음집",
"name_length_insufficient": "모음집 이름은 최소 세 글자 이상이어야 합니다.",
"new": "새 모음집",
"order_changed": "Collection Order Updated",
"renamed": "모음집 이름이 변경됨",
"request_in_use": "사용 중인 요청",
"save_as": "다른 이름으로 저장",
@@ -142,6 +147,7 @@
"remove_team": "이 팀을 삭제하겠습니까?",
"remove_telemetry": "진단 데이터를 보내지 않겠습니까?",
"request_change": "현재 요청을 취소하시겠습니까? 저장되지 않은 변경사항은 삭제됩니다.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "이 작업 공간을 동기화하겠습니까?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "전체화면"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "모음집",
"confirm": "확인",
"edit_request": "요청 수정",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "링크 복사",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "소요 시간",
"enter_curl": "cURL 입력",
"generate_code": "코드 생성",
@@ -396,8 +405,10 @@
"header_list": "헤더 목록",
"invalid_name": "요청 이름을 바르게 입력하세요.",
"method": "메서드",
"moved": "Request moved",
"name": "요청 이름",
"new": "새로운 요청",
"order_changed": "Request Order Updated",
"override": "덮어쓰기",
"override_help": "헤더에 <kbd>Content-Type</kbd>를 설정해주세요.",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "내 링크 보기"
},
"response": {
"audio": "Audio",
"body": "응답 본문",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "헤더",
@@ -434,6 +446,7 @@
"status": "상태",
"time": "시간",
"title": "제목",
"video": "Video",
"waiting_for_connection": "연결 대기 중",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "본문",
"collections": "모음집",
"documentation": "문서",
"environments": "Environments",
"headers": "헤더",
"history": "이력",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "이 계정의 이메일이 일치하지 않습니다. 팀 소유자에게 문의하세요.",
"exit": "팀 나가기",
"exit_disabled": "소유자는 팀을 나갈 수 없습니다.",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "이메일을 바르게 입력하세요.",
"invalid_id": "팀 아이디가 올바르지 않습니다. 팀 소유자에게 문의하세요.",
"invalid_invite_link": "초대 링크가 올바르지 않습니다.",
@@ -675,6 +690,7 @@
"member_removed": "사용자가 삭제됨",
"member_role_updated": "사용자 역할 업데이트됨",
"members": "구성원",
"more_members": "+{count} more",
"name_length_insufficient": "팀 이름은 여섯 글자 이상이어야 합니다.",
"name_updated": "팀 이름 업데이트됨",
"new": "새 팀",
@@ -682,10 +698,13 @@
"new_name": "새 팀",
"no_access": "이 모음집을 수정할 권한이 없습니다.",
"no_invite_found": "초대 내역을 찾을 수 없습니다. 팀 소유자에게 문의하세요.",
"no_request_found": "Request not found.",
"not_found": "팀을 찾을 수 없습니다. 팀 소유자에게 문의하세요.",
"not_valid_viewer": "뷰어 권한이 없습니다. 팀 소유자에게 문의하세요.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "응답을 기다리는 초대",
"permissions": "권한",
"same_target_destination": "Same target and destination",
"saved": "팀이 저장됨",
"select_a_team": "팀을 선택하세요.",
"title": "팀",
@@ -713,5 +732,11 @@
"message": "메시지",
"protocols": "프로토콜",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Bewerking",
"filter": "Filter",
"go_back": "Ga terug",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Label",
"learn_more": "Leer meer",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Collectie gemaakt",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Verzameling bewerken",
"invalid_name": "Geef een geldige naam op voor de collectie",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Mijn collecties",
"name": "Mijn nieuwe collectie",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Nieuwe collectie",
"order_changed": "Collection Order Updated",
"renamed": "Collectie hernoemd",
"request_in_use": "Request in use",
"save_as": "Opslaan als",
@@ -142,6 +147,7 @@
"remove_team": "Weet je zeker dat je dit team wilt verwijderen?",
"remove_telemetry": "Weet u zeker dat u zich wilt afmelden voor telemetrie?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Weet u zeker dat u deze werkruimte wilt synchroniseren?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Zen-modus"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Collecties",
"confirm": "Bevestigen",
"edit_request": "Verzoek bewerken",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Kopieer link",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Duur",
"enter_curl": "Voer cURL . in",
"generate_code": "Genereer code",
@@ -396,8 +405,10 @@
"header_list": "Koplijst",
"invalid_name": "Geef een naam op voor het verzoek",
"method": "Methode",
"moved": "Request moved",
"name": "Naam aanvragen",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Reactie inhoud",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Headers",
@@ -434,6 +446,7 @@
"status": "Status",
"time": "Tijd",
"title": "Antwoord",
"video": "Video",
"waiting_for_connection": "wachten op verbinding",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Inhoud",
"collections": "Collecties",
"documentation": "Documentatie",
"environments": "Environments",
"headers": "Headers",
"history": "Geschiedenis",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Team verlaten",
"exit_disabled": "Alleen de eigenaar kan het team niet verlaten",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "E-mailindeling is ongeldig",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
@@ -675,6 +690,7 @@
"member_removed": "Gebruiker verwijderd",
"member_role_updated": "Gebruikersrollen bijgewerkt",
"members": "Leden",
"more_members": "+{count} more",
"name_length_insufficient": "Teamnaam moet minimaal 6 tekens lang zijn",
"name_updated": "Team name updated",
"new": "Nieuw team",
@@ -682,10 +698,13 @@
"new_name": "Mijn nieuwe team",
"no_access": "U heeft geen bewerkingsrechten voor deze collecties",
"no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "Rechten",
"same_target_destination": "Same target and destination",
"saved": "Team gered",
"select_a_team": "Select a team",
"title": "teams",
@@ -713,5 +732,11 @@
"message": "Bericht",
"protocols": "Protocollen",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Rediger",
"filter": "Filter",
"go_back": "Gå tilbake",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Merkelapp",
"learn_more": "Lær mer",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Samlingen er opprettet",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Rediger samling",
"invalid_name": "Oppgi et gyldig navn på samlingen",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Mine samlinger",
"name": "Min nye samling",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Ny kolleksjon",
"order_changed": "Collection Order Updated",
"renamed": "Samlingen ble omdøpt",
"request_in_use": "Request in use",
"save_as": "Lagre som",
@@ -142,6 +147,7 @@
"remove_team": "Er du sikker på at du vil slette dette laget?",
"remove_telemetry": "Er du sikker på at du vil velge bort telemetri?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Er du sikker på at du vil synkronisere dette arbeidsområdet?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Zen-modus"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Samlinger",
"confirm": "Bekrefte",
"edit_request": "Rediger forespørsel",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Kopier link",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Varighet",
"enter_curl": "Skriv inn cURL",
"generate_code": "Generer kode",
@@ -396,8 +405,10 @@
"header_list": "Toppliste",
"invalid_name": "Oppgi et navn på forespørselen",
"method": "Metode",
"moved": "Request moved",
"name": "Forespørselsnavn",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Svarkropp",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Overskrifter",
@@ -434,6 +446,7 @@
"status": "Status",
"time": "Tid",
"title": "Respons",
"video": "Video",
"waiting_for_connection": "venter på tilkobling",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Kropp",
"collections": "Samlinger",
"documentation": "Dokumentasjon",
"environments": "Environments",
"headers": "Overskrifter",
"history": "Historie",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Avslutt Team",
"exit_disabled": "Bare eieren kan ikke gå ut av teamet",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "E -postformatet er ugyldig",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
@@ -675,6 +690,7 @@
"member_removed": "Bruker fjernet",
"member_role_updated": "Brukerroller er oppdatert",
"members": "Medlemmer",
"more_members": "+{count} more",
"name_length_insufficient": "Lagnavnet bør være minst 6 tegn langt",
"name_updated": "Team name updated",
"new": "Nytt lag",
@@ -682,10 +698,13 @@
"new_name": "Mitt nye lag",
"no_access": "Du har ikke redigeringstilgang til disse samlingene",
"no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "Tillatelser",
"same_target_destination": "Same target and destination",
"saved": "Lag reddet",
"select_a_team": "Select a team",
"title": "Lag",
@@ -713,5 +732,11 @@
"message": "Beskjed",
"protocols": "Protokoller",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Edytuj",
"filter": "Filter",
"go_back": "Wróć",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Etykieta",
"learn_more": "Dowiedz się więcej",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Utworzono kolekcję",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Edytuj kolekcję",
"invalid_name": "Podaj prawidłową nazwę kolekcji",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Moje kolekcje",
"name": "Moja nowa kolekcja",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Nowa kolekcja",
"order_changed": "Collection Order Updated",
"renamed": "Zmieniono nazwę kolekcji",
"request_in_use": "Request in use",
"save_as": "Zapisz jako",
@@ -142,6 +147,7 @@
"remove_team": "Czy na pewno chcesz usunąć ten zespół?",
"remove_telemetry": "Czy na pewno chcesz zrezygnować z telemetrii?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Czy na pewno chcesz zsynchronizować ten obszar roboczy?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Tryb Zen"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Kolekcje",
"confirm": "Potwierdź",
"edit_request": "Edytuj żądanie",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Skopiuj link",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Czas trwania",
"enter_curl": "Wpisz cURL",
"generate_code": "Wygeneruj kod",
@@ -396,8 +405,10 @@
"header_list": "Lista nagłówków",
"invalid_name": "Podaj nazwę żądania",
"method": "metoda",
"moved": "Request moved",
"name": "Nazwa",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Ciało odpowiedzi",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Nagłówki",
@@ -434,6 +446,7 @@
"status": "Status",
"time": "Czas",
"title": "Odpowiedź",
"video": "Video",
"waiting_for_connection": "oczekiwanie na połączenie",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Ciało",
"collections": "Kolekcje",
"documentation": "Dokumentacja",
"environments": "Environments",
"headers": "Nagłówki",
"history": "Historia",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Opóść zespół",
"exit_disabled": "Tylko właściciel nie może opuścić zespołu",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Format e-maila jest nieprawidłowy",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
@@ -675,6 +690,7 @@
"member_removed": "Użytkownik usunięty",
"member_role_updated": "Zaktualizowano role użytkowników",
"members": "Członkowie",
"more_members": "+{count} more",
"name_length_insufficient": "Nazwa zespołu powinna mieć co najmniej 6 znaków",
"name_updated": "Team name updated",
"new": "Nowy zespół",
@@ -682,10 +698,13 @@
"new_name": "Mój nowy zespół",
"no_access": "Nie masz uprawnień do edycji tych kolekcji",
"no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "Uprawnienia",
"same_target_destination": "Same target and destination",
"saved": "Zespół zapisany",
"select_a_team": "Select a team",
"title": "Zespoły",
@@ -713,5 +732,11 @@
"message": "Wiadomość",
"protocols": "Protokoły",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Editar",
"filter": "Filter",
"go_back": "Voltar",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Etiqueta",
"learn_more": "Saber mais",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Coleção criada",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Editar coleção",
"invalid_name": "Forneça um nome válido para a coleção",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Minhas coleções",
"name": "Minha nova coleção",
"name_length_insufficient": "O nome da coleção deve ter pelo menos 3 caracteres",
"new": "Nova coleção",
"order_changed": "Collection Order Updated",
"renamed": "Coleção renomeada",
"request_in_use": "Requisição em uso",
"save_as": "Salvar como",
@@ -142,6 +147,7 @@
"remove_team": "Tem certeza que deseja excluir esta equipe?",
"remove_telemetry": "Tem certeza de que deseja cancelar a telemetria?",
"request_change": "Tem certeza que deseja descartar a requisição atual? Alterações não salvas serão perdidas.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Tem certeza de que deseja sincronizar este espaço de trabalho?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Modo zen"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Coleções",
"confirm": "confirme",
"edit_request": "Editar pedido",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Link de cópia",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Duração",
"enter_curl": "Digite cURL",
"generate_code": "Gerar código",
@@ -396,8 +405,10 @@
"header_list": "Lista de Cabeçalhos",
"invalid_name": "Forneça um nome para a requisição",
"method": "Método",
"moved": "Request moved",
"name": "Nome da requisição",
"new": "Nova requisição",
"order_changed": "Request Order Updated",
"override": "Substituir",
"override_help": "Substituir <kbd>Content-Type</kbd> em Headers",
"overriden": "Substituído",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Corpo de Resposta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Cabeçalhos",
@@ -434,6 +446,7 @@
"status": "Status",
"time": "Tempo",
"title": "Resposta",
"video": "Video",
"waiting_for_connection": "aguardando conexão",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Corpo",
"collections": "Coleções",
"documentation": "Documentação",
"environments": "Environments",
"headers": "Cabeçalhos",
"history": "História",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "O e-mail não corresponde aos detalhes da sua conta. Contate o dono da sua equipe.",
"exit": "Sair da equipe",
"exit_disabled": "Apenas o dono não pode sair da equipe",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "O formato do email é inválido",
"invalid_id": "ID de time inválido. Contate o dono da sua equipe.",
"invalid_invite_link": "Link de convite inválido",
@@ -675,6 +690,7 @@
"member_removed": "Usuário removido",
"member_role_updated": "Funções de usuário atualizadas",
"members": "Membros",
"more_members": "+{count} more",
"name_length_insufficient": "O nome da equipe deve ter pelo menos 6 caracteres",
"name_updated": "Nome do time atualizado",
"new": "Novo time",
@@ -682,10 +698,13 @@
"new_name": "Minha Nova Equipe",
"no_access": "Você não tem acesso de edição a essas coleções",
"no_invite_found": "Convite não encontrado. Contate o dono da sua equipe.",
"no_request_found": "Request not found.",
"not_found": "Equipe não encontrada. Contate o dono da sua equipe",
"not_valid_viewer": "Você não é um visualizado válido. Contate o dono da sua equipe.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Convites pendentes",
"permissions": "Permissões",
"same_target_destination": "Same target and destination",
"saved": "Equipe salva",
"select_a_team": "Selecione uma equipe",
"title": "Equipes",
@@ -713,5 +732,11 @@
"message": "Mensagem",
"protocols": "Protocolos",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -19,6 +19,7 @@
"edit": "Editar",
"filter": "Filter",
"go_back": "Volte",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Etiqueta",
"learn_more": "Saber mais",
@@ -117,12 +118,16 @@
},
"collection": {
"created": "Coleção criada",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Editar coleção",
"invalid_name": "Forneça um nome válido para a coleção",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Minhas coleções",
"name": "Minha nova coleção",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Nova coleção",
"order_changed": "Collection Order Updated",
"renamed": "Coleção renomeada",
"request_in_use": "Request in use",
"save_as": "Salvar como",
@@ -142,6 +147,7 @@
"remove_team": "Tem certeza que deseja excluir esta equipe?",
"remove_telemetry": "Tem certeza de que deseja cancelar a telemetria?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Tem certeza de que deseja sincronizar este espaço de trabalho?"
},
"count": {
@@ -310,6 +316,7 @@
"zen_mode": "Modo zen"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "Coleções",
"confirm": "confirme",
"edit_request": "Editar pedido",
@@ -389,6 +396,8 @@
"text": "Text"
},
"copy_link": "Link de cópia",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Duração",
"enter_curl": "Digite cURL",
"generate_code": "Gerar código",
@@ -396,8 +405,10 @@
"header_list": "Lista de Cabeçalhos",
"invalid_name": "Forneça um nome para o pedido",
"method": "Método",
"moved": "Request moved",
"name": "Nome do pedido",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
@@ -421,6 +432,7 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Corpo de Resposta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Cabeçalhos",
@@ -434,6 +446,7 @@
"status": "Status",
"time": "Tempo",
"title": "Resposta",
"video": "Video",
"waiting_for_connection": "aguardando conexão",
"xml": "XML"
},
@@ -628,6 +641,7 @@
"body": "Corpo",
"collections": "Coleções",
"documentation": "Documentação",
"environments": "Environments",
"headers": "Cabeçalhos",
"history": "História",
"mqtt": "MQTT",
@@ -652,6 +666,7 @@
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Sair da equipe",
"exit_disabled": "Apenas o dono não pode sair da equipe",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "O formato do email é inválido",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_invite_link": "Invalid invite link",
@@ -675,6 +690,7 @@
"member_removed": "Usuário removido",
"member_role_updated": "Funções de usuário atualizadas",
"members": "Membros",
"more_members": "+{count} more",
"name_length_insufficient": "O nome da equipe deve ter pelo menos 6 caracteres",
"name_updated": "Team name updated",
"new": "Novo time",
@@ -682,10 +698,13 @@
"new_name": "Minha Nova Equipe",
"no_access": "Você não tem acesso de edição a essas coleções",
"no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "Permissões",
"same_target_destination": "Same target and destination",
"saved": "Equipe salva",
"select_a_team": "Select a team",
"title": "Times",
@@ -713,5 +732,11 @@
"message": "Mensagem",
"protocols": "Protocolos",
"url": "URL"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

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