Compare commits
130 Commits
feat/realt
...
reference/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6086ebd824 | ||
|
|
dd83f8ef24 | ||
|
|
682200ce68 | ||
|
|
052595c076 | ||
|
|
18910e429c | ||
|
|
924d6a87d0 | ||
|
|
fc15a5a1e4 | ||
|
|
477811c414 | ||
|
|
6b8ae63747 | ||
|
|
c013aa52ac | ||
|
|
631a16feb0 | ||
|
|
d0f4080771 | ||
|
|
0da75cb23d | ||
|
|
017cbb5a71 | ||
|
|
2e1ca0cbb0 | ||
|
|
abba09ea80 | ||
|
|
a9e1a3002e | ||
|
|
fb5967294b | ||
|
|
73fdfbd2c8 | ||
|
|
0c31d9201f | ||
|
|
21d8b8fb2e | ||
|
|
7ce85fee81 | ||
|
|
10615ca1a1 | ||
|
|
dd5c876e32 | ||
|
|
c2002f0f27 | ||
|
|
9cfba797f6 | ||
|
|
d1e6ffda49 | ||
|
|
4f71b163ea | ||
|
|
775bf9a9c3 | ||
|
|
33ecea5d75 | ||
|
|
551dfd1e20 | ||
|
|
8663934075 | ||
|
|
a73d64ddc1 | ||
|
|
99f119d262 | ||
|
|
6a8a687616 | ||
|
|
6a33083790 | ||
|
|
5c7c355d95 | ||
|
|
328fb1176d | ||
|
|
fdf0c95f9a | ||
|
|
9e90e703f7 | ||
|
|
0a241663ac | ||
|
|
d58fc42190 | ||
|
|
7a9bcd0a5c | ||
|
|
e3482f66cc | ||
|
|
186803a465 | ||
|
|
bdfdb44743 | ||
|
|
4f8b346024 | ||
|
|
ec1104396e | ||
|
|
630ab1f4f4 | ||
|
|
cabc775f58 | ||
|
|
f515ac4f52 | ||
|
|
11b8bb4571 | ||
|
|
7bf66d8339 | ||
|
|
8d81ff3dc2 | ||
|
|
5538b9a5b9 | ||
|
|
7077fe4621 | ||
|
|
42144b724b | ||
|
|
14183d8b91 | ||
|
|
f8e1d78824 | ||
|
|
1e8805ab4f | ||
|
|
a294a2804b | ||
|
|
8507278c40 | ||
|
|
d22bae2c60 | ||
|
|
2fefa55dce | ||
|
|
e0787d7fca | ||
|
|
c9c5df36ab | ||
|
|
5768274ef1 | ||
|
|
493594b5d7 | ||
|
|
56c96f952d | ||
|
|
2c06a66c0a | ||
|
|
1a26a0e986 | ||
|
|
9f1ee724b4 | ||
|
|
d28679de15 | ||
|
|
fa0e7f4785 | ||
|
|
e9576dd339 | ||
|
|
c8f62c4f04 | ||
|
|
8aa066e2ab | ||
|
|
a38e6cd427 | ||
|
|
c3ba45f875 | ||
|
|
9061511609 | ||
|
|
443e095775 | ||
|
|
09e6fb246a | ||
|
|
d335ac1d80 | ||
|
|
c0e3a2be0b | ||
|
|
722864da62 | ||
|
|
5413bc584a | ||
|
|
7006fa57e2 | ||
|
|
1a629a1219 | ||
|
|
9b60dc5f2d | ||
|
|
21021a3cd9 | ||
|
|
fd5db6c8c9 | ||
|
|
54a12ef6fa | ||
|
|
d035262e1a | ||
|
|
1ab54b0ce7 | ||
|
|
cac3abd2ab | ||
|
|
c34185dc4b | ||
|
|
cfdab014c7 | ||
|
|
ed6e1c0f94 | ||
|
|
07a8a37739 | ||
|
|
ca553b9d3c | ||
|
|
69aaeaf42a | ||
|
|
015393d98f | ||
|
|
c8dec56b96 | ||
|
|
8fefd37862 | ||
|
|
c1cc1ce295 | ||
|
|
16be7c38f3 | ||
|
|
82b6ad935a | ||
|
|
185dc3f2c9 | ||
|
|
51138fa42d | ||
|
|
7f08a4bd81 | ||
|
|
0244b941b3 | ||
|
|
2d0bd48e00 | ||
|
|
15e433b114 | ||
|
|
97ff089110 | ||
|
|
a6b5295df5 | ||
|
|
6b1ca1dce1 | ||
|
|
04a9c4dc52 | ||
|
|
e5e44b889f | ||
|
|
c46bc40bcb | ||
|
|
a91a8ba575 | ||
|
|
1f536eeedd | ||
|
|
25253c4bdf | ||
|
|
043c49541f | ||
|
|
a78462fbe3 | ||
|
|
52c25e497f | ||
|
|
4f539c9781 | ||
|
|
ba468bb835 | ||
|
|
93faa8d5ff | ||
|
|
cf90d16f8a | ||
|
|
39f72f8458 |
16
.github/workflows/deploy-netlify.yml
vendored
16
.github/workflows/deploy-netlify.yml
vendored
@@ -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
|
||||
|
||||
45
.github/workflows/deploy-staging-netlify.yml
vendored
Normal file
45
.github/workflows/deploy-staging-netlify.yml
vendored
Normal 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
|
||||
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
13
packages/hoppscotch-app/assets/icons/filter.svg
Normal file
13
packages/hoppscotch-app/assets/icons/filter.svg
Normal 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 |
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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")}`)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')}`"
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
271
packages/hoppscotch-app/components/http/PathVariables.vue
Normal file
271
packages/hoppscotch-app/components/http/PathVariables.vue
Normal 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>
|
||||
363
packages/hoppscotch-app/components/http/QueryParams.vue
Normal file
363
packages/hoppscotch-app/components/http/QueryParams.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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'" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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] : []
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
@@ -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,
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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": "团队已保存",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "閱讀更多有關最新版本的內容",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -540,6 +540,6 @@ export function updateEnvironmentVariable(
|
||||
})
|
||||
}
|
||||
|
||||
export function getEnviroment(index: number) {
|
||||
export function getEnvironment(index: number) {
|
||||
return environmentsStore.value.environments[index]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<AppPaneLayout>
|
||||
<AppPaneLayout layout-id="docs">
|
||||
<template #primary>
|
||||
<div class="flex items-start justify-between p-4">
|
||||
<label>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<AppPaneLayout>
|
||||
<AppPaneLayout layout-id="graphql">
|
||||
<template #primary>
|
||||
<GraphqlRequest :conn="gqlConn" />
|
||||
<GraphqlRequestOptions :conn="gqlConn" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<AppPaneLayout>
|
||||
<AppPaneLayout layout-id="http">
|
||||
<template #primary>
|
||||
<HttpRequest />
|
||||
<HttpRequestOptions />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
@@ -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">
|
||||
@@ -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"
|
||||
@@ -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") }}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
6
packages/hoppscotch-app/types/jsonpath-plus.d.ts
vendored
Normal file
6
packages/hoppscotch-app/types/jsonpath-plus.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import { JSONPathOptions } from "jsonpath-plus"
|
||||
|
||||
declare module "jsonpath-plus" {
|
||||
export type JSONPathType = (options: JSONPathOptions) => unknown
|
||||
export const JSONPath: JSONPathType
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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" }],
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user