Compare commits

...

130 Commits

Author SHA1 Message Date
Andrew Bastin
6086ebd824 Merge pull request #2575 from codeday-labs/codeday/main 2022-10-29 18:02:46 -04:00
Jason Jock Nava Casareno
dd83f8ef24 Merge branch 'hoppscotch:main' into codeday/main 2022-08-22 12:54:54 -07:00
Jason Casareno
682200ce68 Rename file and undid unnecessary changes 2022-08-22 12:54:12 -07:00
Nivedin
052595c076 fix: form data with same key send only last one (#2606)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-08-22 23:02:02 +05:30
Jason Casareno
18910e429c Added missing property to request 2022-08-15 16:59:16 -07:00
Jason Casareno
924d6a87d0 Made active parameter counter include existing 'my variables' 2022-08-12 15:49:42 -07:00
Jason Jock Nava Casareno
fc15a5a1e4 Merge branch 'hoppscotch:main' into codeday/main 2022-08-12 15:45:10 -07:00
Jason Casareno
477811c414 Removed console.log messages 2022-08-12 15:03:54 -07:00
Andrew Bastin
6b8ae63747 fix: wrong pick emission on save request modal for teams requests (fixes #2579) 2022-08-12 14:00:47 +05:30
Andrew Bastin
c013aa52ac feat: allow quoted key/values for escaping characters and trail/lead whitespaces in raw key value pairs (#2578) 2022-08-12 13:53:40 +05:30
Jason Casareno
631a16feb0 Added warning msg when variables detect infinite expansion (WIP) 2022-08-10 18:01:34 -07:00
Jason Casareno
d0f4080771 Bug Fix: Environment modal not displaying expand error warning message 2022-08-10 14:48:10 -07:00
Jason Casareno
0da75cb23d Sync with Main Repository 2022-08-10 10:29:05 -07:00
Anwarul Islam
017cbb5a71 feat: update keyboard shortcut to navigate to profile page (#2573) 2022-08-10 17:21:43 +05:30
Sagar
2e1ca0cbb0 feat: remember pane sizes (#2556)
Co-authored-by: Sagar <sagar@Sagars-MacBook-Pro.local>
2022-08-10 05:11:03 +05:30
Jason Jock Nava Casareno
abba09ea80 Merge branch 'hoppscotch:main' into codeday/main 2022-08-08 17:28:25 -07:00
Anwarul Islam
a9e1a3002e Hightlight environment variable with a dash '-' in its name (#2560) 2022-08-08 17:23:16 +05:30
Jason Jock Nava Casareno
fb5967294b Merge branch 'hoppscotch:main' into codeday/main 2022-08-05 15:02:01 -07:00
Deepanshu Dhruw
73fdfbd2c8 feat: added delay flag in @hoppscotch/cli and related tests (#2527)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-08-04 19:19:14 +05:30
Patrick Prakash
0c31d9201f docs: fix PWA broken link (#2558) 2022-08-04 18:41:19 +05:30
Jason Casareno
21d8b8fb2e Added TODO Comments + File Deletion 2022-08-03 17:04:13 -07:00
isaiM6
7ce85fee81 Merge branch 'codeday/main' of https://github.com/codeday-labs/hoppscotch into codeday/main 2022-08-03 16:50:36 -07:00
isaiM6
10615ca1a1 merge commit 2022-08-03 16:49:31 -07:00
Adrian Tuschek
dd5c876e32 Merging my branch into codeday/main 2022-08-03 16:47:52 -07:00
isaiM6
c2002f0f27 fixed merge conflict 2022-08-03 16:37:33 -07:00
isaiM6
9cfba797f6 fixed return statement 2022-08-03 16:23:52 -07:00
isaiM6
d1e6ffda49 fixed return statement 2022-08-03 16:21:33 -07:00
Jason Casareno
4f71b163ea Minor changes 2022-08-03 16:18:15 -07:00
isaiM6
775bf9a9c3 Merged environment.ts and variables.ts 2022-08-03 16:01:19 -07:00
Jason Casareno
33ecea5d75 Separate query parameters and variables vue files 2022-08-03 14:54:10 -07:00
Jason Casareno
551dfd1e20 Re-added the draggable button component to the variables UI component 2022-08-01 18:02:32 -07:00
Jason Casareno
8663934075 Removed unecessary code 2022-08-01 17:18:56 -07:00
Jason Casareno
a73d64ddc1 Renamed 'parameter' into 'variable' for the variable UI component 2022-08-01 17:03:38 -07:00
isaiM6
99f119d262 simplified conditional statement 2022-08-01 16:24:46 -07:00
isaiM6
6a8a687616 merge 2022-08-01 16:21:38 -07:00
Adrian Tuschek
6a33083790 Fixed recursive variables bug 2022-08-01 16:12:35 -07:00
isaiM6
5c7c355d95 modified pane size 2022-08-01 16:09:35 -07:00
isaiM6
328fb1176d changed layout of parameters so the pane with the params is more visible 2022-08-01 15:57:43 -07:00
isaiM6
fdf0c95f9a merged main into my branch 2022-08-01 15:04:46 -07:00
Jason Casareno
9e90e703f7 Deleted unnecessary class 2022-08-01 15:03:12 -07:00
Jason Casareno
0a241663ac Deleted unnecessary class 2022-08-01 15:02:22 -07:00
isaiM6
d58fc42190 fixed compilation errors in my branch 2022-08-01 14:57:12 -07:00
Jason Casareno
7a9bcd0a5c Modified regex expression 2022-08-01 14:48:58 -07:00
Jason Casareno
e3482f66cc Merge codeday/jason => codeday/isai 2022-08-01 14:47:22 -07:00
Jason Casareno
186803a465 Merge codeday/jason => codeday/isai 2022-08-01 14:46:06 -07:00
isaiM6
bdfdb44743 forced commit 2022-08-01 14:42:48 -07:00
Jason Casareno
4f8b346024 Deleted unwanted file 2022-08-01 14:36:17 -07:00
Jason Casareno
ec1104396e Merge codeday/jason => codeday/adrian 2022-08-01 14:34:56 -07:00
isaiM6
630ab1f4f4 merge commit 2022-08-01 14:11:37 -07:00
isaiM6
cabc775f58 commit for merge 2022-08-01 14:08:29 -07:00
Jason Casareno
f515ac4f52 Renaming variables and parameters 2022-08-01 13:15:01 -07:00
Jason Casareno
11b8bb4571 Merge codeday/jason => codeday/main 2022-08-01 13:08:42 -07:00
Jason Casareno
7bf66d8339 Renamed file and refactored, added new TODO 2022-08-01 13:07:27 -07:00
Jason Casareno
8d81ff3dc2 Merge codeday/jason => codeday/main 2022-08-01 12:09:10 -07:00
Jason Casareno
5538b9a5b9 Bug fix, removed console.logs 2022-08-01 12:08:19 -07:00
Jason Casareno
7077fe4621 Merge codeday/jason => codeday/main 2022-08-01 11:46:20 -07:00
Jason Casareno
42144b724b Modified regex expression to pit path variables match cases 2022-08-01 11:44:24 -07:00
Jason Casareno
14183d8b91 Debugging effective final url (WIP) 2022-07-29 18:07:06 -07:00
Adrian Tuschek
f8e1d78824 Debugging effective fial url (Work in Progress) 2022-07-29 18:03:43 -07:00
Jason Casareno
1e8805ab4f Debugging effective final url (WIP) 2022-07-29 18:03:37 -07:00
isaiM6
a294a2804b added files to start parsing functionality of path variables 2022-07-29 18:01:24 -07:00
Adrian Tuschek
8507278c40 Debug branch merge 2022-07-29 16:20:07 -07:00
Jason Casareno
d22bae2c60 Fixing final endpoint url (WIP) 2022-07-29 16:06:20 -07:00
Adrian Tuschek
2fefa55dce Merge Jasons branch 2022-07-29 14:45:42 -07:00
isaiM6
e0787d7fca commiting change with default variables 2022-07-28 17:27:35 -07:00
Jason Casareno
c9c5df36ab Passing variables into input bar (WIP) 2022-07-28 17:24:27 -07:00
isaiM6
5768274ef1 forced commit 2022-07-28 16:01:22 -07:00
isaiM6
493594b5d7 force commit 2022-07-28 15:58:37 -07:00
Adrian Tuschek
56c96f952d Merging changes 2022-07-28 15:56:10 -07:00
Adrian Tuschek
2c06a66c0a Hoppscotch Update 2022-07-28 15:48:41 -07:00
Jason Jock Nava Casareno
1a26a0e986 Merge branch 'hoppscotch:main' into codeday/main 2022-07-28 15:42:32 -07:00
Jason Jock Nava Casareno
9f1ee724b4 Merge branch 'hoppscotch:main' into codeday/jason 2022-07-28 15:42:18 -07:00
Jason Jock Nava Casareno
d28679de15 Merge branch 'hoppscotch:main' into codeday/isai 2022-07-28 15:42:11 -07:00
kyteinsky
fa0e7f4785 fix: curl parser x-www-form-urlencoded body parsing (#2528) 2022-07-28 21:03:05 +05:30
Andrew Bastin
e9576dd339 fix: ignore confirm save modal on same request selection even when no session 2022-07-28 17:41:54 +05:30
Jason Casareno
c8f62c4f04 Created default value for HoppRESTRequest for testing 2022-07-27 17:48:29 -07:00
Jason Casareno
8aa066e2ab Created default value for HoppRESTRequest for testing 2022-07-27 17:47:37 -07:00
isaiM6
a38e6cd427 Merge branch 'hoppscotch:main' into codeday/isai 2022-07-25 14:19:20 -07:00
isaiM6
c3ba45f875 changes to variables.vue 2022-07-25 13:56:15 -07:00
isaiM6
9061511609 changes to variables.vue 2022-07-25 13:55:14 -07:00
Jason Jock Nava Casareno
443e095775 Merge branch 'hoppscotch:main' into codeday/main 2022-07-25 11:06:49 -07:00
Jason Jock Nava Casareno
09e6fb246a Merge branch 'hoppscotch:main' into codeday/jason 2022-07-25 10:27:36 -07:00
Khusroo Hayat
d335ac1d80 fix: search panel position in response (#2510)
Co-authored-by: liyasthomas <liyascthomas@gmail.com>
2022-07-25 14:28:02 +05:30
Joel Jacob Stephen
c0e3a2be0b fix: disabled search in team collection (#2523)
Co-authored-by: liyasthomas <liyascthomas@gmail.com>
2022-07-25 14:01:26 +05:30
SiderealArt
722864da62 update tw.json (#2511) 2022-07-25 13:37:52 +05:30
Jason Casareno
5413bc584a Added missing dispatcher and function 2022-07-22 12:37:47 -07:00
Jason Casareno
7006fa57e2 Small naming changes 2022-07-21 20:58:53 -07:00
isaiM6
1a629a1219 localy stored variable data 2022-07-21 17:25:25 -07:00
Jason Casareno
9b60dc5f2d Modified HoppRESTRequest data structure 2022-07-21 14:21:45 -07:00
Jason Casareno
21021a3cd9 Removed reference to 'bulk params' 2022-07-20 16:50:34 -07:00
Jason Casareno
fd5db6c8c9 Duplicated and disconnected parameter UI for reuse 2022-07-19 15:56:42 -07:00
Akash K
54a12ef6fa fix: team collections tab visible when logging out (#2494) 2022-07-06 22:24:32 +05:30
liyasthomas
d035262e1a refactor: lowercase routes 2022-07-03 18:04:57 +05:30
Andrew Bastin
1ab54b0ce7 fix: i18n breaking on switching between realtime tabs 2022-07-02 21:39:43 +05:30
Andrew Bastin
cac3abd2ab fix: multiple requests appearing on teams (#2455)
Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com>
2022-06-30 18:37:27 +05:30
Nivedin
c34185dc4b fix: environment variables save without pressing 'save' button (#2454)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-06-30 14:42:43 +05:30
Andrew Bastin
cfdab014c7 refactor: allow smart tabs to render inactive tabs as an option 2022-06-28 16:37:43 +05:30
liyasthomas
ed6e1c0f94 chore: update package lock file 2022-06-28 15:19:45 +05:30
Anwarul Islam
07a8a37739 feat: realtime tabs as subpages (#2450)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-06-28 15:12:47 +05:30
Akash K
ca553b9d3c fix: empty string exported when exporting team collections (#2460) 2022-06-27 22:24:38 +05:30
Jitendra Nirnejak
69aaeaf42a fix: environment variables overflowing issue on test results - fix (#2473) 2022-06-27 15:52:32 +05:30
Andrew Bastin
015393d98f fix: allow volar to function properly on gitpod 2022-06-23 14:56:09 +05:30
Nivedin
c8dec56b96 fix: remove confirm change popup and add ability to overwrite request in saveas popup (#2433) 2022-06-22 15:21:35 +05:30
liyasthomas
8fefd37862 fix: make scrollbars visible, fixed #2437, #2411 2022-06-18 18:14:02 +05:30
kyteinsky
c1cc1ce295 fix: curl parser json detection issues (#2370)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-06-18 14:09:16 +05:30
Chun-Hao Lien
16be7c38f3 fix: typo in getEnvironment (#2434) 2022-06-18 07:37:32 +05:30
Vaugen Wake
82b6ad935a fix: resolve removing body parameters in requests (#2390) (#2428)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-06-16 19:47:46 +05:30
Nivedin
185dc3f2c9 fix: Copy and download filtered response body (#2426) 2022-06-16 06:09:43 +05:30
liyasthomas
51138fa42d chore: updated i18n translations 2022-06-16 06:02:57 +05:30
Andrew Bastin
7f08a4bd81 chore: hoppscotch-cli version 0.2.1 2022-06-15 23:58:34 +05:30
Deepanshu Dhruw
0244b941b3 feat: added support for passing env.json file to test cmd (#2373) 2022-06-15 23:53:24 +05:30
Moritz Mock
2d0bd48e00 feat: checks only if not default param (#2410)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-06-14 22:21:03 +05:30
Andrew Bastin
15e433b114 fix: broken volar inference on templates 2022-06-14 17:29:31 +05:30
nicognaW
97ff089110 feat: display response size with a bigger unit in tooltip (#2425)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-06-14 15:40:33 +05:30
Andrew Bastin
a6b5295df5 chore: revert indented line wrap due to issues with mobile (fixes hoppscotch/internal-issues#9) 2022-06-13 23:05:23 +05:30
Nivedin
6b1ca1dce1 feat: filter json body response (#2404) 2022-06-10 18:12:40 +05:30
Andrew Bastin
04a9c4dc52 fix: improve graphql syntax highlighting 2022-06-10 01:26:42 +05:30
liyasthomas
e5e44b889f ci: use pnpm/action-setup in actions 2022-06-08 12:34:03 +05:30
Andrew Bastin
c46bc40bcb chore: add staging backend env vars to nuxt config 2022-06-08 12:21:17 +05:30
Andrew Bastin
a91a8ba575 chore: use pnpm/action-setup instead of manual pnpm install 2022-06-08 12:19:47 +05:30
Andrew Bastin
1f536eeedd fix: volar complaining about jsx 2022-06-07 15:58:14 +05:30
liyasthomas
25253c4bdf chore: improve ui consistency in realtime pages 2022-06-07 14:34:00 +05:30
Andrew Bastin
043c49541f chore: add backend urls to staging env 2022-06-07 02:11:40 +05:30
Andrew Bastin
a78462fbe3 chore: move backend urls to env variables 2022-06-07 01:57:48 +05:30
Andrew Bastin
52c25e497f chore: introduce staging deploy workflow 2022-06-07 01:03:29 +05:30
Andrew Bastin
4f539c9781 fix: remove staging push from deploy-netlify workflow 2022-06-07 01:00:45 +05:30
Andrew Bastin
ba468bb835 chore: allow system env vars to be available during build time 2022-06-07 00:59:10 +05:30
liyasthomas
93faa8d5ff ci: activate staging deployment 2022-06-03 15:32:58 +05:30
Liyas Thomas
cf90d16f8a refactor: use refAutoReset instead of settimeout (#2385)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-06-01 16:54:37 +05:30
Nivedin
39f72f8458 feat: segmented content-type dropdown UI (#2382)
Co-authored-by: liyasthomas <liyascthomas@gmail.com>
2022-05-31 17:04:05 +05:30
123 changed files with 3885 additions and 1214 deletions

View File

@@ -12,11 +12,11 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Install pnpm
run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6
- name: Install Dependencies
run: pnpm install
- name: Setup and run pnpm install
uses: pnpm/action-setup@v2.2.2
with:
version: 7
run_install: true
- name: Setup Environment
run: mv packages/hoppscotch-app/.env.example packages/hoppscotch-app/.env
@@ -24,11 +24,11 @@ jobs:
- name: Build Site
run: pnpm run generate
# Deploy the site with netlify-cli
- name: Deploy to Netlify
# Deploy the production site with netlify-cli
- name: Deploy to Netlify (production)
uses: netlify/actions/cli@master
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_PRODUCTION_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
with:
args: deploy --dir=packages/hoppscotch-app/dist --prod

View File

@@ -0,0 +1,45 @@
name: Deploy to Staging Netlify
on:
push:
# TODO: Migrate to staging branch only
branches: [main]
jobs:
build:
name: Push build files to Netlify
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup and run pnpm install
uses: pnpm/action-setup@v2.2.2
with:
version: 7
run_install: true
- name: Build Site
env:
GA_ID: ${{ secrets.STAGING_GA_ID }}
GTM_ID: ${{ secrets.STAGING_GTM_ID }}
API_KEY: ${{ secrets.STAGING_FB_API_KEY }}
AUTH_DOMAIN: ${{ secrets.STAGING_FB_AUTH_DOMAIN }}
DATABASE_URL: ${{ secrets.STAGING_FB_DATABASE_URL }}
PROJECT_ID: ${{ secrets.STAGING_FB_PROJECT_ID }}
STORAGE_BUCKET: ${{ secrets.STAGING_FB_STORAGE_BUCKET }}
MESSAGING_SENDER_ID: ${{ secrets.STAGING_FB_MESSAGING_SENDER_ID }}
APP_ID: ${{ secrets.STAGING_FB_APP_ID }}
BASE_URL: ${{ secrets.STAGING_BASE_URL }}
BACKEND_GQL_URL: ${{ secrets.STAGING_BACKEND_GQL_URL }}
BACKEND_WS_URL: ${{ secrets.STAGING_BACKEND_WS_URL }}
run: pnpm run generate
# Deploy the staging site with netlify-cli
- name: Deploy to Netlify (staging)
uses: netlify/actions/cli@master
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_STAGING_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
with:
args: deploy --dir=packages/hoppscotch-app/dist --prod

View File

@@ -17,12 +17,15 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Install pnpm
run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6
- name: Setup and run pnpm install
uses: pnpm/action-setup@v2.2.2
with:
version: 7
run_install: true
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Run tests
run: pnpm i && pnpm -r test
run: pnpm test

View File

@@ -84,7 +84,7 @@
_Customized themes are synced with cloud / local session_
🔥 **PWA:** Install as a [PWA](https://developers.google.com/web/progressive-web-apps) on your device.
🔥 **PWA:** Install as a [PWA](https://web.dev/what-are-pwas/) on your device.
- Instant loading with Service Workers
- Offline support

View File

@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/codemirror-lang-graphql",
"version": "0.1.0",
"version": "0.2.0",
"description": "GraphQL language support for CodeMirror",
"author": "Hoppscotch (support@hoppscotch.io)",
"license": "MIT",

View File

@@ -27,16 +27,22 @@ export const GQLLanguage = LRLanguage.define({
},
}),
styleTags({
Name: t.definition(t.variableName),
"OperationDefinition/Name": t.definition(t.function(t.variableName)),
OperationType: t.keyword,
BooleanValue: t.bool,
StringValue: t.string,
IntValue: t.number,
FloatValue: t.number,
NullValue: t.null,
ObjectValue: t.brace,
Comment: t.lineComment,
Name: t.propertyName,
StringValue: t.string,
IntValue: t.integer,
FloatValue: t.float,
NullValue: t.null,
BooleanValue: t.bool,
Comma: t.separator,
"OperationDefinition/Name": t.definition(t.function(t.variableName)),
"OperationType TypeKeyword SchemaKeyword FragmentKeyword OnKeyword DirectiveKeyword RepeatableKeyword SchemaKeyword ExtendKeyword ScalarKeyword InterfaceKeyword UnionKeyword EnumKeyword InputKeyword ImplementsKeyword": t.keyword,
"ExecutableDirectiveLocation TypeSystemDirectiveLocation": t.atom,
"DirectiveName!": t.annotation,
"\"{\" \"}\"": t.brace,
"\"(\" \")\"": t.paren,
"\"[\" \"]\"": t.squareBracket,
"Type! NamedType": t.typeName,
}),
],
}),

View File

@@ -33,16 +33,24 @@ TypeSystemExtension {
TypeExtension
}
SchemaKeyword {
@specialize<Name, "schema">
}
SchemaDefinition {
Description? @specialize<Name, "schema"> Directives? RootTypeDef
Description? SchemaKeyword Directives? RootTypeDef
}
RootTypeDef {
"{" RootOperationTypeDefinition+ "}"
}
ExtendKeyword {
@specialize<Name, "extend">
}
SchemaExtension {
@specialize<Name, "extend"> @specialize<Name, "schema"> Directives? RootTypeDef
ExtendKeyword SchemaKeyword Directives? RootTypeDef
}
TypeExtension {
@@ -54,33 +62,53 @@ TypeExtension {
InputObjectTypeExtension
}
ScalarKeyword {
@specialize<Name, "scalar">
}
ScalarTypeExtension {
@specialize<Name, "extend"> @specialize<Name, "scalar"> Name Directives
ExtendKeyword ScalarKeyword Name Directives
}
ObjectTypeExtension /* precedence: right 0 */ {
@specialize<Name, "extend"> @specialize<Name, "type"> Name ImplementsInterfaces? Directives? !typeDef FieldsDefinition |
@specialize<Name, "extend"> @specialize<Name, "type"> Name ImplementsInterfaces? Directives?
ExtendKeyword TypeKeyword Name ImplementsInterfaces? Directives? !typeDef FieldsDefinition |
ExtendKeyword TypeKeyword Name ImplementsInterfaces? Directives?
}
InterfaceKeyword {
@specialize<Name, "interface">
}
InterfaceTypeExtension /* precedence: right 0 */ {
@specialize<Name, "extend"> @specialize<Name, "interface"> Name ImplementsInterfaces? Directives? FieldsDefinition |
@specialize<Name, "extend"> @specialize<Name, "interface"> Name ImplementsInterfaces? Directives?
ExtendKeyword InterfaceKeyword Name ImplementsInterfaces? Directives? FieldsDefinition |
ExtendKeyword InterfaceKeyword Name ImplementsInterfaces? Directives?
}
UnionKeyword {
@specialize<Name, "union">
}
UnionTypeExtension /* precedence: right 0 */ {
@specialize<Name, "extend"> @specialize<Name, "union"> Name Directives? UnionMemberTypes |
@specialize<Name, "extend"> @specialize<Name, "union"> Name Directives?
ExtendKeyword UnionKeyword Name Directives? UnionMemberTypes |
ExtendKeyword UnionKeyword Name Directives?
}
EnumKeyword {
@specialize<Name, "enum">
}
EnumTypeExtension /* precedence: right 0 */ {
@specialize<Name, "extend"> @specialize<Name, "enum"> Name Directives? !typeDef EnumValuesDefinition |
@specialize<Name, "extend"> @specialize<Name, "enum"> Name Directives?
ExtendKeyword EnumKeyword Name Directives? !typeDef EnumValuesDefinition |
ExtendKeyword EnumKeyword Name Directives?
}
InputKeyword {
@specialize<Name, "input">
}
InputObjectTypeExtension /* precedence: right 0 */ {
@specialize<Name, "extend"> @specialize<Name, "input"> Name Directives? InputFieldsDefinition+ |
@specialize<Name, "extend"> @specialize<Name, "input"> Name Directives?
ExtendKeyword InputKeyword Name Directives? InputFieldsDefinition+ |
ExtendKeyword InputKeyword Name Directives?
}
InputFieldsDefinition {
@@ -95,9 +123,13 @@ EnumValueDefinition {
Description? EnumValue Directives?
}
ImplementsKeyword {
@specialize<Name, "implements">
}
ImplementsInterfaces {
ImplementsInterfaces "&" NamedType |
@specialize<Name, "implements"> "&"? NamedType
ImplementsKeyword "&"? NamedType
}
FieldsDefinition {
@@ -144,27 +176,31 @@ TypeDefinition {
}
ScalarTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "scalar"> Name Directives?
Description? ScalarKeyword Name Directives?
}
TypeKeyword {
@specialize<Name, "type">
}
ObjectTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "type"> Name ImplementsInterfaces? Directives? FieldsDefinition?
Description? TypeKeyword Name ImplementsInterfaces? Directives? FieldsDefinition?
}
InterfaceTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "interface"> Name ImplementsInterfaces? Directives? FieldsDefinition?
Description? InterfaceKeyword Name ImplementsInterfaces? Directives? FieldsDefinition?
}
UnionTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "union"> Name Directives? UnionMemberTypes?
Description? UnionKeyword Name Directives? UnionMemberTypes?
}
EnumTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "enum"> Name Directives? !typeDef EnumValuesDefinition?
Description? EnumKeyword Name Directives? !typeDef EnumValuesDefinition?
}
InputObjectTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "input"> Name Directives? !typeDef InputFieldsDefinition?
Description? InputKeyword Name Directives? !typeDef InputFieldsDefinition?
}
VariableDefinitions {
@@ -237,8 +273,12 @@ FragmentSpread {
"..." FragmentName Directives?
}
FragmentKeyword {
@specialize<Name, "fragment">
}
FragmentDefinition {
@specialize<Name, "fragment"> FragmentName TypeCondition Directives? SelectionSet
FragmentKeyword FragmentName TypeCondition Directives? SelectionSet
}
FragmentName {
@@ -249,20 +289,36 @@ InlineFragment {
"..." TypeCondition? Directives? SelectionSet
}
OnKeyword {
@specialize<Name, "on">
}
TypeCondition {
@specialize<Name, "on"> NamedType
OnKeyword NamedType
}
Directives {
Directive+
}
DirectiveName {
"@" Name
}
Directive {
"@" Name Arguments?
DirectiveName Arguments?
}
DirectiveKeyword {
@specialize<Name, "directive">
}
RepeatableKeyword {
@specialize<Name, "repeatable">
}
DirectiveDefinition /* precedence: right 1 */ {
Description? @specialize<Name, "directive"> "@" Name ArgumentsDefinition? @specialize<Name, "repeatable"> ? @specialize<Name, "on"> DirectiveLocations
Description? DirectiveKeyword "@" Name ArgumentsDefinition? RepeatableKeyword ? OnKeyword DirectiveLocations
}
DirectiveLocations {
@@ -338,17 +394,14 @@ TypeSystemDirectiveLocation {
| @specialize<Name, "INPUT_FIELD_DEFINITION">
}
@skip { whitespace | Comment }
@tokens {
whitespace {
std.whitespace+
}
StringValue {
"\"\"\"" (!["] | "\\n" | "\"" "\""? !["])* "\"\"\"" | "\"" !["\\\n]* "\""
}
IntValue {
"-"? "0"
| "-"? std.digit+
@@ -363,14 +416,19 @@ TypeSystemDirectiveLocation {
Name {
$[_A-Za-z] $[_0-9A-Za-z]*
}
Comment {
"#" ![\n]*
}
Comma {
","
}
"{" "}" "[" "]"
Comment {
"#" ![\n]*
}
"{" "}"
}
@skip { whitespace | Comment }
@detectDelim

View File

@@ -16,3 +16,7 @@ MEASUREMENT_ID=G-BBJ3R80PJT
# Base URL
BASE_URL=https://hoppscotch.io
# Backend URLs
BACKEND_GQL_URL=https://api.hoppscotch.io/graphql
BACKEND_WS_URL=wss://api.hoppscotch.io/graphql

View File

@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@@ -15,6 +15,7 @@
::-webkit-scrollbar-track {
@apply bg-transparent;
@apply border-solid border-l border-t-0 border-b-0 border-r-0 border-dividerLight;
}
::-webkit-scrollbar-thumb {
@@ -27,17 +28,17 @@
::-webkit-scrollbar {
@apply w-4;
@apply h-4;
@apply h-0;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
// .hide-scrollbar {
// -ms-overflow-style: none;
// scrollbar-width: none;
// }
.hide-scrollbar::-webkit-scrollbar {
@apply hidden;
}
// .hide-scrollbar::-webkit-scrollbar {
// @apply hidden;
// }
input::placeholder,
textarea::placeholder,

View File

@@ -255,6 +255,7 @@
--upper-mobile-raw-tertiary-sticky-fold: 8.188rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5rem;
--lower-tertiary-sticky-fold: 7.05rem;
--sidebar-primary-sticky-fold: 2rem;
}
@@ -270,6 +271,7 @@
--upper-mobile-raw-tertiary-sticky-fold: 8.938rem;
--lower-primary-sticky-fold: 3.25rem;
--lower-secondary-sticky-fold: 5.5rem;
--lower-tertiary-sticky-fold: 7.8rem;
--sidebar-primary-sticky-fold: 2.25rem;
}
@@ -285,6 +287,7 @@
--upper-mobile-raw-tertiary-sticky-fold: 9.688rem;
--lower-primary-sticky-fold: 3.5rem;
--lower-secondary-sticky-fold: 6rem;
--lower-tertiary-sticky-fold: 8.55rem;
--sidebar-primary-sticky-fold: 2.5rem;
}

View File

@@ -22,7 +22,7 @@
</template>
<script setup lang="ts">
import { ref } from "@nuxtjs/composition-api"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import {
useI18n,
@@ -45,7 +45,7 @@ const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const copyIcon = ref("copy")
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
// Copy user auth token to clipboard
const copyUserAuthToken = () => {
@@ -53,7 +53,6 @@ const copyUserAuthToken = () => {
copyToClipboard(userAuthToken.value)
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
} else {
toast.error(`${t("error.something_went_wrong")}`)
}

View File

@@ -1,7 +1,7 @@
<template>
<div>
<header
class="flex items-center justify-between flex-1 px-2 py-2 space-x-2"
class="flex items-center justify-between flex-1 px-2 py-2 space-x-2 overflow-x-auto"
>
<div class="inline-flex items-center space-x-2">
<ButtonSecondary

View File

@@ -6,21 +6,26 @@
'!flex-row-reverse': SIDEBAR_ON_LEFT && mdAndLarger,
}"
:horizontal="!mdAndLarger"
@resize="setPaneEvent($event, 'vertical')"
>
<Pane
size="75"
:size="PANE_MAIN_SIZE"
min-size="65"
class="hide-scrollbar !overflow-auto flex flex-col"
>
<Splitpanes class="smart-splitter" :horizontal="COLUMN_LAYOUT">
<Splitpanes
class="smart-splitter"
:horizontal="COLUMN_LAYOUT"
@resize="setPaneEvent($event, 'horizontal')"
>
<Pane
:size="COLUMN_LAYOUT ? 45 : 50"
:size="PANE_MAIN_TOP_SIZE"
class="hide-scrollbar !overflow-auto flex flex-col"
>
<slot name="primary" />
</Pane>
<Pane
:size="COLUMN_LAYOUT ? 65 : 50"
:size="PANE_MAIN_BOTTOM_SIZE"
class="flex flex-col hide-scrollbar !overflow-auto"
>
<slot name="secondary" />
@@ -29,7 +34,7 @@
</Pane>
<Pane
v-if="SIDEBAR && hasSidebar"
size="25"
:size="PANE_SIDEBAR_SIZE"
min-size="20"
class="hide-scrollbar !overflow-auto flex flex-col"
>
@@ -42,8 +47,9 @@
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { computed, useSlots } from "@nuxtjs/composition-api"
import { computed, useSlots, ref } from "@nuxtjs/composition-api"
import { useSetting } from "~/newstore/settings"
import { setLocalConfig, getLocalConfig } from "~/newstore/localpersistence"
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
@@ -57,4 +63,60 @@ const SIDEBAR = useSetting("SIDEBAR")
const slots = useSlots()
const hasSidebar = computed(() => !!slots.sidebar)
const props = defineProps({
layoutId: {
type: String,
default: null,
},
})
type PaneEvent = {
max: number
min: number
size: number
}
const PANE_SIDEBAR_SIZE = ref(25)
const PANE_MAIN_SIZE = ref(75)
const PANE_MAIN_TOP_SIZE = ref(45)
const PANE_MAIN_BOTTOM_SIZE = ref(65)
if (!COLUMN_LAYOUT.value) {
PANE_MAIN_TOP_SIZE.value = 50
PANE_MAIN_BOTTOM_SIZE.value = 50
}
function setPaneEvent(event: PaneEvent[], type: "vertical" | "horizontal") {
if (!props.layoutId) return
const storageKey = `${props.layoutId}-pane-config-${type}`
setLocalConfig(storageKey, JSON.stringify(event))
}
function populatePaneEvent() {
if (!props.layoutId) return
const verticalPaneData = getPaneData("vertical")
if (verticalPaneData) {
const [mainPane, sidebarPane] = verticalPaneData
PANE_MAIN_SIZE.value = mainPane?.size
PANE_SIDEBAR_SIZE.value = sidebarPane?.size
}
const horizontalPaneData = getPaneData("horizontal")
if (horizontalPaneData) {
const [mainTopPane, mainBottomPane] = horizontalPaneData
PANE_MAIN_TOP_SIZE.value = mainTopPane?.size
PANE_MAIN_BOTTOM_SIZE.value = mainBottomPane?.size
}
}
function getPaneData(type: "vertical" | "horizontal"): PaneEvent[] | null {
const storageKey = `${props.layoutId}-pane-config-${type}`
const paneEvent = getLocalConfig(storageKey)
if (!paneEvent) return null
return JSON.parse(paneEvent)
}
populatePaneEvent()
</script>

View File

@@ -36,7 +36,7 @@
</template>
<script setup lang="ts">
import { ref } from "@nuxtjs/composition-api"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n, useToast } from "~/helpers/utils/composables"
@@ -60,7 +60,8 @@ const subject = "Checkout Hoppscotch - an open source API development ecosystem"
const summary = `Hi there!%0D%0A%0D%0AI thought you'll like this new platform that I joined called Hoppscotch - https://hoppscotch.io.%0D%0AIt is a simple and intuitive interface for creating and managing your APIs. You can build, test, document, and share your APIs.%0D%0A%0D%0AThe best part about Hoppscotch is that it is open source and free to get started.%0D%0A%0D%0A`
const twitter = "hoppscotch_io"
const copyIcon = ref("copy")
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
const platforms = [
{
name: "Email",
@@ -93,7 +94,6 @@ const copyAppLink = () => {
copyToClipboard(url)
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
const hideModal = () => {

View File

@@ -7,6 +7,7 @@
:to="localePath(navigation.target)"
class="nav-link"
tabindex="0"
:exact="navigation.exact"
>
<div v-if="navigation.svg">
<SmartIcon :name="navigation.svg" class="svg-icons" />
@@ -40,26 +41,31 @@ const primaryNavigation = [
target: "index",
svg: "link-2",
title: t("navigation.rest"),
exact: true,
},
{
target: "graphql",
svg: "graphql",
title: t("navigation.graphql"),
exact: false,
},
{
target: "realtime",
svg: "globe",
title: t("navigation.realtime"),
exact: false,
},
{
target: "documentation",
svg: "book-open",
title: t("navigation.doc"),
exact: false,
},
{
target: "settings",
svg: "settings",
title: t("navigation.settings"),
exact: false,
},
]
</script>
@@ -105,6 +111,20 @@ const primaryNavigation = [
@apply text-tiny;
}
&.active-link {
@apply text-secondaryDark;
@apply bg-primaryLight;
@apply hover:text-secondaryDark;
.material-icons,
.svg-icons {
@apply opacity-100;
}
&::after {
@apply bg-accent;
}
}
&.exact-active-link {
@apply text-secondaryDark;
@apply bg-primaryLight;

View File

@@ -1,6 +1,10 @@
<template>
<div v-if="show">
<SmartTabs :id="'collections_tab'" v-model="selectedCollectionTab">
<div v-show="show">
<SmartTabs
:id="'collections_tab'"
v-model="selectedCollectionTab"
render-inactive-tabs
>
<SmartTab
:id="'my-collections'"
:label="`${$t('collection.my_collections')}`"

View File

@@ -244,7 +244,7 @@ const createCollectionGist = async () => {
return
}
getJSONCollection()
await getJSONCollection()
try {
const res = await axios.$post(
@@ -316,8 +316,8 @@ const importToTeams = async (content: HoppCollection<HoppRESTRequest>) => {
importingMyCollections.value = false
}
const exportJSON = () => {
getJSONCollection()
const exportJSON = async () => {
await getJSONCollection()
const dataToWrite = collectionJson.value
const file = new Blob([dataToWrite], { type: "application/json" })

View File

@@ -11,6 +11,7 @@
autocomplete="off"
:placeholder="$t('action.search')"
class="py-2 pl-4 pr-2 bg-transparent"
:disabled="collectionsType.type == 'team-collections'"
/>
</div>
<CollectionsChooseType

View File

@@ -322,20 +322,31 @@ const setRestReq = (request: any) => {
)
}
/** Loads request from the save once, checks for unsaved changes, but ignores default values */
const selectRequest = () => {
if (!active.value) {
confirmChange.value = true
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
if (props.saveRequest) {
emit("select", {
picked: {
pickedType: "my-request",
collectionIndex: props.collectionIndex,
folderPath: props.folderPath,
folderName: props.folderName,
requestIndex: props.requestIndex,
},
})
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
confirmChange.value = false
setRestReq(props.request)
} else if (!active.value) {
// If the current request is the same as the request to be loaded in, there is no data loss
const currentReq = getRESTRequest()
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "my-request",
collectionIndex: props.collectionIndex,
folderPath: props.folderPath,
folderName: props.folderName,
requestIndex: props.requestIndex,
},
})
if (isEqualHoppRESTRequest(currentReq, props.request)) {
setRestReq(props.request)
} else {
confirmChange.value = true
}
} else {
const currentReqWithNoChange = active.value.req
const currentFullReq = getRESTRequest()
@@ -345,16 +356,6 @@ const selectRequest = () => {
// Check if there is any changes done on the current request
if (isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)) {
setRestReq(props.request)
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "my-request",
collectionIndex: props.collectionIndex,
folderPath: props.folderPath,
folderName: props.folderName,
requestIndex: props.requestIndex,
},
})
} else {
confirmChange.value = true
}
@@ -374,16 +375,6 @@ const saveRequestChange = () => {
/** Discard changes and change the current request and context */
const discardRequestChange = () => {
setRestReq(props.request)
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "my-request",
collectionIndex: props.collectionIndex,
folderPath: props.folderPath,
folderName: props.folderName,
requestIndex: props.requestIndex,
},
})
if (!isActive.value) {
setRESTSaveContext({
originLocation: "user-collection",

View File

@@ -261,7 +261,7 @@ const active = useReadonlyStream(restSaveContext$, null)
const isSelected = computed(
() =>
props.picked &&
props.picked.pickedType === "team-collection" &&
props.picked.pickedType === "teams-request" &&
props.picked.requestID === props.requestIndex
)
@@ -308,16 +308,19 @@ const setRestReq = (request: HoppRESTRequest) => {
}
const selectRequest = () => {
if (!active.value) {
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
if (props.saveRequest) {
emit("select", {
picked: {
pickedType: "teams-request",
requestID: props.requestIndex,
},
})
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
confirmChange.value = false
setRestReq(props.request)
} else if (!active.value) {
confirmChange.value = true
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "team-collection",
requestID: props.requestIndex,
},
})
} else {
const currentReqWithNoChange = active.value.req
const currentFullReq = getRESTRequest()
@@ -327,13 +330,6 @@ const selectRequest = () => {
// Check if there is any changes done on the current request
if (isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)) {
setRestReq(props.request)
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "team-collection",
requestID: props.requestIndex,
},
})
} else {
confirmChange.value = true
}
@@ -353,13 +349,6 @@ const saveRequestChange = () => {
/** Discard changes and change the current request and context */
const discardRequestChange = () => {
setRestReq(props.request)
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "team-collection",
requestID: props.requestIndex,
},
})
if (!isActive.value) {
setRESTSaveContext({
originLocation: "team-collection",
@@ -367,7 +356,6 @@ const discardRequestChange = () => {
req: props.request,
})
}
confirmChange.value = false
}

View File

@@ -50,18 +50,18 @@
</div>
<div class="border rounded divide-y divide-dividerLight border-divider">
<div
v-for="(variable, index) in vars"
:key="`variable-${index}`"
v-for="({ id, env }, index) in vars"
:key="`variable-${id}-${index}`"
class="flex divide-x divide-dividerLight"
>
<input
v-model="variable.key"
v-model="env.key"
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
:name="'param' + index"
/>
<SmartEnvInput
v-model="variable.value"
v-model="env.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'value' + index"
@@ -119,11 +119,15 @@
import clone from "lodash/clone"
import { computed, ref, watch } from "@nuxtjs/composition-api"
import * as E from "fp-ts/Either"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import { pipe, flow } from "fp-ts/function"
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import {
createEnvironment,
environments$,
getEnviroment,
getEnvironment,
getGlobalVariables,
globalEnv$,
setCurrentEnvironment,
@@ -136,6 +140,14 @@ import {
useToast,
} from "~/helpers/utils/composables"
type EnvironmentVariable = {
id: number
env: {
key: string
value: string
}
}
const t = useI18n()
const toast = useToast()
@@ -158,9 +170,14 @@ const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const idTicker = ref(0)
const name = ref<string | null>(null)
const vars = ref([{ key: "", value: "" }])
const clearIcon = ref("trash-2")
const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } },
])
const clearIcon = refAutoReset<"trash-2" | "check">("trash-2", 1000)
const globalVars = useReadonlyStream(globalEnv$, [])
@@ -176,7 +193,7 @@ const workingEnv = computed(() => {
variables: props.envVars(),
}
} else if (props.editingEnvironmentIndex !== null) {
return getEnviroment(props.editingEnvironmentIndex)
return getEnvironment(props.editingEnvironmentIndex)
} else {
return null
}
@@ -185,15 +202,15 @@ const workingEnv = computed(() => {
const envList = useReadonlyStream(environments$, []) || props.envVars()
const evnExpandError = computed(() => {
for (const variable of vars.value) {
const result = parseTemplateStringE(variable.value.toString(), vars.value)
const variables = pipe(
vars.value,
A.map((e) => e.env)
)
if (E.isLeft(result)) {
console.error("error", result.left)
return true
}
}
return false
return pipe(
variables,
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
)
})
const liveEnvs = computed(() => {
@@ -216,22 +233,38 @@ watch(
(show) => {
if (show) {
name.value = workingEnv.value?.name ?? null
vars.value = clone(workingEnv.value?.variables ?? [])
vars.value = pipe(
workingEnv.value?.variables ?? [],
A.map((e) => ({
id: idTicker.value++,
env: clone(e),
}))
)
}
}
)
const clearContent = () => {
vars.value = []
vars.value = [
{
id: idTicker.value++,
env: {
key: "",
value: "",
},
},
]
clearIcon.value = "check"
toast.success(`${t("state.cleared")}`)
setTimeout(() => (clearIcon.value = "trash-2"), 1000)
}
const addEnvironmentVariable = () => {
vars.value.push({
key: "",
value: "",
id: idTicker.value++,
env: {
key: "",
value: "",
},
})
}
@@ -245,9 +278,19 @@ const saveEnvironment = () => {
return
}
const filterdVariables = pipe(
vars.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.env.key !== ""),
O.map((e) => e.env)
)
)
)
const environmentUpdated: Environment = {
name: name.value,
variables: vars.value,
variables: filterdVariables,
}
if (props.action === "new") {

View File

@@ -3,6 +3,7 @@
<SmartTabs
v-model="selectedOptionTab"
styles="sticky bg-primary top-upperPrimaryStickyFold z-10"
render-inactive-tabs
>
<SmartTab
:id="'query'"
@@ -312,6 +313,7 @@ import {
import draggable from "vuedraggable"
import isEqual from "lodash/isEqual"
import cloneDeep from "lodash/cloneDeep"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import {
useNuxt,
@@ -612,10 +614,13 @@ useCodemirror(queryEditor, gqlQueryString, {
environmentHighlights: false,
})
const copyQueryIcon = ref("copy")
const copyVariablesIcon = ref("copy")
const prettifyQueryIcon = ref("wand")
const prettifyVariablesIcon = ref("wand")
const copyQueryIcon = refAutoReset<"copy" | "check">("copy", 1000)
const copyVariablesIcon = refAutoReset<"copy" | "check">("copy", 1000)
const prettifyQueryIcon = refAutoReset<"wand" | "check" | "info">("wand", 1000)
const prettifyVariablesIcon = refAutoReset<"wand" | "check" | "info">(
"wand",
1000
)
const showSaveRequestModal = ref(false)
@@ -623,7 +628,6 @@ const copyQuery = () => {
copyToClipboard(gqlQueryString.value)
copyQueryIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
}
const response = useStream(gqlResponse$, "", setGQLResponse)
@@ -699,7 +703,6 @@ const prettifyQuery = () => {
toast.error(`${t("error.gql_prettify_invalid_query")}`)
prettifyQueryIcon.value = "info"
}
setTimeout(() => (prettifyQueryIcon.value = "wand"), 1000)
}
const saveRequest = () => {
@@ -710,7 +713,6 @@ const copyVariables = () => {
copyToClipboard(variableString.value)
copyVariablesIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
}
const prettifyVariableString = () => {
@@ -723,7 +725,6 @@ const prettifyVariableString = () => {
prettifyVariablesIcon.value = "info"
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
setTimeout(() => (prettifyVariablesIcon.value = "wand"), 1000)
}
const clearGQLQuery = () => {

View File

@@ -78,6 +78,7 @@
<script setup lang="ts">
import { reactive, ref } from "@nuxtjs/composition-api"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import {
@@ -111,14 +112,16 @@ useCodemirror(
})
)
const downloadResponseIcon = ref("download")
const copyResponseIcon = ref("copy")
const downloadResponseIcon = refAutoReset<"download" | "check">(
"download",
1000
)
const copyResponseIcon = refAutoReset<"copy" | "check">("copy", 1000)
const copyResponse = () => {
copyToClipboard(responseString.value!)
copyResponseIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
}
const downloadResponse = () => {
@@ -135,7 +138,6 @@ const downloadResponse = () => {
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadResponseIcon.value = "download"
}, 1000)
}
</script>

View File

@@ -3,6 +3,7 @@
v-model="selectedNavigationTab"
styles="sticky bg-primary z-10 top-0"
vertical
render-inactive-tabs
>
<SmartTab :id="'history'" icon="clock" :label="`${t('tab.history')}`">
<History
@@ -64,6 +65,7 @@
<SmartTabs
v-model="selectedGqlTab"
styles="border-t border-b border-dividerLight bg-primary sticky z-10 top-sidebarPrimaryStickyFold"
render-inactive-tabs
>
<SmartTab
v-if="queryFields.length > 0"
@@ -193,6 +195,7 @@ import { computed, nextTick, reactive, ref } from "@nuxtjs/composition-api"
import { GraphQLField, GraphQLType } from "graphql"
import { map } from "rxjs/operators"
import { GQLHeader } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { GQLConnection } from "~/helpers/GQLConnection"
import { copyToClipboard } from "~/helpers/utils/clipboard"
@@ -306,8 +309,8 @@ const graphqlTypes = useReadonlyStream(
[]
)
const downloadSchemaIcon = ref("download")
const copySchemaIcon = ref("copy")
const downloadSchemaIcon = refAutoReset<"download" | "check">("download", 1000)
const copySchemaIcon = refAutoReset<"copy" | "check">("copy", 1000)
const graphqlFieldsFilterText = ref("")
@@ -423,7 +426,6 @@ const downloadSchema = () => {
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadSchemaIcon.value = "download"
}, 1000)
}
@@ -432,7 +434,6 @@ const copySchema = () => {
copyToClipboard(schemaString.value)
copySchemaIcon.value = "check"
setTimeout(() => (copySchemaIcon.value = "copy"), 1000)
}
const handleUseHistory = (entry: GQLHistoryEntry) => {

View File

@@ -22,7 +22,10 @@
/>
</span>
</template>
<div class="flex flex-col" role="menu">
<div
class="flex flex-col space-y-1 divide-y divide-dividerLight"
role="menu"
>
<SmartItem
:label="$t('state.none').toLowerCase()"
:info-icon="contentType === null ? 'done' : ''"
@@ -34,19 +37,36 @@
}
"
/>
<SmartItem
v-for="(contentTypeItem, index) in validContentTypes"
:key="`contentTypeItem-${index}`"
:label="contentTypeItem"
:info-icon="contentTypeItem === contentType ? 'done' : ''"
:active-info-icon="contentTypeItem === contentType"
@click.native="
() => {
contentType = contentTypeItem
$refs.contentTypeOptions.tippy().hide()
}
"
/>
<div
v-for="(
contentTypeItems, contentTypeItemsIndex
) in segmentedContentTypes"
:key="`contentTypeItems-${contentTypeItemsIndex}`"
class="flex flex-col py-2 text-left"
>
<div class="flex rounded py-2 px-4">
<span class="text-tiny text-secondaryLight font-bold">
{{ $t(contentTypeItems.title) }}
</span>
</div>
<div class="flex flex-col">
<SmartItem
v-for="(
contentTypeItem, contentTypeIndex
) in contentTypeItems.contentTypes"
:key="`contentTypeItem-${contentTypeIndex}`"
:label="contentTypeItem"
:info-icon="contentTypeItem === contentType ? 'done' : ''"
:active-info-icon="contentTypeItem === contentType"
@click.native="
() => {
contentType = contentTypeItem
$refs.contentTypeOptions.tippy().hide()
}
"
/>
</div>
</div>
</div>
</tippy>
<ButtonSecondary
@@ -106,7 +126,7 @@ import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import { RequestOptionTabs } from "./RequestOptions.vue"
import { useStream } from "~/helpers/utils/composables"
import { knownContentTypes } from "~/helpers/utils/contenttypes"
import { segmentedContentTypes } from "~/helpers/utils/contenttypes"
import {
restContentType$,
restHeaders$,
@@ -119,7 +139,6 @@ const emit = defineEmits<{
(e: "change-tab", value: string): void
}>()
const validContentTypes = Object.keys(knownContentTypes)
const contentType = useStream(restContentType$, null, setRESTContentType)
// The functional headers list (the headers actually in the system)

View File

@@ -38,8 +38,8 @@
drag-class="cursor-grabbing"
>
<div
v-for="(param, index) in workingParams"
:key="`param-${index}`"
v-for="({ id, entry }, index) in workingParams"
:key="`param=${id}-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
@@ -54,21 +54,21 @@
/>
</span>
<SmartEnvInput
v-model="param.key"
v-model="entry.key"
:placeholder="`${$t('count.parameter', { count: index + 1 })}`"
@change="
updateBodyParam(index, {
key: $event,
value: param.value,
active: param.active,
isFile: param.isFile,
value: entry.value,
active: entry.active,
isFile: entry.isFile,
})
"
/>
<div v-if="param.isFile" class="file-chips-container hide-scrollbar">
<div v-if="entry.isFile" class="file-chips-container hide-scrollbar">
<div class="space-x-2 file-chips-wrapper">
<SmartFileChip
v-for="(file, fileIndex) in param.value"
v-for="(file, fileIndex) in entry.value"
:key="`param-${index}-file-${fileIndex}`"
>{{ file.name }}</SmartFileChip
>
@@ -76,14 +76,14 @@
</div>
<span v-else class="flex flex-1">
<SmartEnvInput
v-model="param.value"
v-model="entry.value"
:placeholder="`${$t('count.value', { count: index + 1 })}`"
@change="
updateBodyParam(index, {
key: param.key,
key: entry.key,
value: $event,
active: param.active,
isFile: param.isFile,
active: entry.active,
isFile: entry.isFile,
})
"
/>
@@ -97,7 +97,7 @@
type="file"
multiple
class="p-1 cursor-pointer transition file:transition file:cursor-pointer text-secondaryLight hover:text-secondaryDark file:mr-2 file:py-1 file:px-4 file:rounded file:border-0 file:text-tiny text-tiny file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
@change="setRequestAttachment(index, param, $event)"
@change="setRequestAttachment(index, entry, $event)"
/>
</label>
</span>
@@ -105,15 +105,15 @@
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
param.hasOwnProperty('active')
? param.active
entry.hasOwnProperty('active')
? entry.active
? $t('action.turn_off')
: $t('action.turn_on')
: $t('action.turn_off')
"
:svg="
param.hasOwnProperty('active')
? param.active
entry.hasOwnProperty('active')
? entry.active
? 'check-circle'
: 'circle'
: 'check-circle'
@@ -121,10 +121,10 @@
color="green"
@click.native="
updateBodyParam(index, {
key: param.key,
value: param.value,
active: param.hasOwnProperty('active') ? !param.active : false,
isFile: param.isFile,
key: entry.key,
value: entry.value,
active: entry.hasOwnProperty('active') ? !entry.active : false,
isFile: entry.isFile,
})
"
/>
@@ -164,6 +164,9 @@
<script setup lang="ts">
import { ref, Ref, watch } from "@nuxtjs/composition-api"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import { FormDataKeyValue } from "@hoppscotch/data"
import isEqual from "lodash/isEqual"
import { clone } from "lodash"
@@ -171,10 +174,14 @@ import draggable from "vuedraggable"
import { pluckRef, useI18n, useToast } from "~/helpers/utils/composables"
import { useRESTRequestBody } from "~/newstore/RESTSession"
type WorkingFormDataKeyValue = { id: number; entry: FormDataKeyValue }
const t = useI18n()
const toast = useToast()
const idTicker = ref(0)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const bodyParams = pluckRef<any, any>(useRESTRequestBody(), "body") as Ref<
@@ -182,23 +189,32 @@ const bodyParams = pluckRef<any, any>(useRESTRequestBody(), "body") as Ref<
>
// The UI representation of the parameters list (has the empty end param)
const workingParams = ref<FormDataKeyValue[]>([
const workingParams = ref<WorkingFormDataKeyValue[]>([
{
key: "",
value: "",
active: true,
isFile: false,
id: idTicker.value++,
entry: {
key: "",
value: "",
active: true,
isFile: false,
},
},
])
// Rule: Working Params always have last element is always an empty param
watch(workingParams, (paramsList) => {
if (paramsList.length > 0 && paramsList[paramsList.length - 1].key !== "") {
if (
paramsList.length > 0 &&
paramsList[paramsList.length - 1].entry.key !== ""
) {
workingParams.value.push({
key: "",
value: "",
active: true,
isFile: false,
id: idTicker.value++,
entry: {
key: "",
value: "",
active: true,
isFile: false,
},
})
}
})
@@ -208,19 +224,37 @@ watch(
bodyParams,
(newParamsList) => {
// Sync should overwrite working params
const filteredWorkingParams = workingParams.value.filter(
(e) => e.key !== ""
const filteredWorkingParams = pipe(
workingParams.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.entry.key !== ""),
O.map((e) => e.entry)
)
)
)
if (!isEqual(newParamsList, filteredWorkingParams)) {
workingParams.value = newParamsList
workingParams.value = pipe(
newParamsList,
A.map((x) => ({ id: idTicker.value++, entry: x }))
)
}
},
{ immediate: true }
)
watch(workingParams, (newWorkingParams) => {
const fixedParams = newWorkingParams.filter((e) => e.key !== "")
const fixedParams = pipe(
newWorkingParams,
A.filterMap(
flow(
O.fromPredicate((e) => e.entry.key !== ""),
O.map((e) => e.entry)
)
)
)
if (!isEqual(bodyParams.value, fixedParams)) {
bodyParams.value = fixedParams
}
@@ -228,16 +262,19 @@ watch(workingParams, (newWorkingParams) => {
const addBodyParam = () => {
workingParams.value.push({
key: "",
value: "",
active: true,
isFile: false,
id: idTicker.value++,
entry: {
key: "",
value: "",
active: true,
isFile: false,
},
})
}
const updateBodyParam = (index: number, param: FormDataKeyValue) => {
const updateBodyParam = (index: number, entry: FormDataKeyValue) => {
workingParams.value = workingParams.value.map((h, i) =>
i === index ? param : h
i === index ? { id: h.id, entry } : h
)
}
@@ -280,10 +317,13 @@ const clearContent = () => {
// set params list to the initial state
workingParams.value = [
{
key: "",
value: "",
active: true,
isFile: false,
id: idTicker.value++,
entry: {
key: "",
value: "",
active: true,
isFile: false,
},
},
]
}

View File

@@ -87,6 +87,7 @@
import { computed, ref, watch } from "@nuxtjs/composition-api"
import * as O from "fp-ts/Option"
import { Environment, makeRESTRequest } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import {
@@ -118,9 +119,10 @@ const options = ref<any | null>(null)
const request = ref(getRESTRequest())
const codegenType = ref<CodegenName>("shell-curl")
const copyIcon = ref("copy")
const errorState = ref(false)
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
const requestCode = computed(() => {
const aggregateEnvs = getAggregateEnvs()
const env: Environment = {
@@ -184,7 +186,6 @@ const copyRequestCode = () => {
copyToClipboard(requestCode.value)
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
const searchQuery = ref("")

View File

@@ -39,6 +39,7 @@
<script setup lang="ts">
import { ref, watch } from "@nuxtjs/composition-api"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { setRESTRequest } from "~/newstore/RESTSession"
import { useI18n, useToast } from "~/helpers/utils/composables"
@@ -95,7 +96,7 @@ const handleImport = () => {
hideModal()
}
const pasteIcon = ref("clipboard")
const pasteIcon = refAutoReset<"clipboard" | "check">("clipboard", 1000)
const handlePaste = async () => {
try {
@@ -103,7 +104,6 @@ const handlePaste = async () => {
if (text) {
curl.value = text
pasteIcon.value = "check"
setTimeout(() => (pasteIcon.value = "clipboard"), 1000)
}
} catch (e) {
console.error("Failed to copy: ", e)

View File

@@ -1,363 +1,13 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.parameter_list") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/parameters"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
svg="edit"
:class="{ '!text-accent': bulkMode }"
@click.native="bulkMode = !bulkMode"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
svg="plus"
:disabled="bulkMode"
@click.native="addParam"
/>
</div>
</div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-col flex-1"></div>
<div v-else>
<draggable
v-model="workingParams"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<div
v-for="(param, index) in workingParams"
:key="`param-${param.id}-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<ButtonSecondary
svg="grip-vertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingParams?.length - 1,
}"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="param.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
@change="
updateParam(index, {
id: param.id,
key: $event,
value: param.value,
active: param.active,
})
"
/>
<SmartEnvInput
v-model="param.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
@change="
updateParam(index, {
id: param.id,
key: param.key,
value: $event,
active: param.active,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
param.hasOwnProperty('active')
? param.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:svg="
param.hasOwnProperty('active')
? param.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateParam(index, {
id: param.id,
key: param.key,
value: param.value,
active: param.hasOwnProperty('active')
? !param.active
: false,
})
"
/>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
svg="trash"
color="red"
@click.native="deleteParam(index)"
/>
</span>
</div>
</draggable>
<div
v-if="workingParams.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_files.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.parameters')}`"
/>
<span class="pb-4 text-center">{{ t("empty.parameters") }}</span>
<ButtonSecondary
:label="`${t('add.new')}`"
svg="plus"
filled
class="mb-4"
@click.native="addParam"
/>
</div>
</div>
<div>
<HttpQueryParams />
<br />
<HttpPathVariables />
</div>
</template>
<script setup lang="ts">
import { Ref, ref, watch } from "@nuxtjs/composition-api"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RA from "fp-ts/ReadonlyArray"
import * as E from "fp-ts/Either"
import {
HoppRESTParam,
parseRawKeyValueEntriesE,
rawKeyValueEntriesToString,
RawKeyValueEntry,
} from "@hoppscotch/data"
import isEqual from "lodash/isEqual"
import cloneDeep from "lodash/cloneDeep"
import draggable from "vuedraggable"
import linter from "~/helpers/editor/linting/rawKeyValue"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { useI18n, useToast, useStream } from "~/helpers/utils/composables"
import { restParams$, setRESTParams } from "~/newstore/RESTSession"
import { throwError } from "~/helpers/functional/error"
import { objRemoveKey } from "~/helpers/functional/object"
const t = useI18n()
const toast = useToast()
const idTicker = ref(0)
const bulkMode = ref(false)
const bulkParams = ref("")
const bulkEditor = ref<any | null>(null)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
useCodemirror(bulkEditor, bulkParams, {
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
},
linter,
completer: null,
environmentHighlights: true,
})
// The functional parameters list (the parameters actually applied to the session)
const params = useStream(restParams$, [], setRESTParams) as Ref<HoppRESTParam[]>
// The UI representation of the parameters list (has the empty end param)
const workingParams = ref<Array<HoppRESTParam & { id: number }>>([
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
])
// Rule: Working Params always have last element is always an empty param
watch(workingParams, (paramsList) => {
if (paramsList.length > 0 && paramsList[paramsList.length - 1].key !== "") {
workingParams.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
})
// Sync logic between params and working/bulk params
watch(
params,
(newParamsList) => {
// Sync should overwrite working params
const filteredWorkingParams: HoppRESTParam[] = pipe(
workingParams.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkParams = pipe(
parseRawKeyValueEntriesE(bulkParams.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newParamsList, filteredWorkingParams)) {
workingParams.value = pipe(
newParamsList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newParamsList, filteredBulkParams)) {
bulkParams.value = rawKeyValueEntriesToString(newParamsList)
}
},
{ immediate: true }
)
watch(workingParams, (newWorkingParams) => {
const fixedParams = pipe(
newWorkingParams,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(params.value, fixedParams)) {
params.value = cloneDeep(fixedParams)
}
})
watch(bulkParams, (newBulkParams) => {
const filteredBulkParams = pipe(
parseRawKeyValueEntriesE(newBulkParams),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(params.value, filteredBulkParams)) {
params.value = filteredBulkParams
}
})
const addParam = () => {
workingParams.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateParam = (index: number, param: HoppRESTParam & { id: number }) => {
workingParams.value = workingParams.value.map((h, i) =>
i === index ? param : h
)
}
const deleteParam = (index: number) => {
const paramsBeforeDeletion = cloneDeep(workingParams.value)
if (
!(
paramsBeforeDeletion.length > 0 &&
index === paramsBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingParams.value = paramsBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingParams.value = pipe(
workingParams.value,
A.deleteAt(index),
O.getOrElseW(() => throwError("Working Params Deletion Out of Bounds"))
)
}
const clearContent = () => {
// set params list to the initial state
workingParams.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkParams.value = ""
}
/**
* TODO: Code duplication between QueryParams and Variables
*/
</script>

View File

@@ -0,0 +1,271 @@
<template>
<div>
<div
v-if="envExpandError"
class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
>
{{ nestedVars }}
</div>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight"> My Variables </label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/#"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
svg="plus"
@click.native="addVar"
/>
</div>
</div>
<div>
<draggable
v-model="workingVars"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<div
v-for="(variable, index) in workingVars"
:key="`vari-${variable.id}-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<ButtonSecondary
svg="grip-vertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingVars?.length - 1,
}"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="variable.key"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
@change="
updateVar(index, {
id: variable.id,
key: $event,
value: variable.value,
})
"
/>
<SmartEnvInput
v-model="variable.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
@change="
updateVar(index, {
id: variable.id,
key: variable.key,
value: $event,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
svg="trash"
color="red"
@click.native="deleteVar(index)"
/>
</span>
</div>
</draggable>
<div
v-if="workingVars.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_files.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.parameters')}`"
/>
<span class="pb-4 text-center">{{ emptyVars }}</span>
<ButtonSecondary
:label="`${t('add.new')}`"
svg="plus"
filled
class="mb-4"
@click.native="addVar"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, Ref, ref, watch } from "@nuxtjs/composition-api"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import { HoppRESTVar, parseMyVariablesString } from "@hoppscotch/data"
import draggable from "vuedraggable"
import cloneDeep from "lodash/cloneDeep"
import isEqual from "lodash/isEqual"
import * as E from "fp-ts/Either"
import { useI18n, useStream, useToast } from "~/helpers/utils/composables"
import { throwError } from "~/helpers/functional/error"
import { restVars$, setRESTVars } from "~/newstore/RESTSession"
import { objRemoveKey } from "~/helpers/functional/object"
const t = useI18n()
const toast = useToast()
const emptyVars: string = "Add a new variable"
const nestedVars: string = "nested variables greater than 10 levels"
const idTicker = ref(0)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
// The functional variables list (the variables actually applied to the session)
const vars = useStream(restVars$, [], setRESTVars) as Ref<HoppRESTVar[]>
// The UI representation of the variables list (has the empty end variable)
const workingVars = ref<Array<HoppRESTVar & { id: number }>>([
{
id: idTicker.value++,
key: "",
value: "",
},
])
// Rule: Working vars always have last element is always an empty var
watch(workingVars, (varsList) => {
if (varsList.length > 0 && varsList[varsList.length - 1].key !== "") {
workingVars.value.push({
id: idTicker.value++,
key: "",
value: "",
})
}
})
// Sync logic between params and working/bulk params
watch(
vars,
(newVarsList) => {
// Sync should overwrite working params
const filteredWorkingVars: HoppRESTVar[] = pipe(
workingVars.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(newVarsList, filteredWorkingVars)) {
workingVars.value = pipe(
newVarsList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
},
{ immediate: true }
)
watch(workingVars, (newWorkingVars) => {
const fixedVars = pipe(
newWorkingVars,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(vars.value, fixedVars)) {
vars.value = cloneDeep(fixedVars)
}
})
const addVar = () => {
workingVars.value.push({
id: idTicker.value++,
key: "",
value: "",
})
}
const updateVar = (index: number, vari: HoppRESTVar & { id: number }) => {
workingVars.value = workingVars.value.map((h, i) => (i === index ? vari : h))
}
const deleteVar = (index: number) => {
const varsBeforeDeletion = cloneDeep(workingVars.value)
if (
!(varsBeforeDeletion.length > 0 && index === varsBeforeDeletion.length - 1)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingVars.value = varsBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingVars.value = pipe(
workingVars.value,
A.deleteAt(index),
O.getOrElseW(() => throwError("Working Params Deletion Out of Bounds"))
)
}
const envExpandError = computed(() => {
const variables = pipe(vars.value)
return pipe(
variables,
A.exists(({ value }) => E.isLeft(parseMyVariablesString(value, variables)))
)
})
const clearContent = () => {
// set params list to the initial state
workingVars.value = [
{
id: idTicker.value++,
key: "",
value: "",
},
]
}
</script>

View File

@@ -0,0 +1,363 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.parameter_list") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/parameters"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
svg="edit"
:class="{ '!text-accent': bulkMode }"
@click.native="bulkMode = !bulkMode"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
svg="plus"
:disabled="bulkMode"
@click.native="addParam"
/>
</div>
</div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-col flex-1"></div>
<div v-else>
<draggable
v-model="workingParams"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<div
v-for="(param, index) in workingParams"
:key="`param-${param.id}-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<ButtonSecondary
svg="grip-vertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingParams?.length - 1,
}"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="param.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
@change="
updateParam(index, {
id: param.id,
key: $event,
value: param.value,
active: param.active,
})
"
/>
<SmartEnvInput
v-model="param.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
@change="
updateParam(index, {
id: param.id,
key: param.key,
value: $event,
active: param.active,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
param.hasOwnProperty('active')
? param.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:svg="
param.hasOwnProperty('active')
? param.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateParam(index, {
id: param.id,
key: param.key,
value: param.value,
active: param.hasOwnProperty('active')
? !param.active
: false,
})
"
/>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
svg="trash"
color="red"
@click.native="deleteParam(index)"
/>
</span>
</div>
</draggable>
<div
v-if="workingParams.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_files.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.parameters')}`"
/>
<span class="pb-4 text-center">{{ t("empty.parameters") }}</span>
<ButtonSecondary
:label="`${t('add.new')}`"
svg="plus"
filled
class="mb-4"
@click.native="addParam"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Ref, ref, watch } from "@nuxtjs/composition-api"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RA from "fp-ts/ReadonlyArray"
import * as E from "fp-ts/Either"
import {
HoppRESTParam,
parseRawKeyValueEntriesE,
rawKeyValueEntriesToString,
RawKeyValueEntry,
} from "@hoppscotch/data"
import isEqual from "lodash/isEqual"
import cloneDeep from "lodash/cloneDeep"
import draggable from "vuedraggable"
import linter from "~/helpers/editor/linting/rawKeyValue"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { useI18n, useToast, useStream } from "~/helpers/utils/composables"
import { restParams$, setRESTParams } from "~/newstore/RESTSession"
import { throwError } from "~/helpers/functional/error"
import { objRemoveKey } from "~/helpers/functional/object"
const t = useI18n()
const toast = useToast()
const idTicker = ref(0)
const bulkMode = ref(false)
const bulkParams = ref("")
const bulkEditor = ref<any | null>(null)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
useCodemirror(bulkEditor, bulkParams, {
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
},
linter,
completer: null,
environmentHighlights: true,
})
// The functional parameters list (the parameters actually applied to the session)
const params = useStream(restParams$, [], setRESTParams) as Ref<HoppRESTParam[]>
// The UI representation of the parameters list (has the empty end param)
const workingParams = ref<Array<HoppRESTParam & { id: number }>>([
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
])
// Rule: Working Params always have last element is always an empty param
watch(workingParams, (paramsList) => {
if (paramsList.length > 0 && paramsList[paramsList.length - 1].key !== "") {
workingParams.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
})
// Sync logic between params and working/bulk params
watch(
params,
(newParamsList) => {
// Sync should overwrite working params
const filteredWorkingParams: HoppRESTParam[] = pipe(
workingParams.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkParams = pipe(
parseRawKeyValueEntriesE(bulkParams.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newParamsList, filteredWorkingParams)) {
workingParams.value = pipe(
newParamsList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newParamsList, filteredBulkParams)) {
bulkParams.value = rawKeyValueEntriesToString(newParamsList)
}
},
{ immediate: true }
)
watch(workingParams, (newWorkingParams) => {
const fixedParams = pipe(
newWorkingParams,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(params.value, fixedParams)) {
params.value = cloneDeep(fixedParams)
}
})
watch(bulkParams, (newBulkParams) => {
const filteredBulkParams = pipe(
parseRawKeyValueEntriesE(newBulkParams),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(params.value, filteredBulkParams)) {
params.value = filteredBulkParams
}
})
const addParam = () => {
workingParams.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateParam = (index: number, param: HoppRESTParam & { id: number }) => {
workingParams.value = workingParams.value.map((h, i) =>
i === index ? param : h
)
}
const deleteParam = (index: number) => {
const paramsBeforeDeletion = cloneDeep(workingParams.value)
if (
!(
paramsBeforeDeletion.length > 0 &&
index === paramsBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingParams.value = paramsBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingParams.value = pipe(
workingParams.value,
A.deleteAt(index),
O.getOrElseW(() => throwError("Working Params Deletion Out of Bounds"))
)
}
const clearContent = () => {
// set params list to the initial state
workingParams.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkParams.value = ""
}
</script>

View File

@@ -61,6 +61,7 @@ import { computed, reactive, Ref, ref } from "@nuxtjs/composition-api"
import * as TO from "fp-ts/TaskOption"
import { pipe } from "fp-ts/function"
import { HoppRESTReqBody, ValidContentTypes } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { getEditorLangForMimeType } from "~/helpers/editorutils"
import { pluckRef, useI18n, useToast } from "~/helpers/utils/composables"
@@ -91,7 +92,8 @@ const rawParamsBody = pluckRef(
>,
"body"
)
const prettifyIcon = ref("wand")
const prettifyIcon = refAutoReset<"wand" | "check" | "info">("wand", 1000)
const rawInputEditorLang = computed(() =>
getEditorLangForMimeType(props.contentType)
@@ -148,6 +150,5 @@ const prettifyRequestBody = () => {
prettifyIcon.value = "info"
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
setTimeout(() => (prettifyIcon.value = "wand"), 1000)
}
</script>

View File

@@ -215,6 +215,7 @@ import { computed, ref, watch } from "@nuxtjs/composition-api"
import { isLeft, isRight } from "fp-ts/lib/Either"
import * as E from "fp-ts/Either"
import cloneDeep from "lodash/cloneDeep"
import { refAutoReset } from "@vueuse/core"
import {
updateRESTResponse,
restEndpoint$,
@@ -347,7 +348,8 @@ const newSendRequest = async () => {
const ensureMethodInEndpoint = () => {
if (
!/^http[s]?:\/\//.test(newEndpoint.value) &&
!newEndpoint.value.startsWith("<<")
!newEndpoint.value.startsWith("<<") &&
!newEndpoint.value.startsWith("{{")
) {
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
@@ -393,7 +395,11 @@ const clearContent = () => {
resetRESTRequest()
}
const copyLinkIcon = hasNavigatorShare ? ref("share-2") : ref("copy")
const copyLinkIcon = refAutoReset<"share-2" | "copy" | "check">(
hasNavigatorShare ? "share-2" : "copy",
1000
)
const shareLink = ref<string | null>("")
const fetchingShareLink = ref(false)
@@ -448,7 +454,6 @@ const copyShareLink = (shareLink: string) => {
copyLinkIcon.value = "check"
copyToClipboard(`https://hopp.sh/r${shareLink}`)
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyLinkIcon.value = "copy"), 2000)
}
}

View File

@@ -2,11 +2,12 @@
<SmartTabs
v-model="selectedRealtimeTab"
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
render-inactive-tabs
>
<SmartTab
:id="'params'"
:label="`${$t('tab.parameters')}`"
:info="`${newActiveParamsCount$}`"
:info="`${Number(newActiveParamsCount$) + Number(newActiveVarsCount$)}`"
>
<HttpParameters />
</SmartTab>
@@ -49,6 +50,7 @@ import { useReadonlyStream } from "~/helpers/utils/composables"
import {
restActiveHeadersCount$,
restActiveParamsCount$,
restActiveVarsCount$,
usePreRequestScript,
useTestScript,
} from "~/newstore/RESTSession"
@@ -75,6 +77,16 @@ const newActiveParamsCount$ = useReadonlyStream(
null
)
const newActiveVarsCount$ = useReadonlyStream(
restActiveVarsCount$.pipe(
map((e) => {
if (e === 0) return null
return `${e}`
})
),
null
)
const newActiveHeadersCount$ = useReadonlyStream(
restActiveHeadersCount$.pipe(
map((e) => {

View File

@@ -117,9 +117,21 @@
<span class="text-secondary"> {{ t("response.time") }}: </span>
{{ `${response.meta.responseDuration} ms` }}
</span>
<span v-if="response.meta && response.meta.responseSize">
<span
v-if="response.meta && response.meta.responseSize"
v-tippy="
readableResponseSize
? { theme: 'tooltip' }
: { onShow: () => false }
"
:title="`${response.meta.responseSize} B`"
>
<span class="text-secondary"> {{ t("response.size") }}: </span>
{{ `${response.meta.responseSize} B` }}
{{
readableResponseSize
? readableResponseSize
: `${response.meta.responseSize} B`
}}
</span>
</div>
</div>
@@ -141,6 +153,29 @@ const props = defineProps<{
response: HoppRESTResponse
}>()
/**
* Gives the response size in a human readable format
* (changes unit from B to MB/KB depending on the size)
* If no changes (error res state) or value can be made (size < 1KB ?),
* it returns undefined
*/
const readableResponseSize = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail" ||
props.response.type === "script_fail" ||
props.response.type === "fail"
)
return undefined
const size = props.response.meta.responseSize
if (size >= 100000) return (size / 1000000).toFixed(2) + " MB"
if (size >= 1000) return (size / 1000).toFixed(2) + " KB"
return undefined
})
const statusCategory = computed(() => {
if (
props.response.type === "loading" ||

View File

@@ -3,6 +3,7 @@
v-model="selectedNavigationTab"
styles="sticky bg-primary z-10 top-0"
vertical
render-inactive-tabs
>
<SmartTab :id="'history'" icon="clock" :label="`${$t('tab.history')}`">
<History ref="historyComponent" :page="'rest'" />

View File

@@ -12,10 +12,13 @@
<span class="text-secondaryDark">
{{ env.key }}
</span>
<span class="text-secondaryDark">
<span class="text-secondaryDark pl-2 break-all">
{{ ` \xA0 — \xA0 ${env.value}` }}
</span>
<span v-if="status === 'updations'" class="text-secondaryLight">
<span
v-if="status === 'updations'"
class="text-secondaryLight px-2 break-all"
>
{{ ` \xA0 ← \xA0 ${env.previousValue}` }}
</span>
</div>

View File

@@ -26,8 +26,8 @@
</template>
<script setup lang="ts">
import { ref } from "@nuxtjs/composition-api"
import { HoppRESTHeader } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n, useToast } from "~/helpers/utils/composables"
@@ -39,12 +39,11 @@ const props = defineProps<{
headers: Array<HoppRESTHeader>
}>()
const copyIcon = ref("copy")
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
const copyHeaders = () => {
copyToClipboard(JSON.stringify(props.headers))
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
</script>

View File

@@ -28,7 +28,7 @@
</template>
<script setup lang="ts">
import { ref } from "@nuxtjs/composition-api"
import { refAutoReset } from "@vueuse/core"
import { HoppRESTHeader } from "~/../hoppscotch-data/dist"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n, useToast } from "~/helpers/utils/composables"
@@ -41,12 +41,11 @@ defineProps<{
header: HoppRESTHeader
}>()
const copyIcon = ref("copy")
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
const copyHeader = (headerValue: string) => {
copyToClipboard(headerValue)
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
</script>

View File

@@ -3,6 +3,7 @@
v-if="response"
v-model="selectedLensTab"
styles="sticky z-10 bg-primary top-lowerPrimaryStickyFold"
render-inactive-tabs
>
<SmartTab
v-for="(lens, index) in validLenses"

View File

@@ -1,12 +1,15 @@
<template>
<div class="flex flex-col flex-1">
<div
v-if="response.type === 'success' || response.type === 'fail'"
class="flex flex-col flex-1"
>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("response.body") }}
</label>
<div class="flex">
<div class="flex items-center">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
@@ -15,6 +18,14 @@
svg="wrap-text"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.filter_response')"
svg="filter"
:class="{ '!text-accent': toggleFilter }"
@click.native.prevent="toggleFilterState"
/>
<ButtonSecondary
v-if="response.body"
ref="downloadResponse"
@@ -33,7 +44,47 @@
/>
</div>
</div>
<div ref="jsonResponse" class="flex flex-col flex-1"></div>
<div
v-if="toggleFilter"
class="bg-primary flex sticky top-lowerTertiaryStickyFold z-10 border-b border-dividerLight"
>
<div
class="bg-primaryLight border-divider text-secondaryDark inline-flex flex-1 items-center"
>
<span class="inline-flex flex-1 items-center px-4">
<SmartIcon name="search" class="h-4 w-4 text-secondaryLight" />
<input
v-model="filterQueryText"
v-focus
class="input !border-0 !px-2"
:placeholder="`${t('response.filter_response_body')}`"
type="text"
/>
</span>
<div
v-if="filterResponseError"
class="px-2 py-1 text-tiny flex items-center justify-center text-accentContrast rounded"
:class="{
'bg-red-500':
filterResponseError.type === 'JSON_PARSE_FAILED' ||
filterResponseError.type === 'JSON_PATH_QUERY_ERROR',
'bg-amber-500': filterResponseError.type === 'RESPONSE_EMPTY',
}"
>
<SmartIcon name="info" class="svg-icons mr-1.5" />
<span>{{ filterResponseError.error }}</span>
</div>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="t('app.wiki')"
svg="help-circle"
to="https://github.com/JSONPath-Plus/JSONPath"
blank
/>
</div>
</div>
<div ref="jsonResponse" class="flex flex-col flex-1 h-auto h-full"></div>
<div
v-if="outlinePath"
class="sticky bottom-0 z-10 flex px-2 overflow-auto border-t bg-primaryLight border-dividerLight flex-nowrap hide-scrollbar"
@@ -142,8 +193,10 @@
<script setup lang="ts">
import * as LJSON from "lossless-json"
import * as O from "fp-ts/Option"
import * as E from "fp-ts/Either"
import { pipe } from "fp-ts/function"
import { computed, ref, reactive } from "@nuxtjs/composition-api"
import { JSONPath } from "jsonpath-plus"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
@@ -165,16 +218,51 @@ const props = defineProps<{
const { responseBodyText } = useResponseBody(props.response)
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
const toggleFilter = ref(false)
const filterQueryText = ref("")
const { downloadIcon, downloadResponse } = useDownloadResponse(
"application/json",
responseBodyText
type BodyParseError =
| { type: "JSON_PARSE_FAILED" }
| { type: "JSON_PATH_QUERY_FAILED"; error: Error }
const responseJsonObject = computed(() =>
pipe(
responseBodyText.value,
E.tryCatchK(
LJSON.parse,
(): BodyParseError => ({ type: "JSON_PARSE_FAILED" })
)
)
)
const jsonResponseBodyText = computed(() => {
if (filterQueryText.value.length > 0) {
return pipe(
responseJsonObject.value,
E.chain((parsedJSON) =>
E.tryCatch(
() =>
JSONPath({
path: filterQueryText.value,
json: parsedJSON,
}) as undefined,
(err): BodyParseError => ({
type: "JSON_PATH_QUERY_FAILED",
error: err as Error,
})
)
),
E.map(JSON.stringify)
)
} else {
return E.right(responseBodyText.value)
}
})
const jsonBodyText = computed(() =>
pipe(
responseBodyText.value,
jsonResponseBodyText.value,
E.getOrElse(() => responseBodyText.value),
O.tryCatchK(LJSON.parse),
O.map((val) => LJSON.stringify(val, undefined, 2)),
O.getOrElse(() => responseBodyText.value)
@@ -189,6 +277,38 @@ const ast = computed(() =>
)
)
const filterResponseError = computed(() =>
pipe(
jsonResponseBodyText.value,
E.match(
(e) => {
switch (e.type) {
case "JSON_PATH_QUERY_FAILED":
return { type: "JSON_PATH_QUERY_ERROR", error: e.error.message }
case "JSON_PARSE_FAILED":
return {
type: "JSON_PARSE_FAILED",
error: t("error.json_parsing_failed").toString(),
}
}
},
(result) =>
result === "[]"
? {
type: "RESPONSE_EMPTY",
error: t("error.no_results_found").toString(),
}
: undefined
)
)
)
const { copyIcon, copyResponse } = useCopyResponse(jsonBodyText)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"application/json",
jsonBodyText
)
const outlineOptions = ref<any | null>(null)
const jsonResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
@@ -227,6 +347,11 @@ const outlinePath = computed(() =>
O.getOrElseW(() => null)
)
)
const toggleFilterState = () => {
filterQueryText.value = ""
toggleFilter.value = !toggleFilter.value
}
</script>
<style lang="scss" scoped>

View File

@@ -65,6 +65,7 @@ import { pipe } from "fp-ts/function"
import * as RR from "fp-ts/ReadonlyRecord"
import * as O from "fp-ts/Option"
import { translateToNewRequest } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { useI18n, useToast } from "~/helpers/utils/composables"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { Shortcode } from "~/helpers/shortcodes/Shortcode"
@@ -93,7 +94,8 @@ const requestMethodLabels = {
} as const
const timeStampRef = ref()
const copyIconRefs = ref<"copy" | "check">("copy")
const copyIconRefs = refAutoReset<"copy" | "check">("copy", 1000)
const parseShortcodeRequest = computed(() =>
pipe(props.shortcode.request, JSON.parse, translateToNewRequest)
@@ -118,7 +120,6 @@ const copyShortcode = (codeID: string) => {
copyToClipboard(`https://hopp.sh/r/${codeID}`)
toast.success(`${t("state.copied_to_clipboard")}`)
copyIconRefs.value = "check"
setTimeout(() => (copyIconRefs.value = "copy"), 1000)
}
</script>

View File

@@ -12,7 +12,7 @@
/>
</div>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold text-secondaryLight">
@@ -113,6 +113,7 @@ import { computed, reactive, ref } from "@nuxtjs/composition-api"
import { pipe } from "fp-ts/function"
import * as TO from "fp-ts/TaskOption"
import * as O from "fp-ts/Option"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "~/helpers/editor/codemirror"
import jsonLinter from "~/helpers/editor/linting/json"
import { readFileAsText } from "~/helpers/functional/files"
@@ -145,7 +146,8 @@ const toast = useToast()
const linewrapEnabled = ref(true)
const wsCommunicationBody = ref<HTMLElement>()
const prettifyIcon = ref<"wand" | "check" | "info">("wand")
const prettifyIcon = refAutoReset<"wand" | "check" | "info">("wand", 1000)
const knownContentTypes = {
JSON: "application/ld+json",
@@ -216,6 +218,5 @@ const prettifyRequestBody = () => {
prettifyIcon.value = "info"
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
setTimeout(() => (prettifyIcon.value = "wand"), 1000)
}
</script>

View File

@@ -51,7 +51,11 @@
</div>
</div>
<div v-if="!minimized" class="overflow-hidden bg-primaryLight">
<SmartTabs v-model="selectedTab" styles="bg-primaryLight">
<SmartTabs
v-model="selectedTab"
styles="bg-primaryLight"
render-inactive-tabs
>
<SmartTab v-if="isJSON(entry.payload)" id="json" label="JSON" />
<SmartTab id="raw" label="Raw" />
</SmartTabs>
@@ -203,7 +207,7 @@ import * as LJSON from "lossless-json"
import * as O from "fp-ts/Option"
import { pipe } from "fp-ts/function"
import { ref, computed, reactive, watch } from "@nuxtjs/composition-api"
import { useTimeAgo } from "@vueuse/core"
import { refAutoReset, useTimeAgo } from "@vueuse/core"
import { LogEntryData } from "./Log.vue"
import { useI18n } from "~/helpers/utils/composables"
import { copyToClipboard } from "~/helpers/utils/clipboard"
@@ -310,11 +314,11 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
logPayload
)
const copyQueryIcon = ref("copy")
const copyQueryIcon = refAutoReset<"copy" | "check">("copy", 1000)
const copyQuery = (entry: string) => {
copyToClipboard(entry)
copyQueryIcon.value = "check"
setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
}
// Relative Time

View File

@@ -35,10 +35,13 @@ import { EditorState, Extension } from "@codemirror/state"
import clone from "lodash/clone"
import { tooltips } from "@codemirror/tooltip"
import { history, historyKeymap } from "@codemirror/history"
import { HoppRESTVar } from "@hoppscotch/data"
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { HoppReactiveVarPlugin } from "~/helpers/editor/extensions/HoppVariable"
import { restVars$ } from "~/newstore/RESTSession"
const props = withDefaults(
defineProps<{
@@ -46,6 +49,7 @@ const props = withDefaults(
placeholder: string
styles: string
envs: { key: string; value: string; source: string }[] | null
vars: { key: string; value: string }[] | null
focus: boolean
readonly: boolean
}>(),
@@ -54,6 +58,7 @@ const props = withDefaults(
placeholder: "",
styles: "",
envs: null,
vars: null,
focus: false,
readonly: false,
}
@@ -109,6 +114,7 @@ let pastedValue: string | null = null
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
AggregateEnvironment[]
>
const aggregateVars = useReadonlyStream(restVars$, []) as Ref<HoppRESTVar[]>
const envVars = computed(() =>
props.envs
@@ -120,7 +126,17 @@ const envVars = computed(() =>
: aggregateEnvs.value
)
const varVars = computed(() =>
props.vars
? props.vars.map((x) => ({
key: x.key,
value: x.value,
}))
: aggregateVars.value
)
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
const varTooltipPlugin = new HoppReactiveVarPlugin(varVars, view)
const initView = (el: any) => {
const extensions: Extension = [
@@ -146,6 +162,7 @@ const initView = (el: any) => {
position: "absolute",
}),
envTooltipPlugin,
varTooltipPlugin,
placeholderExt(props.placeholder),
EditorView.domEventHandlers({
paste(ev) {

View File

@@ -1,5 +1,5 @@
<template>
<div v-show="active" class="flex flex-col flex-1">
<div v-if="shouldRender" v-show="active" class="flex flex-col flex-1">
<slot></slot>
</div>
</template>
@@ -33,11 +33,24 @@ const tabMeta = computed<TabMeta>(() => ({
label: props.label,
}))
const { activeTabID, addTabEntry, updateTabEntry, removeTabEntry } =
inject<TabProvider>("tabs-system")!
const {
activeTabID,
renderInactive,
addTabEntry,
updateTabEntry,
removeTabEntry,
} = inject<TabProvider>("tabs-system")!
const active = computed(() => activeTabID.value === props.id)
const shouldRender = computed(() => {
// If render inactive is true, then it should be rendered nonetheless
if (renderInactive.value) return true
// Else, return whatever is the active state
return active.value
})
onMounted(() => {
addTabEntry(props.id, tabMeta.value)
})

View File

@@ -80,6 +80,8 @@ export type TabMeta = {
}
export type TabProvider = {
// Whether inactive tabs should remain rendered
renderInactive: ComputedRef<boolean>
activeTabID: ComputedRef<string>
addTabEntry: (tabID: string, meta: TabMeta) => void
updateTabEntry: (tabID: string, newMeta: TabMeta) => void
@@ -91,6 +93,10 @@ const props = defineProps({
type: String,
default: "",
},
renderInactiveTabs: {
type: Boolean,
default: false,
},
vertical: {
type: Boolean,
default: false,
@@ -144,6 +150,7 @@ const removeTabEntry = (tabID: string) => {
}
provide<TabProvider>("tabs-system", {
renderInactive: computed(() => props.renderInactiveTabs),
activeTabID: computed(() => props.value),
addTabEntry,
updateTabEntry,

View File

@@ -25,7 +25,7 @@ import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession"
import {
environmentsStore,
getCurrentEnvironment,
getEnviroment,
getEnvironment,
getGlobalVariables,
setGlobalEnvVariables,
updateEnvironment,
@@ -97,7 +97,7 @@ export const runRESTRequest$ = (): TaskEither<
setGlobalEnvVariables(runResult.right.envs.global)
if (environmentsStore.value.currentEnvironmentIndex !== -1) {
const env = getEnviroment(
const env = getEnvironment(
environmentsStore.value.currentEnvironmentIndex
)
updateEnvironment(

View File

@@ -45,28 +45,23 @@ import {
} from "~/helpers/fb/auth"
const BACKEND_GQL_URL =
process.env.context === "production"
? "https://api.hoppscotch.io/graphql"
: "https://api.hoppscotch.io/graphql"
process.env.BACKEND_GQL_URL ?? "https://api.hoppscotch.io/graphql"
const BACKEND_WS_URL =
process.env.BACKEND_WS_URL ?? "wss://api.hoppscotch.io/graphql"
// const storage = makeDefaultStorage({
// idbName: "hoppcache-v1",
// maxAge: 7,
// })
const subscriptionClient = new SubscriptionClient(
process.env.context === "production"
? "wss://api.hoppscotch.io/graphql"
: "wss://api.hoppscotch.io/graphql",
{
reconnect: true,
connectionParams: () => {
return {
authorization: `Bearer ${authIdToken$.value}`,
}
},
}
)
const subscriptionClient = new SubscriptionClient(BACKEND_WS_URL, {
reconnect: true,
connectionParams: () => {
return {
authorization: `Bearer ${authIdToken$.value}`,
}
},
})
authIdToken$.subscribe(() => {
subscriptionClient.client?.close()
@@ -226,7 +221,7 @@ export const runGQLSubscription = <
createRequest(args.query, args.variables)
)
wonkaPipe(
const sub = wonkaPipe(
source,
subscribe((res) => {
result$.next(
@@ -261,7 +256,8 @@ export const runGQLSubscription = <
})
)
return result$
// Returns the stream and a subscription handle to unsub
return [result$, sub] as const
}
export const useGQLQuery = <DocType, DocVarType, DocErrorType extends string>(

View File

@@ -809,6 +809,37 @@ const samples = [
testScript: "",
}),
},
{
command: `curl https://example.com -d "alpha=beta&request_id=4"`,
response: makeRESTRequest({
method: "POST",
name: "Untitled request",
endpoint: "https://example.com/",
auth: {
authType: "none",
authActive: true,
},
body: {
contentType: "application/x-www-form-urlencoded",
body: rawKeyValueEntriesToString([
{
active: true,
key: "alpha",
value: "beta",
},
{
active: true,
key: "request_id",
value: "4",
},
]),
},
params: [],
headers: [],
preRequestScript: "",
testScript: "",
}),
},
]
describe("Parse curl command to Hopp REST Request", () => {

View File

@@ -93,7 +93,8 @@ export const parseCurlCommand = (curlCommand: string) => {
hasBodyBeenParsed = true
} else if (
rawContentType.includes("application/x-www-form-urlencoded") &&
!!pairs
!!pairs &&
Array.isArray(rawData)
) {
body = pairs.map((p) => p.join(": ")).join("\n") || null
contentType = "application/x-www-form-urlencoded"

View File

@@ -158,14 +158,11 @@ const getXMLBody = (rawData: string) =>
O.alt(() => O.some(rawData))
)
const getFormattedJSON = (jsonString: string) =>
pipe(
jsonString.replaceAll('\\"', '"'),
safeParseJSON,
O.map((parsedJSON) => JSON.stringify(parsedJSON, null, 2)),
O.getOrElse(() => "{ }"),
O.of
)
const getFormattedJSON = flow(
safeParseJSON,
O.map((parsedJSON) => JSON.stringify(parsedJSON, null, 2)),
O.getOrElse(() => "{ }")
)
const getXWWWFormUrlEncodedBody = flow(
decodeURIComponent,
@@ -191,7 +188,7 @@ export function parseBody(
case "application/ld+json":
case "application/vnd.api+json":
case "application/json":
return getFormattedJSON(rawData)
return O.some(getFormattedJSON(rawData))
case "application/x-www-form-urlencoded":
return getXWWWFormUrlEncodedBody(rawData)

View File

@@ -38,7 +38,6 @@ import { Completer } from "./completion"
import { LinterDefinition } from "./linting/linter"
import { basicSetup, baseTheme, baseHighlightStyle } from "./themes/baseTheme"
import { HoppEnvironmentPlugin } from "./extensions/HoppEnvironment"
import { IndentedLineWrapPlugin } from "./extensions/IndentedLineWrap"
// TODO: Migrate from legacy mode
type ExtendedEditorConfig = {
@@ -238,7 +237,7 @@ export function useCodemirror(
),
lineWrapping.of(
options.extendedEditorConfig.lineWrapping
? [IndentedLineWrapPlugin]
? [EditorView.lineWrapping]
: []
),
keymap.of([
@@ -325,7 +324,7 @@ export function useCodemirror(
(newMode) => {
view.value?.dispatch({
effects: lineWrapping.reconfigure(
newMode ? [EditorView.lineWrapping, IndentedLineWrapPlugin] : []
newMode ? [EditorView.lineWrapping] : []
),
})
}

View File

@@ -16,7 +16,7 @@ import {
getAggregateEnvs,
} from "~/newstore/environments"
const HOPP_ENVIRONMENT_REGEX = /(<<\w+>>)/g
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
const HOPP_ENV_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
@@ -44,8 +44,9 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
let start = pos
let end = pos
while (start > from && /\w/.test(text[start - from - 1])) start--
while (end < to && /\w/.test(text[end - from])) end++
while (start > from && /[a-zA-Z0-9-_]+/.test(text[start - from - 1]))
start--
while (end < to && /[a-zA-Z0-9-_]+/.test(text[end - from])) end++
if (
(start === pos && side < 0) ||

View File

@@ -0,0 +1,149 @@
import { watch, Ref } from "@nuxtjs/composition-api"
import { Compartment } from "@codemirror/state"
import { hoverTooltip } from "@codemirror/tooltip"
import {
Decoration,
EditorView,
MatchDecorator,
ViewPlugin,
} from "@codemirror/view"
import * as E from "fp-ts/Either"
import { HoppRESTVar, parseTemplateStringE } from "@hoppscotch/data"
const HOPP_ENVIRONMENT_REGEX = /({{\w+}})/g
const HOPP_ENV_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
const HOPP_ENV_HIGHLIGHT_FOUND =
"bg-accentDark text-accentContrast hover:bg-accent"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND =
"bg-red-500 text-accentContrast hover:bg-red-600"
const cursorTooltipField = (aggregateEnvs: HoppRESTVar[]) =>
hoverTooltip(
(view, pos, side) => {
const { from, to, text } = view.state.doc.lineAt(pos)
// TODO: When Codemirror 6 allows this to work (not make the
// popups appear half of the time) use this implementation
// const wordSelection = view.state.wordAt(pos)
// if (!wordSelection) return null
// const word = view.state.doc.sliceString(
// wordSelection.from - 2,
// wordSelection.to + 2
// )
// if (!HOPP_ENVIRONMENT_REGEX.test(word)) return null
// Tracking the start and the end of the words
let start = pos
let end = pos
while (start > from && /\w/.test(text[start - from - 1])) start--
while (end < to && /\w/.test(text[end - from])) end++
if (
(start === pos && side < 0) ||
(end === pos && side > 0) ||
!HOPP_ENVIRONMENT_REGEX.test(
text.slice(start - from - 2, end - from + 2)
)
)
return null
const envValue =
aggregateEnvs.find(
(env) => env.key === text.slice(start - from, end - from)
// env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2)
)?.value ?? "not found"
const result = parseTemplateStringE(envValue, aggregateEnvs)
const finalEnv = E.isLeft(result) ? "error" : result.right
return {
pos: start,
end: to,
above: true,
arrow: true,
create() {
const dom = document.createElement("span")
const xmp = document.createElement("xmp")
xmp.textContent = finalEnv
dom.appendChild(xmp)
dom.className = "tooltip-theme"
return { dom }
},
}
},
// HACK: This is a hack to fix hover tooltip not coming half of the time
// https://github.com/codemirror/tooltip/blob/765c463fc1d5afcc3ec93cee47d72606bed27e1d/src/tooltip.ts#L622
// Still doesn't fix the not showing up some of the time issue, but this is atleast more consistent
{ hoverTime: 1 } as any
)
function checkEnv(env: string, aggregateEnvs: HoppRESTVar[]) {
const className = aggregateEnvs.find(
(k: { key: string }) => k.key === env.slice(2, -2)
)
? HOPP_ENV_HIGHLIGHT_FOUND
: HOPP_ENV_HIGHLIGHT_NOT_FOUND
return Decoration.mark({
class: `${HOPP_ENV_HIGHLIGHT} ${className}`,
})
}
const getMatchDecorator = (aggregateEnvs: HoppRESTVar[]) =>
new MatchDecorator({
regexp: HOPP_ENVIRONMENT_REGEX,
decoration: (m) => checkEnv(m[0], aggregateEnvs),
})
export const environmentHighlightStyle = (aggregateEnvs: HoppRESTVar[]) => {
const decorator = getMatchDecorator(aggregateEnvs)
return ViewPlugin.define(
(view) => ({
decorations: decorator.createDeco(view),
update(u) {
this.decorations = decorator.updateDeco(u, this.decorations)
},
}),
{
decorations: (v) => v.decorations,
}
)
}
export class HoppReactiveVarPlugin {
private compartment = new Compartment()
private envs: HoppRESTVar[] = []
constructor(
envsRef: Ref<HoppRESTVar[]>,
private editorView: Ref<EditorView | undefined>
) {
watch(
envsRef,
(envs) => {
this.envs = envs
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
]),
})
},
{ immediate: true }
)
}
get extension() {
return this.compartment.of([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
])
}
}

View File

@@ -1,27 +0,0 @@
import { EditorView } from "@codemirror/view"
const WrappedLineIndenter = EditorView.updateListener.of((update) => {
const view = update.view
const charWidth = view.defaultCharacterWidth
const lineHeight = view.defaultLineHeight
const basePadding = 10
view.viewportLines((line) => {
const domAtPos = view.domAtPos(line.from)
const lineCount = (line.bottom - line.top) / lineHeight
if (lineCount <= 1) return
const belowPadding = basePadding * charWidth
const node = domAtPos.node as HTMLElement
node.style.textIndent = `-${belowPadding - charWidth + 1}px`
node.style.paddingLeft = `${belowPadding}px`
})
})
export const IndentedLineWrapPlugin = [
EditorView.lineWrapping,
WrappedLineIndenter,
]

View File

@@ -61,6 +61,8 @@ export const baseTheme = EditorView.theme({
},
".cm-panels.cm-panels-top": {
borderBottom: "1px solid var(--divider-light-color)",
top: "var(--lower-tertiary-sticky-fold) !important",
"z-index": "10",
},
".cm-panels.cm-panels-bottom": {
borderTop: "1px solid var(--divider-light-color)",
@@ -388,5 +390,7 @@ export const basicSetup: Extension = [
...completionKeymap,
...lintKeymap,
]),
search(),
search({
top: true,
}),
]

View File

@@ -1,3 +1,11 @@
/**
* Converts an array of key-value tuples (for e.g ["key", "value"]), into a record.
* (for eg. output -> { "key": "value" })
* NOTE: This function will discard duplicate key occurances and only keep the last occurance. If you do not want that behaviour,
* use `tupleWithSamesKeysToRecord`.
* @param tuples Array of tuples ([key, value])
* @returns A record with value corresponding to the last occurance of that key
*/
export const tupleToRecord = <
KeyType extends string | number | symbol,
ValueType
@@ -5,5 +13,32 @@ export const tupleToRecord = <
tuples: [KeyType, ValueType][]
): Record<KeyType, ValueType> =>
tuples.length > 0
? (Object.assign as any)(...tuples.map(([key, val]) => ({ [key]: val })))
? (Object.assign as any)(...tuples.map(([key, val]) => ({ [key]: val }))) // This is technically valid, but we have no way of telling TypeScript it is valid. Hence the assertion
: {}
/**
* Converts an array of key-value tuples (for e.g ["key", "value"]), into a record.
* (for eg. output -> { "key": ["value"] })
* NOTE: If you do not want the array as values (because of duplicate keys) and want to instead get the last occurance, use `tupleToRecord`
* @param tuples Array of tuples ([key, value])
* @returns A Record with values being arrays corresponding to each key occurance
*/
export const tupleWithSameKeysToRecord = <
KeyType extends string | number | symbol,
ValueType
>(
tuples: [KeyType, ValueType][]
): Record<KeyType, ValueType[]> => {
// By the end of the function we do ensure this typing, this can't be infered now though, hence the assertion
const out = {} as Record<KeyType, ValueType[]>
for (const [key, value] of tuples) {
if (!out[key]) {
out[key] = [value]
} else {
out[key].push(value)
}
}
return out
}

View File

@@ -56,6 +56,7 @@ export const bindings: {
"alt-q": "navigation.jump.graphql",
"alt-w": "navigation.jump.realtime",
"alt-d": "navigation.jump.documentation",
"alt-m": "navigation.jump.profile",
"alt-s": "navigation.jump.settings",
}

View File

@@ -1,4 +1,5 @@
import { Ref, ref } from "@nuxtjs/composition-api"
import { Ref } from "@nuxtjs/composition-api"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n, useToast } from "~/helpers/utils/composables"
@@ -8,13 +9,13 @@ export default function useCopyResponse(responseBodyText: Ref<any>): {
} {
const toast = useToast()
const t = useI18n()
const copyIcon = ref("copy")
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
const copyResponse = () => {
copyToClipboard(responseBodyText.value)
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
return {

View File

@@ -1,7 +1,8 @@
import * as S from "fp-ts/string"
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
import { pipe } from "fp-ts/function"
import { Ref, ref } from "@nuxtjs/composition-api"
import { Ref } from "@nuxtjs/composition-api"
import { refAutoReset } from "@vueuse/core"
import { useI18n, useToast } from "~/helpers/utils/composables"
export type downloadResponseReturnType = (() => void) | Ref<any>
@@ -13,7 +14,8 @@ export default function useDownloadResponse(
downloadIcon: Ref<string>
downloadResponse: () => void
} {
const downloadIcon = ref("download")
const downloadIcon = refAutoReset<"download" | "check">("download", 1000)
const toast = useToast()
const t = useI18n()
@@ -42,7 +44,6 @@ export default function useDownloadResponse(
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadIcon.value = "download"
}, 1000)
}
return {

View File

@@ -1,5 +1,6 @@
import * as E from "fp-ts/Either"
import { BehaviorSubject, Subscription } from "rxjs"
import { Subscription as WSubscription } from "wonka"
import { GQLError, runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
import {
GetUserShortcodesQuery,
@@ -22,6 +23,9 @@ export default class ShortcodeListAdapter {
private myShortcodesCreated: Subscription | null
private myShortcodesRevoked: Subscription | null
private myShortcodesCreatedSub: WSubscription | null
private myShortcodesRevokedSub: WSubscription | null
constructor(deferInit: boolean = false) {
this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
this.loading$ = new BehaviorSubject<boolean>(false)
@@ -33,6 +37,8 @@ export default class ShortcodeListAdapter {
this.isDispose = false
this.myShortcodesCreated = null
this.myShortcodesRevoked = null
this.myShortcodesCreatedSub = null
this.myShortcodesRevokedSub = null
if (!deferInit) this.initialize()
}
@@ -40,6 +46,8 @@ export default class ShortcodeListAdapter {
unsubscribeSubscriptions() {
this.myShortcodesCreated?.unsubscribe()
this.myShortcodesRevoked?.unsubscribe()
this.myShortcodesCreatedSub?.unsubscribe()
this.myShortcodesRevokedSub?.unsubscribe()
}
initialize() {
@@ -124,9 +132,12 @@ export default class ShortcodeListAdapter {
}
private registerSubscriptions() {
this.myShortcodesCreated = runGQLSubscription({
const [myShortcodeCreated$, myShortcodeCreatedSub] = runGQLSubscription({
query: ShortcodeCreatedDocument,
}).subscribe((result) => {
})
this.myShortcodesCreatedSub = myShortcodeCreatedSub
this.myShortcodesCreated = myShortcodeCreated$.subscribe((result) => {
if (E.isLeft(result)) {
console.error(result.left)
throw new Error(`Shortcode Create Error ${result.left}`)
@@ -135,9 +146,12 @@ export default class ShortcodeListAdapter {
this.createShortcode(result.right.myShortcodesCreated)
})
this.myShortcodesRevoked = runGQLSubscription({
const [myShortcodesRevoked$, myShortcodeRevokedSub] = runGQLSubscription({
query: ShortcodeDeletedDocument,
}).subscribe((result) => {
})
this.myShortcodesRevokedSub = myShortcodeRevokedSub
this.myShortcodesRevoked = myShortcodesRevoked$.subscribe((result) => {
if (E.isLeft(result)) {
console.error(result.left)
throw new Error(`Shortcode Delete Error ${result.left}`)

View File

@@ -103,7 +103,7 @@ export default [
label: "shortcut.navigation.settings",
},
{
keys: [getPlatformAlternateKey(), "P"],
keys: [getPlatformAlternateKey(), "M"],
label: "shortcut.navigation.profile",
},
],
@@ -171,7 +171,7 @@ export const spotlight = [
icon: "arrow-right",
},
{
keys: [getPlatformAlternateKey(), "P"],
keys: [getPlatformAlternateKey(), "M"],
label: "shortcut.navigation.profile",
action: "navigation.jump.profile",
icon: "arrow-right",
@@ -267,7 +267,7 @@ export const fuse = [
tags: ["settings", "jump", "page", "navigation", "account", "theme", "go"],
},
{
keys: [getPlatformAlternateKey(), "P"],
keys: [getPlatformAlternateKey(), "M"],
label: "shortcut.navigation.profile",
action: "navigation.jump.profile",
icon: "arrow-right",

View File

@@ -3,6 +3,7 @@ import { BehaviorSubject, Subscription } from "rxjs"
import { translateToNewRequest } from "@hoppscotch/data"
import pull from "lodash/pull"
import remove from "lodash/remove"
import { Subscription as WSubscription } from "wonka"
import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
import { TeamCollection } from "./TeamCollection"
import { TeamRequest } from "./TeamRequest"
@@ -193,6 +194,13 @@ export default class NewTeamCollectionAdapter {
private teamRequestUpdated$: Subscription | null
private teamRequestDeleted$: Subscription | null
private teamCollectionAddedSub: WSubscription | null
private teamCollectionUpdatedSub: WSubscription | null
private teamCollectionRemovedSub: WSubscription | null
private teamRequestAddedSub: WSubscription | null
private teamRequestUpdatedSub: WSubscription | null
private teamRequestDeletedSub: WSubscription | null
constructor(private teamID: string | null) {
this.collections$ = new BehaviorSubject<TeamCollection[]>([])
this.loadingCollections$ = new BehaviorSubject<string[]>([])
@@ -204,6 +212,13 @@ export default class NewTeamCollectionAdapter {
this.teamRequestDeleted$ = null
this.teamRequestUpdated$ = null
this.teamCollectionAddedSub = null
this.teamCollectionUpdatedSub = null
this.teamCollectionRemovedSub = null
this.teamRequestAddedSub = null
this.teamRequestDeletedSub = null
this.teamRequestUpdatedSub = null
if (this.teamID) this.initialize()
}
@@ -228,6 +243,13 @@ export default class NewTeamCollectionAdapter {
this.teamRequestAdded$?.unsubscribe()
this.teamRequestDeleted$?.unsubscribe()
this.teamRequestUpdated$?.unsubscribe()
this.teamCollectionAddedSub?.unsubscribe()
this.teamCollectionUpdatedSub?.unsubscribe()
this.teamCollectionRemovedSub?.unsubscribe()
this.teamRequestAddedSub?.unsubscribe()
this.teamRequestDeletedSub?.unsubscribe()
this.teamRequestUpdatedSub?.unsubscribe()
}
private async initialize() {
@@ -406,12 +428,16 @@ export default class NewTeamCollectionAdapter {
private registerSubscriptions() {
if (!this.teamID) return
this.teamCollectionAdded$ = runGQLSubscription({
const [teamCollAdded$, teamCollAddedSub] = runGQLSubscription({
query: TeamCollectionAddedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamCollectionAddedSub = teamCollAddedSub
this.teamCollectionAdded$ = teamCollAdded$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Collection Added Error: ${result.left}`)
@@ -426,12 +452,15 @@ export default class NewTeamCollectionAdapter {
)
})
this.teamCollectionUpdated$ = runGQLSubscription({
const [teamCollUpdated$, teamCollUpdatedSub] = runGQLSubscription({
query: TeamCollectionUpdatedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamCollectionUpdatedSub = teamCollUpdatedSub
this.teamCollectionUpdated$ = teamCollUpdated$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Collection Updated Error: ${result.left}`)
@@ -441,24 +470,30 @@ export default class NewTeamCollectionAdapter {
})
})
this.teamCollectionRemoved$ = runGQLSubscription({
const [teamCollRemoved$, teamCollRemovedSub] = runGQLSubscription({
query: TeamCollectionRemovedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamCollectionRemovedSub = teamCollRemovedSub
this.teamCollectionRemoved$ = teamCollRemoved$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Collection Removed Error: ${result.left}`)
this.removeCollection(result.right.teamCollectionRemoved)
})
this.teamRequestAdded$ = runGQLSubscription({
const [teamReqAdded$, teamReqAddedSub] = runGQLSubscription({
query: TeamRequestAddedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamRequestAddedSub = teamReqAddedSub
this.teamRequestAdded$ = teamReqAdded$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Request Added Error: ${result.left}`)
@@ -472,12 +507,15 @@ export default class NewTeamCollectionAdapter {
})
})
this.teamRequestUpdated$ = runGQLSubscription({
const [teamReqUpdated$, teamReqUpdatedSub] = runGQLSubscription({
query: TeamRequestUpdatedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamRequestUpdatedSub = teamReqUpdatedSub
this.teamRequestUpdated$ = teamReqUpdated$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Request Updated Error: ${result.left}`)
@@ -489,12 +527,15 @@ export default class NewTeamCollectionAdapter {
})
})
this.teamRequestDeleted$ = runGQLSubscription({
const [teamReqDeleted$, teamReqDeleted] = runGQLSubscription({
query: TeamRequestDeletedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamRequestUpdatedSub = teamReqDeleted
this.teamRequestDeleted$ = teamReqDeleted$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Request Deleted Error ${result.left}`)

View File

@@ -1,6 +1,10 @@
import * as A from "fp-ts/Array"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import * as S from "fp-ts/string"
import qs from "qs"
import { pipe } from "fp-ts/function"
import { flow, pipe } from "fp-ts/function"
import { combineLatest, Observable } from "rxjs"
import { map } from "rxjs/operators"
import {
@@ -9,14 +13,15 @@ import {
HoppRESTRequest,
parseTemplateString,
parseBodyEnvVariables,
parseRawKeyValueEntries,
Environment,
HoppRESTHeader,
HoppRESTParam,
parseRawKeyValueEntriesE,
parseTemplateStringE,
} from "@hoppscotch/data"
import { arrayFlatMap, arraySort } from "../functional/array"
import { toFormData } from "../functional/formData"
import { tupleToRecord } from "../functional/record"
import { tupleWithSameKeysToRecord } from "../functional/record"
import { getGlobalVariables } from "~/newstore/environments"
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
@@ -29,6 +34,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
effectiveFinalHeaders: { key: string; value: string }[]
effectiveFinalParams: { key: string; value: string }[]
effectiveFinalBody: FormData | string | null
effectiveFinalVars: { key: string; value: string }[]
}
/**
@@ -210,25 +216,40 @@ function getFinalBodyFromRequest(
}
if (request.body.contentType === "application/x-www-form-urlencoded") {
return pipe(
const parsedBodyRecord = pipe(
request.body.body,
parseRawKeyValueEntries,
parseRawKeyValueEntriesE,
E.map(
flow(
RA.toArray,
/**
* Filtering out empty keys and non-active pairs.
*/
A.filter(({ active, key }) => active && !S.isEmpty(key)),
// Filter out active
A.filter((x) => x.active),
// Convert to tuple
A.map(
({ key, value }) =>
[
parseTemplateString(key, envVariables),
parseTemplateString(value, envVariables),
] as [string, string]
),
// Tuple to Record object
tupleToRecord,
// Stringify
qs.stringify
/**
* Mapping each key-value to template-string-parser with either on array,
* which will be resolved in further steps.
*/
A.map(({ key, value }) => [
parseTemplateStringE(key, envVariables),
parseTemplateStringE(value, envVariables),
]),
/**
* Filtering and mapping only right-eithers for each key-value as [string, string].
*/
A.filterMap(([key, value]) =>
E.isRight(key) && E.isRight(value)
? O.some([key.right, value.right] as [string, string])
: O.none
),
tupleWithSameKeysToRecord,
(obj) => qs.stringify(obj, { indices: false })
)
)
)
return E.isRight(parsedBodyRecord) ? parsedBodyRecord.right : null
}
if (request.body.contentType === "multipart/form-data") {
@@ -298,15 +319,21 @@ export function getEffectiveRESTRequest(
value: parseTemplateString(x.value, envVariables),
}))
)
const effectiveFinalVars = request.vars
const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
return {
...request,
effectiveFinalURL: parseTemplateString(request.endpoint, envVariables),
effectiveFinalURL: parseTemplateString(
request.endpoint,
envVariables,
request.vars
),
effectiveFinalHeaders,
effectiveFinalParams,
effectiveFinalBody,
effectiveFinalVars,
}
}

View File

@@ -143,6 +143,15 @@ export function useStreamSubscriber(): {
}
}
export function useI18nPathInfo() {
const { localePath, getRouteBaseName } = useContext() as any
return {
localePath: localePath as (x: string) => string,
getRouteBaseName: getRouteBaseName as (x?: any) => string, // Should be a route
}
}
export function useI18n() {
const {
app: { i18n },

View File

@@ -14,6 +14,37 @@ export const knownContentTypes: Record<ValidContentTypes, Content> = {
"text/plain": "plain",
}
type ContentTypeTitle =
| "request.content_type_titles.text"
| "request.content_type_titles.structured"
| "request.content_type_titles.others"
type SegmentedContentType = {
title: ContentTypeTitle
contentTypes: ValidContentTypes[]
}
export const segmentedContentTypes: SegmentedContentType[] = [
{
title: "request.content_type_titles.text",
contentTypes: [
"application/json",
"application/ld+json",
"application/hal+json",
"application/vnd.api+json",
"application/xml",
],
},
{
title: "request.content_type_titles.structured",
contentTypes: ["application/x-www-form-urlencoded", "multipart/form-data"],
},
{
title: "request.content_type_titles.others",
contentTypes: ["text/html", "text/plain"],
},
]
export function isJSONContentType(contentType: string) {
return /\bjson\b/i.test(contentType)
}

View File

@@ -1,7 +1,7 @@
{
"action": {
"cancel": "取消",
"choose_file": "选择一个文件",
"choose_file": "选择文件",
"clear": "清除",
"clear_all": "全部清除",
"connect": "连接",
@@ -9,18 +9,18 @@
"delete": "删除",
"disconnect": "断开连接",
"dismiss": "忽略",
"dont_save": "Don't save",
"dont_save": "不保存",
"download_file": "下载文件",
"duplicate": "复制",
"edit": "编辑",
"go_back": "返回",
"label": "标签",
"learn_more": "了解更多",
"less": "Less",
"less": "更少",
"more": "更多",
"new": "新增",
"no": "否",
"paste": "Paste",
"paste": "粘贴",
"prettify": "美化",
"remove": "移除",
"restore": "恢复",
@@ -45,9 +45,9 @@
"chat_with_us": "与我们交谈",
"contact_us": "联系我们",
"copy": "复制",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
"copy_user_id": "复制认证 Token",
"developer_option": "开发者选项",
"developer_option_description": "开发者工具,有助于开发和维护 Hoppscotch",
"discord": "Discord",
"documentation": "帮助文档",
"github": "GitHub",
@@ -60,7 +60,7 @@
"keyboard_shortcuts": "键盘快捷键",
"name": "Hoppscotch",
"new_version_found": "已发现新版本。刷新页面以更新。",
"options": "Options",
"options": "选项",
"proxy_privacy_policy": "代理隐私政策",
"reload": "重新加载",
"search": "搜索",
@@ -68,7 +68,7 @@
"shortcuts": "快捷方式",
"spotlight": "聚光灯",
"status": "状态",
"status_description": "Check the status of the website",
"status_description": "检查网站状态",
"terms_and_privacy": "隐私条款",
"twitter": "Twitter",
"type_a_command_search": "输入命令或搜索内容……",
@@ -82,7 +82,7 @@
"continue_with_email": "使用电子邮箱登录",
"continue_with_github": "使用 GitHub 登录",
"continue_with_google": "使用 Google 登录",
"continue_with_microsoft": "Continue with Microsoft",
"continue_with_microsoft": "使用 Microsoft 登录",
"email": "电子邮箱地址",
"logged_out": "登出",
"login": "登录",
@@ -106,32 +106,32 @@
"username": "用户名"
},
"collection": {
"created": "合已创建",
"edit": "编辑合",
"invalid_name": "请提供有效的合名称",
"my_collections": "我的合",
"name": "我的新合",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "新建合",
"renamed": "合已更名",
"request_in_use": "Request in use",
"created": "合已创建",
"edit": "编辑合",
"invalid_name": "请提供有效的合名称",
"my_collections": "我的合",
"name": "我的新合",
"name_length_insufficient": "集合名字至少需要 3 个字符",
"new": "新建合",
"renamed": "合已更名",
"request_in_use": "请求正在使用中",
"save_as": "另存为",
"select": "选择一个合",
"select": "选择一个合",
"select_location": "选择位置",
"select_team": "选择一个团队",
"team_collections": "团队合"
"team_collections": "团队合"
},
"confirm": {
"exit_team": "你确定要离开此团队吗?",
"logout": "你确定要登出吗?",
"remove_collection": "你确定要永久删除该合吗?",
"remove_collection": "你确定要永久删除该合吗?",
"remove_environment": "你确定要永久删除该环境吗?",
"remove_folder": "你确定要永久删除该文件夹吗?",
"remove_history": "你确定要永久删除全部历史记录吗?",
"remove_request": "你确定要永久删除该请求吗?",
"remove_team": "你确定要删除该团队吗?",
"remove_telemetry": "你确定要退出遥测服务吗?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"request_change": "你确定你要放弃当前的请求,未保存的修改将被丢失。",
"sync": "您确定要同步该工作区吗?"
},
"count": {
@@ -144,13 +144,13 @@
},
"documentation": {
"generate": "生成文档",
"generate_message": "导入 Hoppscotch 合以随时随地生成 API 文档。"
"generate_message": "导入 Hoppscotch 合以随时随地生成 API 文档。"
},
"empty": {
"authorization": "该请求没有使用任何授权",
"body": "该请求没有任何请求体",
"collection": "合为空",
"collections": "合为空",
"collection": "合为空",
"collections": "合为空",
"documentation": "连接至 GraphQL 端点以查看文档",
"endpoint": "端点不能为空",
"environments": "环境为空",
@@ -169,20 +169,20 @@
"tests": "没有针对该请求的测试"
},
"environment": {
"add_to_global": "Add to Global",
"added": "Environment addition",
"create_new": "创建新环境",
"created": "Environment created",
"deleted": "Environment deletion",
"add_to_global": "添加到全局环境",
"added": "环境已添加",
"create_new": "创建新环境",
"created": "环境已创建",
"deleted": "环境已删除",
"edit": "编辑环境",
"invalid_name": "请提供有效的环境名称",
"nested_overflow": "nested environment variables are limited to 10 levels",
"nested_overflow": "环境嵌套深度超过限制10层",
"new": "新建环境",
"no_environment": "无环境",
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
"no_environment_description": "没有选择环境。选择如何处理以下变量。",
"select": "选择环境",
"title": "环境",
"updated": "Environment updation",
"updated": "环境已更新",
"variable_list": "变量列表"
},
"error": {
@@ -190,9 +190,9 @@
"check_console_details": "检查控制台日志以获悉详情",
"curl_invalid_format": "cURL 格式不正确",
"empty_req_name": "空请求名称",
"f12_details": "(F12 详情)",
"f12_details": "F12 详情",
"gql_prettify_invalid_query": "无法美化无效的查询,处理查询语法错误并重试",
"incomplete_config_urls": "Incomplete configuration URLs",
"incomplete_config_urls": "配置文件中的 URL 无效",
"incorrect_email": "电子邮箱错误",
"invalid_link": "无效链接",
"invalid_link_description": "你点击的链接无效或已过期。",
@@ -202,7 +202,7 @@
"no_duration": "无持续时间",
"script_fail": "无法执行预请求脚本",
"something_went_wrong": "发生了一些错误",
"test_script_fail": "Could not execute post-request script"
"test_script_fail": "无法执行请求脚本"
},
"export": {
"as_json": "导出为 JSON",
@@ -215,7 +215,7 @@
"created": "已创建文件夹",
"edit": "编辑文件夹",
"invalid_name": "请提供文件夹的名称",
"name_length_insufficient": "Folder name should be at least 3 characters long",
"name_length_insufficient": "文件夹名称应至少为 3 个字符",
"new": "新文件夹",
"renamed": "文件夹已更名"
},
@@ -238,46 +238,46 @@
"post_request_tests": "测试脚本使用 JavaScript 编写,并在收到响应后执行。",
"pre_request_script": "预请求脚本使用 JavaScript 编写,并在请求发送前执行。",
"script_fail": "预请求脚本中似乎存在故障。 检查下面的错误并相应地修复脚本。",
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
"test_script_fail": "测试脚本似乎有一个错误。请修复错误并再次运行测试",
"tests": "编写测试脚本以自动调试。"
},
"hide": {
"collection": "Collapse Collection Panel",
"collection": "隐藏集合",
"more": "隐藏更多",
"preview": "隐藏预览",
"sidebar": "隐藏侧边栏"
},
"import": {
"collections": "导入合",
"collections": "导入合",
"curl": "导入 cURL",
"failed": "导入失败",
"from_gist": "从 Gist 导入",
"from_gist_description": "Import from Gist URL",
"from_insomnia": "Import from Insomnia",
"from_insomnia_description": "Import from Insomnia collection",
"from_json": "Import from Hoppscotch",
"from_json_description": "Import from Hoppscotch collection file",
"from_my_collections": "从我的合导入",
"from_my_collections_description": "Import from My Collections file",
"from_openapi": "Import from OpenAPI",
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection",
"from_url": "Import from URL",
"from_gist_description": " Gist URL 导入",
"from_insomnia": " Insomnia 导入",
"from_insomnia_description": "从 Insomnia 集合中导入",
"from_json": " Hoppscotch 导入",
"from_json_description": " Hoppscotch 集合中导入",
"from_my_collections": "从我的合导入",
"from_my_collections_description": "从我的集合文件导入",
"from_openapi": " OpenAPI 导入",
"from_openapi_description": "从 OpenAPI 文件导入(YML/JSON",
"from_postman": " Postman 导入",
"from_postman_description": "从 Postman 集合中导入",
"from_url": " URL 导入",
"gist_url": "输入 Gist URL",
"json_description": "Import collections from a Hoppscotch Collections JSON file",
"json_description": "从 Hoppscotch 的集合文件导入JSON",
"title": "导入"
},
"layout": {
"collapse_collection": "Collapse or Expand Collections",
"collapse_sidebar": "Collapse or Expand the sidebar",
"collapse_collection": "折叠/展开集合",
"collapse_sidebar": "折叠/展开边栏",
"column": "垂直布局",
"name": "Layout",
"name": "布局",
"row": "水平布局",
"zen_mode": "禅意模式"
"zen_mode": "ZEN 模式"
},
"modal": {
"collections": "合",
"collections": "合",
"confirm": "确认",
"edit_request": "编辑请求",
"import_export": "导入/导出"
@@ -315,12 +315,12 @@
"email_verification_mail": "确认邮件已发送至你的邮箱,请点击链接以验证你的电子邮箱。",
"no_permission": "你无权执行此操作。",
"owner": "所有者",
"owner_description": "所有者可以添加、编辑和删除请求、合及团队成员。",
"owner_description": "所有者可以添加、编辑和删除请求、合及团队成员。",
"roles": "角色",
"roles_description": "角色用以控制共享合的访问权限。",
"roles_description": "角色用以控制共享合的访问权限。",
"updated": "档案已更新",
"viewer": "阅览者",
"viewer_description": "阅览者只可查看与使用请求。"
"viewer": "查看者",
"viewer_description": "查看者只可查看与使用请求。"
},
"remove": {
"star": "移除星标"
@@ -340,10 +340,10 @@
"invalid_name": "请提供请求名称",
"method": "方法",
"name": "请求名称",
"new": "New Request",
"override": "Override",
"override_help": "Set <xmp>Content-Type</xmp> in Headers",
"overriden": "Overridden",
"new": "新请求",
"override": "覆盖",
"override_help": "设置 <xmp>Content-Type</xmp> ",
"overriden": "覆盖",
"parameter_list": "查询参数",
"parameters": "参数",
"path": "路径",
@@ -356,7 +356,7 @@
"save_as": "另存为",
"saved": "请求已保存",
"share": "分享",
"share_description": "Share Hoppscotch with your friends",
"share_description": "分享 Hoppscotch 给你的朋友",
"title": "请求",
"type": "请求类型",
"url": "URL",
@@ -396,7 +396,7 @@
"extension_version": "扩展版本",
"extensions": "扩展",
"extensions_use_toggle": "使用浏览器扩展发送请求(如果存在)",
"follow": "Follow Us",
"follow": "关注我们",
"font_size": "字体大小",
"font_size_large": "大",
"font_size_medium": "中",
@@ -417,7 +417,7 @@
"reset_default": "重置为默认",
"sidebar_on_left": "侧边栏移至左侧",
"sync": "同步",
"sync_collections": "合",
"sync_collections": "合",
"sync_description": "这些设置会同步到云。",
"sync_environments": "环境",
"sync_history": "历史",
@@ -464,21 +464,21 @@
"previous_method": "选择上一个方法",
"put_method": "选择 PUT 方法",
"reset_request": "重置请求",
"save_to_collections": "保存到合",
"save_to_collections": "保存到合",
"send_request": "发送请求",
"title": "请求"
},
"theme": {
"black": "Switch theme to black mode",
"dark": "Switch theme to dark mode",
"light": "Switch theme to light mode",
"system": "Switch theme to system mode",
"title": "Theme"
"black": "切换为黑色主题",
"dark": "切换为深色主题",
"light": "切换为浅色主题",
"system": "切换为系统主题",
"title": "主题"
}
},
"show": {
"code": "显示代码",
"collection": "Expand Collection Panel",
"collection": "展开集合",
"more": "显示更多",
"sidebar": "显示侧边栏"
},
@@ -525,7 +525,7 @@
"community": "提问与互助",
"documentation": "阅读更多有关 Hoppscotch 的内容",
"forum": "答疑解惑",
"github": "Follow us on Github",
"github": " Github 关注我们",
"shortcuts": "更快浏览应用",
"team": "与团队保持联系",
"title": "支持",
@@ -534,7 +534,7 @@
"tab": {
"authorization": "授权",
"body": "请求体",
"collections": "合",
"collections": "合",
"documentation": "帮助文档",
"headers": "请求头",
"history": "历史记录",
@@ -552,18 +552,18 @@
"websocket": "WebSocket"
},
"team": {
"already_member": "你已经是此团队的成员。请联系你的团队所有人。",
"already_member": "你已经是此团队的成员。请联系你的团队。",
"create_new": "创建新团队",
"deleted": "团队已删除",
"edit": "编辑团队",
"email": "电子邮箱",
"email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队所有人。",
"email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队。",
"exit": "退出团队",
"exit_disabled": "团队所有者无法退出团队",
"invalid_email_format": "电子邮箱格式无效",
"invalid_id": "无效的团队 ID请联系你的团队所有人。",
"invalid_id": "无效的团队 ID请联系你的团队。",
"invalid_invite_link": "无效的邀请链接",
"invalid_invite_link_description": "你点击的链接无效。请联系你的团队所有人。",
"invalid_invite_link_description": "你点击的链接无效。请联系你的团队。",
"invalid_member_permission": "请为团队成员提供有效的权限",
"invite": "邀请",
"invite_more": "邀请更多成员",
@@ -578,8 +578,8 @@
"login_to_continue": "登录以继续",
"login_to_continue_description": "你需要登录以加入团队",
"logout_and_try_again": "登出并以其他帐户登录",
"member_has_invite": "此邮箱 ID 已有邀请。请联系你的团队所有人。",
"member_not_found": "未找到成员。请联系你的团队所有人。",
"member_has_invite": "此邮箱 ID 已有邀请。请联系你的团队。",
"member_not_found": "未找到成员。请联系你的团队。",
"member_removed": "用户已移除",
"member_role_updated": "用户角色已更新",
"members": "成员",
@@ -588,10 +588,10 @@
"new": "新团队",
"new_created": "已创建新团队",
"new_name": "我的新团队",
"no_access": "你没有编辑合的权限",
"no_invite_found": "未找到邀请。请联系你的团队所有人。",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "你不是有效的阅览者。请联系你的团队所有人。",
"no_access": "你没有编辑合的权限",
"no_invite_found": "未找到邀请。请联系你的团队。",
"not_found": "没有找到团队,请联系您的团队所有者。",
"not_valid_viewer": "你不是有效的查看者。请联系你的团队。",
"pending_invites": "待办邀请",
"permissions": "权限",
"saved": "团队已保存",

View File

@@ -14,6 +14,7 @@
"download_file": "Download file",
"duplicate": "Duplicate",
"edit": "Edit",
"filter_response": "Filter response",
"go_back": "Go back",
"label": "Label",
"learn_more": "Learn more",
@@ -202,9 +203,11 @@
"invalid_link": "Invalid link",
"invalid_link_description": "The link you clicked is invalid or expired.",
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
"json_parsing_failed": "Invalid JSON",
"network_error": "There seems to be a network error. Please try again.",
"network_fail": "Could not send request",
"no_duration": "No duration",
"no_results_found": "No matches found",
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script"
@@ -340,6 +343,11 @@
"body": "Request Body",
"choose_language": "Choose language",
"content_type": "Content Type",
"content_type_titles": {
"others": "Others",
"structured": "Structured",
"text": "Text"
},
"copy_link": "Copy link",
"duration": "Duration",
"enter_curl": "Enter cURL",
@@ -374,6 +382,7 @@
},
"response": {
"body": "Response Body",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Headers",
"html": "HTML",
"image": "Image",

View File

@@ -2,16 +2,16 @@
"action": {
"cancel": "Cancelar",
"choose_file": "Escolha um arquivo",
"clear": "Claro",
"clear": "Limpar",
"clear_all": "Limpar tudo",
"connect": "Conectar",
"copy": "Copiar",
"delete": "Excluir",
"disconnect": "desconectar",
"disconnect": "Desconectar",
"dismiss": "Dispensar",
"dont_save": "Don't save",
"dont_save": "Não Salvar",
"download_file": "⇬ Fazer download do arquivo",
"duplicate": "Duplicate",
"duplicate": "Duplicar",
"edit": "Editar",
"go_back": "Voltar",
"label": "Etiqueta",
@@ -35,7 +35,7 @@
"turn_off": "Desligar",
"turn_on": "Ligar",
"undo": "Desfazer",
"yes": "sim"
"yes": "Sim"
},
"add": {
"new": "Adicionar novo",
@@ -45,9 +45,9 @@
"chat_with_us": "Converse conosco",
"contact_us": "Contate-Nos",
"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 autenticação do usuário",
"developer_option": "Opções de desenvolvedor",
"developer_option_description": "Opções de desenvolvedor que ajudam no desenvolvimento e manutenção do Hoppscotch.",
"discord": "Discord",
"documentation": "Documentação",
"github": "GitHub",
@@ -60,18 +60,18 @@
"keyboard_shortcuts": "Atalhos do teclado",
"name": "Hoppscotch",
"new_version_found": "Nova versão encontrada. Atualize para atualizar.",
"options": "Options",
"options": "Opções",
"proxy_privacy_policy": "Política de privacidade do proxy",
"reload": "recarregar",
"reload": "Recarregar",
"search": "Procurar",
"share": "Compartilhado",
"shortcuts": "Atalhos",
"spotlight": "Holofote",
"status": "Status",
"status_description": "Check the status of the website",
"status": "Estado",
"status_description": "Cheque o estado do website.",
"terms_and_privacy": "Termos e privacidade",
"twitter": "Twitter",
"type_a_command_search": "Digite um comando ou pesquise ...",
"type_a_command_search": "Digite um comando ou pesquise...",
"we_use_cookies": "Usamos cookies",
"whats_new": "O que há de novo?",
"wiki": "Wiki"
@@ -114,7 +114,7 @@
"name_length_insufficient": "O nome da coleção deve ter pelo menos 3 caracteres",
"new": "Nova coleção",
"renamed": "Coleção renomeada",
"request_in_use": "Request in use",
"request_in_use": "Requisição em uso",
"save_as": "Salvar como",
"select": "Selecione uma coleção",
"select_location": "Selecione a localização",
@@ -131,7 +131,7 @@
"remove_request": "Tem certeza de que deseja excluir permanentemente esta solicitação?",
"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.",
"request_change": "Tem certeza que deseja descartar a requisição atual? Alterações não salvas serão perdidas.",
"sync": "Tem certeza de que deseja sincronizar este espaço de trabalho?"
},
"count": {
@@ -151,8 +151,8 @@
"body": "Este pedido não tem corpo",
"collection": "Coleção está vazia",
"collections": "Coleções estão vazias",
"documentation": "Connect to a GraphQL endpoint to view documentation",
"endpoint": "Endpoint cannot be empty",
"documentation": "Se conecte à um endpoint GraphQL para ver a documentação",
"endpoint": "O endpoint não pode ser vazio",
"environments": "Ambientes estão vazios",
"folder": "Pasta está vazia",
"headers": "Esta solicitação não possui cabeçalhos",
@@ -172,11 +172,11 @@
"add_to_global": "Adicionar ao Global",
"added": "Adição de ambiente",
"create_new": "Crie um novo ambiente",
"created": "Environment created",
"created": "Ambiente criado",
"deleted": "Deleção de ambiente",
"edit": "Editar Ambiente",
"invalid_name": "Forneça um nome válido para o ambiente",
"nested_overflow": "variáveis de ambiente aninhadas são limitadas a 10 níveis",
"nested_overflow": "Variáveis de ambiente aninhadas são limitadas a 10 níveis",
"new": "Novo ambiente",
"no_environment": "Sem ambiente",
"no_environment_description": "Nenhum ambiente foi selecionado. Escolha o que fazer com as seguintes variáveis.",
@@ -195,9 +195,9 @@
"incomplete_config_urls": "URLs de configuração incompletas",
"incorrect_email": "Email incorreto",
"invalid_link": "Link inválido",
"invalid_link_description": "The link you clicked is invalid or expired.",
"invalid_link_description": "O link que você clicou é inválido ou já expirou.",
"json_prettify_invalid_body": "Não foi possível embelezar um corpo inválido, resolver erros de sintaxe json e tentar novamente",
"network_error": "There seems to be a network error. Please try again.",
"network_error": "Parece que houve um problema de rede. Por favor, tente novamente.",
"network_fail": "Não foi possível enviar requisição",
"no_duration": "Sem duração",
"script_fail": "Não foi possível executar o script pré-requisição",
@@ -252,25 +252,25 @@
"curl": "Importar cURL",
"failed": "A importação falhou",
"from_gist": "Importar do Gist",
"from_gist_description": "Import from Gist URL",
"from_insomnia": "Import from Insomnia",
"from_insomnia_description": "Import from Insomnia collection",
"from_json": "Import from Hoppscotch",
"from_json_description": "Import from Hoppscotch collection file",
"from_gist_description": "Importar de URL Gist",
"from_insomnia": "Importar de Insomnia",
"from_insomnia_description": "Importa de coleção Insomnia",
"from_json": "Importar de Hoppscotch",
"from_json_description": "Importa de arquivo de coleção Hoppscotch",
"from_my_collections": "Importar de minhas coleções",
"from_my_collections_description": "Import from My Collections file",
"from_openapi": "Import from OpenAPI",
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection",
"from_url": "Import from URL",
"gist_url": "Insira o URL da essência",
"json_description": "Import collections from a Hoppscotch Collections JSON file",
"from_my_collections_description": "Importa de arquivo Minhas Coleções",
"from_openapi": "Importar de OpenAPI",
"from_openapi_description": "Importa de arquivo de especificação OpenAPI (YML/JSON)",
"from_postman": "Importar de Postman",
"from_postman_description": "Importa de coleção Postman",
"from_url": "Importar de URL",
"gist_url": "Insira o URL do Gist",
"json_description": "Importa coleções de um arquivo JSON de Coleções Hoppscotch",
"title": "Importar"
},
"layout": {
"collapse_collection": "Collapse or Expand Collections",
"collapse_sidebar": "Collapse or Expand the sidebar",
"collapse_collection": "Encolher ou expandir coleções",
"collapse_sidebar": "Encolher ou Expandir a barra lateral",
"column": "Layout vertical",
"name": "Layout",
"row": "Layout horizontal",
@@ -311,16 +311,16 @@
"profile": {
"app_settings": "App Settings",
"editor": "Editor",
"editor_description": "Editors can add, edit, and delete requests.",
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
"no_permission": "You do not have permission to perform this action.",
"owner": "Owner",
"owner_description": "Owners can add, edit, and delete requests, collections and team members.",
"roles": "Roles",
"roles_description": "Roles are used to control access to the shared collections.",
"updated": "Profile updated",
"viewer": "Viewer",
"viewer_description": "Viewers can only view and use requests."
"editor_description": "Editores podem adicionar, editar e deletar requisições.",
"email_verification_mail": "Um e-mail de verificação foi enviado ao seu endereço de e-mail. Por favor, clique no link para verificar seu endereço e-mail.",
"no_permission": "Você não tem permissão para realizar esta ação.",
"owner": "Dono",
"owner_description": "Donos podem adicionar, editar e deletar requisições, coleções e membros de equipe.",
"roles": "Funções",
"roles_description": "Funções são utilizadas para gerenciar acesso às coleções compartilhadas.",
"updated": "Perfil atualizado",
"viewer": "Espectador",
"viewer_description": "Espectadores só podem ver e usar requisições."
},
"remove": {
"star": "Remover estrela"
@@ -340,10 +340,10 @@
"invalid_name": "Forneça um nome para a requisição",
"method": "Método",
"name": "Nome da requisição",
"new": "New Request",
"override": "Override",
"override_help": "Set <xmp>Content-Type</xmp> in Headers",
"overriden": "Overridden",
"new": "Nova requisição",
"override": "Substituir",
"override_help": "Substituir <xmp>Content-Type</xmp> em Headers",
"overriden": "Substituído",
"parameter_list": "Parâmetros da requisição",
"parameters": "Parâmetros",
"path": "Caminho",
@@ -356,7 +356,7 @@
"save_as": "Salvar como",
"saved": "Requisição salva",
"share": "Compartilhadar",
"share_description": "Share Hoppscotch with your friends",
"share_description": "Compartilhe o Hoppscotch com seus amigos",
"title": "Solicitar",
"type": "Tipo de requisição",
"url": "URL",
@@ -396,7 +396,7 @@
"extension_version": "Versão da extensão",
"extensions": "Extensões",
"extensions_use_toggle": "Use a extensão do navegador para enviar solicitações (se houver)",
"follow": "Follow Us",
"follow": "Nos siga",
"font_size": "Tamanho da fonte",
"font_size_large": "Grande",
"font_size_medium": "Médio",
@@ -407,7 +407,7 @@
"light_mode": "Luz",
"official_proxy_hosting": "Official Proxy é hospedado por Hoppscotch.",
"profile": "Perfil",
"profile_description": "Update your profile details",
"profile_description": "Atualize os detalhes de seu perfil",
"profile_email": "Endereço de email",
"profile_name": "Nome do perfil",
"proxy": "Proxy",

View File

@@ -1,5 +1,6 @@
{
"action": {
"autoscroll": "自動捲動",
"cancel": "取消",
"choose_file": "選擇一個檔案",
"clear": "清除",
@@ -9,10 +10,11 @@
"delete": "刪除",
"disconnect": "斷開連線",
"dismiss": "忽略",
"download_file": "下載檔案",
"dont_save": "不要儲存",
"download_file": "下載檔案",
"duplicate": "複製",
"edit": "編輯",
"filter_response": "篩選回應",
"go_back": "返回",
"label": "標籤",
"learn_more": "瞭解更多",
@@ -20,11 +22,14 @@
"more": "更多",
"new": "新增",
"no": "否",
"open_workspace": "開啟工作區",
"paste": "貼上",
"prettify": "美化",
"remove": "移除",
"restore": "還原",
"save": "儲存",
"scroll_to_bottom": "捲動至底部",
"scroll_to_top": "捲動至頂部",
"search": "搜尋",
"send": "傳送",
"start": "開始",
@@ -46,10 +51,10 @@
"contact_us": "聯絡我們",
"copy": "複製",
"copy_user_id": "複製使用者驗證權杖",
"discord": "Discord",
"documentation": "幫助文件",
"developer_option": "開發者選項",
"developer_option_description": "協助開發和維護 Hoppscotch 的工具。",
"discord": "Discord",
"documentation": "幫助文件",
"github": "GitHub",
"help": "幫助與回饋",
"home": "主頁",
@@ -164,6 +169,7 @@
"profile": "登入以檢視您的設定檔",
"protocols": "協議為空",
"schema": "連線至 GraphQL 端點",
"shortcodes": "Shortcodes 為空",
"team_name": "團隊名稱為空",
"teams": "團隊為空",
"tests": "沒有針對該請求的測試"
@@ -197,9 +203,11 @@
"invalid_link": "連結無效",
"invalid_link_description": "您點擊的連結無效或已過期。",
"json_prettify_invalid_body": "無法美化無效的請求主體,處理 JSON 語法錯誤並重試",
"json_parsing_failed": "JSON 無效",
"network_error": "似乎有網路錯誤。請再試一次。",
"network_fail": "無法傳送請求",
"no_duration": "無持續時間",
"no_results_found": "找不到結果",
"script_fail": "無法執行預請求指令碼",
"something_went_wrong": "發生了一些錯誤",
"test_script_fail": "無法執行測試指令碼"
@@ -266,15 +274,19 @@
"from_url": "從網址匯入",
"gist_url": "輸入 Gist 網址",
"json_description": "從 Hoppscotch 組合 JSON 檔匯入組合",
"title": "匯入"
"title": "匯入",
"import_from_url_success": "已匯入組合",
"import_from_url_invalid_file_format": "匯入組合時發生錯誤",
"import_from_url_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'",
"import_from_url_invalid_fetch": "無法從網址取得資料"
},
"layout": {
"column": "垂直布局",
"row": "水平布局",
"zen_mode": "專注模式",
"collapse_sidebar": "隱藏或顯示側邊欄",
"collapse_collection": "隱藏或顯示組合",
"name": "配置"
"collapse_sidebar": "隱藏或顯示側邊欄",
"column": "垂直布局",
"name": "配置",
"row": "水平布局",
"zen_mode": "專注模式"
},
"modal": {
"collections": "組合",
@@ -331,6 +343,11 @@
"body": "請求本體",
"choose_language": "選擇語言",
"content_type": "內容類型",
"content_type_titles": {
"others": "其他",
"structured": "結構",
"text": "文字"
},
"copy_link": "複製連結",
"duration": "持續時間",
"enter_curl": "輸入 cURL",
@@ -341,6 +358,9 @@
"method": "方法",
"name": "請求名稱",
"new": "新請求",
"override": "覆寫",
"override_help": "在標頭設置 <xmp>Content-Type</xmp>",
"overriden": "已覆寫",
"parameter_list": "查詢參數",
"parameters": "參數",
"path": "路徑",
@@ -358,12 +378,11 @@
"type": "請求類型",
"url": "網址",
"variables": "變數",
"override": "覆寫",
"override_help": "在標頭設置 <xmp>Content-Type</xmp>",
"overriden": "已覆寫"
"view_my_links": "檢視我的連結"
},
"response": {
"body": "回應本體",
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
"headers": "回應標頭",
"html": "HTML",
"image": "影像",
@@ -415,6 +434,8 @@
"proxy_use_toggle": "使用 Proxy 中介軟體傳送請求",
"read_the": "閱讀",
"reset_default": "重置為預設",
"short_codes": "快捷碼",
"short_codes_description": "我們為您打造的快捷碼。",
"sidebar_on_left": "左側邊欄",
"sync": "同步",
"sync_collections": "組合",
@@ -447,7 +468,7 @@
"documentation": "前往文件頁面",
"forward": "前往下一頁面",
"graphql": "前往 GraphQL 頁面",
"profile": "Go to Profile page",
"profile": "前往個人檔案頁面",
"realtime": "前往實時頁面",
"rest": "前往 REST 頁面",
"settings": "前往設定頁面",
@@ -476,6 +497,15 @@
"title": "主題"
}
},
"shortcodes":{
"actions":"操作",
"created_on": "建立於",
"deleted" : "已刪除快捷碼",
"method": "方法",
"not_found":"找不到快捷碼",
"short_code":"快捷碼",
"url": "網址"
},
"show": {
"code": "顯示程式碼",
"more": "顯示更多",
@@ -487,7 +517,8 @@
"event_name": "事件名稱",
"events": "事件",
"log": "日誌",
"url": "網址"
"url": "網址",
"connection_not_authorized": "此 SocketIO 連線未使用任何驗證。"
},
"sse": {
"event_type": "事件類型",
@@ -517,7 +548,19 @@
"loading": "正在載入……",
"none": "無",
"nothing_found": "沒有找到",
"waiting_send_request": "等待傳送請求"
"waiting_send_request": "等待傳送請求",
"subscribed_success": "成功訂閱此主題:{topic}",
"unsubscribed_success": "成功取消訂閱此主題:{topic}",
"subscribed_failed": "無法訂閱此主題:{topic}",
"unsubscribed_failed": "無法取消訂閱此主題:{topic}",
"published_message": "已將此訊息:{message} 發布至主題:{topic}",
"published_error": "將訊息:{topic} 發布至主題:{message} 時發生錯誤",
"message_received": "訊息:{message}已抵達主題:{topic}",
"mqtt_subscription_failed": "訂閱此主題時發生錯誤:{topic}",
"connection_lost": "失去連線",
"connection_failed": "連線失敗",
"connection_error": "連線失敗",
"reconnection_error": "重新連線失敗"
},
"support": {
"changelog": "閱讀更多有關最新版本的內容",

View File

@@ -4,6 +4,7 @@ import {
FormDataKeyValue,
HoppRESTHeader,
HoppRESTParam,
HoppRESTVar,
HoppRESTReqBody,
HoppRESTRequest,
RESTReqSchemaVersion,
@@ -29,6 +30,7 @@ export const getDefaultRESTRequest = (): HoppRESTRequest => ({
endpoint: "https://echo.hoppscotch.io",
name: "Untitled request",
params: [],
vars: [],
headers: [],
method: "GET",
auth: {
@@ -80,6 +82,14 @@ const dispatchers = defineDispatchers({
},
}
},
setVars(curr: RESTSession, { entries }: { entries: HoppRESTVar[] }) {
return {
request: {
...curr.request,
vars: entries,
},
}
},
addParam(curr: RESTSession, { newParam }: { newParam: HoppRESTParam }) {
return {
request: {
@@ -88,6 +98,14 @@ const dispatchers = defineDispatchers({
},
}
},
addVar(curr: RESTSession, { newVar }: { newVar: HoppRESTVar }) {
return {
request: {
...curr.request,
vars: [...curr.request.vars, newVar],
},
}
},
updateParam(
curr: RESTSession,
{ index, updatedParam }: { index: number; updatedParam: HoppRESTParam }
@@ -104,6 +122,22 @@ const dispatchers = defineDispatchers({
},
}
},
updateVar(
curr: RESTSession,
{ index, updatedVar }: { index: number; updatedVar: HoppRESTVar }
) {
const newVars = curr.request.vars.map((vari, i) => {
if (i === index) return updatedVar
else return vari
})
return {
request: {
...curr.request,
vars: newVars,
},
}
},
deleteParam(curr: RESTSession, { index }: { index: number }) {
const newParams = curr.request.params.filter((_x, i) => i !== index)
@@ -114,6 +148,16 @@ const dispatchers = defineDispatchers({
},
}
},
deleteVar(curr: RESTSession, { index }: { index: number }) {
const newVars = curr.request.vars.filter((_x, i) => i !== index)
return {
request: {
...curr.request,
vars: newVars,
},
}
},
deleteAllParams(curr: RESTSession) {
return {
request: {
@@ -373,6 +417,14 @@ export function setRESTParams(entries: HoppRESTParam[]) {
},
})
}
export function setRESTVars(entries: HoppRESTVar[]) {
restSessionStore.dispatch({
dispatcher: "setVars",
payload: {
entries,
},
})
}
export function addRESTParam(newParam: HoppRESTParam) {
restSessionStore.dispatch({
@@ -382,6 +434,14 @@ export function addRESTParam(newParam: HoppRESTParam) {
},
})
}
export function addRESTVar(newVar: HoppRESTVar) {
restSessionStore.dispatch({
dispatcher: "addVar",
payload: {
newVar,
},
})
}
export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
restSessionStore.dispatch({
@@ -392,6 +452,15 @@ export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
},
})
}
export function updateRESTVar(index: number, updatedVar: HoppRESTVar) {
restSessionStore.dispatch({
dispatcher: "updateVar",
payload: {
updatedVar,
index,
},
})
}
export function deleteRESTParam(index: number) {
restSessionStore.dispatch({
@@ -402,6 +471,15 @@ export function deleteRESTParam(index: number) {
})
}
export function deleteRESTVar(index: number) {
restSessionStore.dispatch({
dispatcher: "deleteVar",
payload: {
index,
},
})
}
export function deleteAllRESTParams() {
restSessionStore.dispatch({
dispatcher: "deleteAllParams",
@@ -592,12 +670,20 @@ export const restParams$ = restSessionStore.subject$.pipe(
distinctUntilChanged()
)
export const restVars$ = restSessionStore.subject$.pipe(
pluck("request", "vars"),
distinctUntilChanged()
)
export const restActiveParamsCount$ = restParams$.pipe(
map(
(params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
)
)
export const restActiveVarsCount$ = restVars$.pipe(
map((vars) => vars.filter((x) => x.key !== "" || x.value !== "").length)
)
export const restMethod$ = restSessionStore.subject$.pipe(
pluck("request", "method"),

View File

@@ -540,6 +540,6 @@ export function updateEnvironmentVariable(
})
}
export function getEnviroment(index: number) {
export function getEnvironment(index: number) {
return environmentsStore.value.environments[index]
}

View File

@@ -315,6 +315,7 @@ completedRESTResponse$.subscribe((res) => {
method: res.req.method,
name: res.req.name,
params: res.req.params,
vars: res.req.vars,
preRequestScript: res.req.preRequestScript,
testScript: res.req.testScript,
v: res.req.v,

View File

@@ -132,7 +132,7 @@ export default {
// https://github.com/nuxt/typescript
["@nuxt/typescript-build", { typeCheck: false }],
// https://github.com/nuxt-community/dotenv-module
"@nuxtjs/dotenv",
["@nuxtjs/dotenv", { systemvars: true }],
// https://github.com/nuxt-community/composition-api
"@nuxtjs/composition-api/module",
"~/modules/emit-volar-types.ts",
@@ -339,6 +339,8 @@ export default {
APP_ID: process.env.APP_ID,
MEASUREMENT_ID: process.env.MEASUREMENT_ID,
BASE_URL: process.env.BASE_URL,
BACKEND_GQL_URL: process.env.BACKEND_GQL_URL,
BACKEND_WS_URL: process.env.BACKEND_WS_URL,
},
publicRuntimeConfig: {

View File

@@ -57,8 +57,8 @@
"@codemirror/text": "^0.19.6",
"@codemirror/tooltip": "^0.19.16",
"@codemirror/view": "^0.19.48",
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.1.0",
"@hoppscotch/data": "workspace:^0.4.2",
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.2.0",
"@hoppscotch/data": "workspace:^0.4.3",
"@hoppscotch/js-sandbox": "workspace:^2.0.0",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.32.0",
@@ -88,6 +88,7 @@
"io-ts": "^2.2.16",
"js-yaml": "^4.1.0",
"json-loader": "^0.5.7",
"jsonpath-plus": "^6.0.1",
"lodash": "^4.17.21",
"lossless-json": "^1.0.5",
"mustache": "^4.2.0",

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="docs">
<template #primary>
<div class="flex items-start justify-between p-4">
<label>

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="graphql">
<template #primary>
<GraphqlRequest :conn="gqlConn" />
<GraphqlRequestOptions :conn="gqlConn" />

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="http">
<template #primary>
<HttpRequest />
<HttpRequestOptions />

View File

@@ -83,7 +83,7 @@
<FirebaseLogout outline />
</div>
</div>
<SmartTabs v-model="selectedProfileTab">
<SmartTabs v-model="selectedProfileTab" render-inactive-tabs>
<SmartTab :id="'sync'" :label="t('settings.account')">
<div class="grid grid-cols-1">
<section class="p-4">

View File

@@ -1,53 +1,72 @@
<template>
<SmartTabs
v-model="selectedNavigationTab"
class="h-full !overflow-hidden"
styles="sticky bg-primary top-0 z-10 border-b border-dividerLight !overflow-visible"
>
<SmartTabs v-model="currentTab">
<SmartTab
id="websocket"
:label="$t('tab.websocket')"
style="height: calc(100% - var(--sidebar-primary-sticky-fold))"
v-for="{ target, title } in REALTIME_NAVIGATION"
:id="target"
:key="target"
:label="title"
>
<RealtimeWebsocket />
</SmartTab>
<SmartTab
id="sse"
:label="$t('tab.sse')"
style="height: calc(100% - var(--sidebar-primary-sticky-fold))"
>
<RealtimeSse />
</SmartTab>
<SmartTab
id="socketio"
:label="$t('tab.socketio')"
style="height: calc(100% - var(--sidebar-primary-sticky-fold))"
>
<RealtimeSocketio />
</SmartTab>
<SmartTab
id="mqtt"
:label="$t('tab.mqtt')"
style="height: calc(100% - var(--sidebar-primary-sticky-fold))"
>
<RealtimeMqtt />
<NuxtChild />
</SmartTab>
</SmartTabs>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { watch, ref, useRouter, useRoute } from "@nuxtjs/composition-api"
import { useI18n, useI18nPathInfo } from "~/helpers/utils/composables"
export default defineComponent({
data() {
return {
selectedNavigationTab: "websocket",
}
const t = useI18n()
const { localePath, getRouteBaseName } = useI18nPathInfo()
const router = useRouter()
const route = useRoute()
const REALTIME_NAVIGATION = [
{
target: "websocket",
title: t("tab.websocket"),
},
head() {
return {
title: `${this.$t("navigation.realtime")} • Hoppscotch`,
}
{
target: "sse",
title: t("tab.sse"),
},
{
target: "socketio",
title: t("tab.socketio"),
},
{
target: "mqtt",
title: t("tab.mqtt"),
},
] as const
type RealtimeNavTab = typeof REALTIME_NAVIGATION[number]["target"]
const currentTab = ref<RealtimeNavTab>("websocket")
// Update the router when the tab is updated
watch(currentTab, (newTab) => {
router.push(localePath(`/realtime/${newTab}`))
})
// Update the tab when router is upgrad
watch(
route,
(updateRoute) => {
const path = getRouteBaseName(updateRoute)
if (path.endsWith("realtime")) {
router.replace(localePath(`/realtime/websocket`))
return
}
const destination: string | undefined = path.split("realtime-")[1]
const target = REALTIME_NAVIGATION.find(
({ target }) => target === destination
)?.target
if (target) currentTab.value = target
},
{ immediate: true }
)
</script>

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="mqtt">
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="socketio">
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"
@@ -85,11 +85,13 @@
<SmartTabs
v-model="selectedTab"
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
styles="sticky bg-primary top-upperPrimaryStickyFold z-10"
render-inactive-tabs
>
<SmartTab
:id="'communication'"
:label="`${t('websocket.communication')}`"
render-inactive-tabs
>
<RealtimeCommunication
:show-event-field="true"
@@ -99,7 +101,7 @@
</SmartTab>
<SmartTab :id="'protocols'" :label="`${t('request.authorization')}`">
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold text-secondaryLight">

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="sse">
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="websocket">
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"
@@ -37,7 +37,8 @@
</div>
<SmartTabs
v-model="selectedTab"
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
styles="sticky bg-primary top-upperPrimaryStickyFold z-10"
render-inactive-tabs
>
<SmartTab
:id="'communication'"
@@ -50,7 +51,7 @@
</SmartTab>
<SmartTab :id="'protocols'" :label="`${$t('websocket.protocols')}`">
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("websocket.protocols") }}

View File

@@ -236,6 +236,7 @@
<script setup lang="ts">
import { ref, computed, watch, defineComponent } from "@nuxtjs/composition-api"
import { refAutoReset } from "@vueuse/core"
import { applySetting, toggleSetting, useSetting } from "~/newstore/settings"
import {
useToast,
@@ -276,7 +277,7 @@ const hasFirefoxExtInstalled = computed(
() => browserIsFirefox() && currentExtensionStatus.value === "available"
)
const clearIcon = ref("rotate-ccw")
const clearIcon = refAutoReset<"rotate-ccw" | "check">("rotate-ccw", 1000)
const confirmRemove = ref(false)
@@ -322,7 +323,6 @@ const resetProxy = () => {
applySetting("PROXY_URL", `https://proxy.hoppscotch.io/`)
clearIcon.value = "check"
toast.success(`${t("state.cleared")}`)
setTimeout(() => (clearIcon.value = "rotate-ccw"), 1000)
}
const getColorModeName = (colorMode: string) => {

View File

@@ -10,6 +10,7 @@
"sourceMap": true,
"skipLibCheck": true,
"strict": true,
"jsx": "preserve",
"noEmit": true,
"baseUrl": ".",
"paths": {
@@ -29,6 +30,7 @@
},
"exclude": ["node_modules", ".nuxt", "dist"],
"vueCompilerOptions": {
"target": 2,
"experimentalCompatMode": 2
}
}

View File

@@ -0,0 +1,6 @@
import { JSONPathOptions } from "jsonpath-plus"
declare module "jsonpath-plus" {
export type JSONPathType = (options: JSONPathOptions) => unknown
export const JSONPath: JSONPathType
}

View File

@@ -18,6 +18,7 @@ export default defineConfig({
"var(--upper-mobile-raw-tertiary-sticky-fold)",
lowerPrimaryStickyFold: "var(--lower-primary-sticky-fold)",
lowerSecondaryStickyFold: "var(--lower-secondary-sticky-fold)",
lowerTertiaryStickyFold: "var(--lower-tertiary-sticky-fold)",
sidebarPrimaryStickyFold: "var(--sidebar-primary-sticky-fold)",
},
colors: {

View File

@@ -24,13 +24,26 @@ hopp [options or commands] arguments
- Displays the help text
3. #### **`hopp test <file_path>`**
3. #### **`hopp test [options] <file_path>`**
- Interactive CLI to accept Hoppscotch collection JSON path
- Parses the collection JSON and executes each requests
- Executes pre-request script.
- Outputs the response of each request.
- 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"`
## Install
Install [@hoppscotch/cli](https://www.npmjs.com/package/@hoppscotch/cli) from npm by running:

View File

@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/cli",
"version": "0.1.14",
"version": "0.3.0",
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io",
"main": "dist/index.js",
@@ -36,7 +36,7 @@
"license": "MIT",
"private": false,
"devDependencies": {
"@hoppscotch/data": "workspace:^0.4.2",
"@hoppscotch/data": "workspace:^0.4.3",
"@hoppscotch/js-sandbox": "workspace:^2.0.0",
"@relmify/jest-fp-ts": "^2.0.2",
"@swc/core": "^1.2.181",

View File

@@ -8,7 +8,7 @@ describe("Test 'hopp test <file>' command:", () => {
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("NO_FILE_PATH");
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Collection file not found.", async () => {
@@ -42,7 +42,7 @@ describe("Test 'hopp test <file>' command:", () => {
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("FILE_NOT_JSON");
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("Some errors occured (exit code 1).", async () => {
@@ -62,3 +62,71 @@ describe("Test 'hopp test <file>' command:", () => {
expect(error).toBeNull();
});
});
describe("Test 'hopp test <file> --env <file>' command:", () => {
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
"passes.json"
)}`;
test("No env file path provided.", async () => {
const cmd = `${VALID_TEST_CMD} --env`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("ENV file not JSON type.", async () => {
const cmd = `${VALID_TEST_CMD} --env ${getTestJsonFilePath("notjson.txt")}`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("ENV file not found.", async () => {
const cmd = `${VALID_TEST_CMD} --env notfound.json`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("No errors occured (exit code 0).", async () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error } = await execAsync(cmd);
expect(error).toBeNull();
});
});
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
"passes.json"
)}`;
test("No value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Invalid value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Valid value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay 1`;
const { error } = await execAsync(cmd);
expect(error).toBeNull();
});
});

View File

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

View File

@@ -37,6 +37,8 @@ const SAMPLE_RESOLVED_RESPONSE = <AxiosResponse>{
headers: [],
};
const SAMPLE_ENVS = { global: [], selected: [] };
describe("collectionsRunner", () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -47,19 +49,24 @@ describe("collectionsRunner", () => {
});
test("Empty HoppCollection.", () => {
return expect(collectionsRunner([])()).resolves.toStrictEqual([]);
return expect(
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })()
).resolves.toStrictEqual([]);
});
test("Empty requests and folders in collection.", () => {
return expect(
collectionsRunner([
{
v: 1,
name: "name",
folders: [],
requests: [],
},
])()
collectionsRunner({
collections: [
{
v: 1,
name: "name",
folders: [],
requests: [],
},
],
envs: SAMPLE_ENVS,
})()
).resolves.toMatchObject([]);
});
@@ -67,14 +74,17 @@ describe("collectionsRunner", () => {
(axios as unknown as jest.Mock).mockResolvedValue(SAMPLE_RESOLVED_RESPONSE);
return expect(
collectionsRunner([
{
v: 1,
name: "collection",
folders: [],
requests: [SAMPLE_HOPP_REQUEST],
},
])()
collectionsRunner({
collections: [
{
v: 1,
name: "collection",
folders: [],
requests: [SAMPLE_HOPP_REQUEST],
},
],
envs: SAMPLE_ENVS,
})()
).resolves.toMatchObject([
{
path: "collection/request",
@@ -89,21 +99,24 @@ describe("collectionsRunner", () => {
(axios as unknown as jest.Mock).mockResolvedValue(SAMPLE_RESOLVED_RESPONSE);
return expect(
collectionsRunner([
{
v: 1,
name: "collection",
folders: [
{
v: 1,
name: "folder",
folders: [],
requests: [SAMPLE_HOPP_REQUEST],
},
],
requests: [],
},
])()
collectionsRunner({
collections: [
{
v: 1,
name: "collection",
folders: [
{
v: 1,
name: "folder",
folders: [],
requests: [SAMPLE_HOPP_REQUEST],
},
],
requests: [],
},
],
envs: SAMPLE_ENVS,
})()
).resolves.toMatchObject([
{
path: "collection/folder/request",

View File

@@ -1,6 +1,8 @@
import { Environment } from "@hoppscotch/data";
import { getEffectiveFinalMetaData } from "../../../utils/getters";
import "@relmify/jest-fp-ts";
const DEFAULT_ENV = <Environment>{
name: "name",
variables: [{ key: "PARAM", value: "parsed_param" }],

View File

@@ -1,12 +1,14 @@
import { HoppCLIError } from "../../../types/errors";
import { parseCollectionData } from "../../../utils/mutators";
import "@relmify/jest-fp-ts";
describe("parseCollectionData", () => {
test("Reading non-existing file.", () => {
return expect(
parseCollectionData("./src/__tests__/samples/notexist.txt")()
parseCollectionData("./src/__tests__/samples/notexist.json")()
).resolves.toSubsetEqualLeft(<HoppCLIError>{
code: "UNKNOWN_ERROR",
code: "FILE_NOT_FOUND",
});
});

View File

@@ -3,6 +3,8 @@ import { EffectiveHoppRESTRequest } from "../../../interfaces/request";
import { HoppCLIError } from "../../../types/errors";
import { getEffectiveRESTRequest } from "../../../utils/pre-request";
import "@relmify/jest-fp-ts";
const DEFAULT_ENV = <Environment>{
name: "name",
variables: [

View File

@@ -0,0 +1,30 @@
import { hrtime } from "process";
import { getDurationInSeconds } from "../../../utils/getters";
import { delayPromiseFunction } from "../../../utils/request";
describe("describePromiseFunction", () => {
let promiseFunc = (): Promise<number> => new Promise((resolve) => resolve(2));
beforeEach(() => {
promiseFunc = (): Promise<number> => new Promise((resolve) => resolve(2));
});
it("Should resolve the promise<number> after 2 seconds.", async () => {
const start = hrtime();
const res = await delayPromiseFunction(promiseFunc, 2000);
const end = hrtime(start);
const duration = getDurationInSeconds(end);
expect(Math.floor(duration)).toEqual(2);
expect(typeof res).toBe("number");
});
it("Should resolve the promise<number> after 4 seconds.", async () => {
const start = hrtime();
const res = await delayPromiseFunction(promiseFunc, 4000);
const end = hrtime(start);
const duration = getDurationInSeconds(end);
expect(Math.floor(duration)).toEqual(4);
expect(typeof res).toBe("number");
});
});

View File

@@ -58,7 +58,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect(
processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/collection/path")()
processRequest({
request: SAMPLE_REQUEST,
envs: DEFAULT_ENVS,
path: "fake/collection/path",
delay: 0,
})()
).resolves.toMatchObject({
report: {
result: true,
@@ -79,7 +84,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect(
processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/collection/path")()
processRequest({
request: SAMPLE_REQUEST,
envs: DEFAULT_ENVS,
path: "fake/collection/path",
delay: 0,
})()
).resolves.toMatchObject({
envs: {
selected: [{ key: "ENDPOINT", value: "https://example.com" }],
@@ -96,7 +106,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect(
processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/request/path")()
processRequest({
request: SAMPLE_REQUEST,
envs: DEFAULT_ENVS,
path: "fake/request/path",
delay: 0,
})()
).resolves.toMatchObject({
report: { result: false },
});

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