Compare commits
2 Commits
orphan-pr/
...
feat/realt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d13b381097 | ||
|
|
663da34e08 |
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: Setup and run pnpm install
|
||||
uses: pnpm/action-setup@v2.2.2
|
||||
with:
|
||||
version: 7
|
||||
run_install: true
|
||||
- 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 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 production site with netlify-cli
|
||||
- name: Deploy to Netlify (production)
|
||||
# Deploy the site with netlify-cli
|
||||
- name: Deploy to Netlify
|
||||
uses: netlify/actions/cli@master
|
||||
env:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_PRODUCTION_SITE_ID }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_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
45
.github/workflows/deploy-staging-netlify.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Deploy to Staging Netlify
|
||||
|
||||
on:
|
||||
push:
|
||||
# TODO: Migrate to staging branch only
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Push build files to Netlify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup and run pnpm install
|
||||
uses: pnpm/action-setup@v2.2.2
|
||||
with:
|
||||
version: 7
|
||||
run_install: true
|
||||
|
||||
- name: Build Site
|
||||
env:
|
||||
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,15 +17,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- name: Setup and run pnpm install
|
||||
uses: pnpm/action-setup@v2.2.2
|
||||
with:
|
||||
version: 7
|
||||
run_install: true
|
||||
- name: Install pnpm
|
||||
run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6
|
||||
- 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 test
|
||||
run: pnpm i && pnpm -r test
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hoppscotch/codemirror-lang-graphql",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.0",
|
||||
"description": "GraphQL language support for CodeMirror",
|
||||
"author": "Hoppscotch (support@hoppscotch.io)",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -27,22 +27,16 @@ export const GQLLanguage = LRLanguage.define({
|
||||
},
|
||||
}),
|
||||
styleTags({
|
||||
Comment: t.lineComment,
|
||||
Name: t.propertyName,
|
||||
StringValue: t.string,
|
||||
IntValue: t.integer,
|
||||
FloatValue: t.float,
|
||||
NullValue: t.null,
|
||||
BooleanValue: t.bool,
|
||||
Comma: t.separator,
|
||||
Name: t.definition(t.variableName),
|
||||
"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,
|
||||
OperationType: t.keyword,
|
||||
BooleanValue: t.bool,
|
||||
StringValue: t.string,
|
||||
IntValue: t.number,
|
||||
FloatValue: t.number,
|
||||
NullValue: t.null,
|
||||
ObjectValue: t.brace,
|
||||
Comment: t.lineComment,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -33,24 +33,16 @@ TypeSystemExtension {
|
||||
TypeExtension
|
||||
}
|
||||
|
||||
SchemaKeyword {
|
||||
@specialize<Name, "schema">
|
||||
}
|
||||
|
||||
SchemaDefinition {
|
||||
Description? SchemaKeyword Directives? RootTypeDef
|
||||
Description? @specialize<Name, "schema"> Directives? RootTypeDef
|
||||
}
|
||||
|
||||
RootTypeDef {
|
||||
"{" RootOperationTypeDefinition+ "}"
|
||||
}
|
||||
|
||||
ExtendKeyword {
|
||||
@specialize<Name, "extend">
|
||||
}
|
||||
|
||||
SchemaExtension {
|
||||
ExtendKeyword SchemaKeyword Directives? RootTypeDef
|
||||
@specialize<Name, "extend"> @specialize<Name, "schema"> Directives? RootTypeDef
|
||||
}
|
||||
|
||||
TypeExtension {
|
||||
@@ -62,53 +54,33 @@ TypeExtension {
|
||||
InputObjectTypeExtension
|
||||
}
|
||||
|
||||
ScalarKeyword {
|
||||
@specialize<Name, "scalar">
|
||||
}
|
||||
|
||||
ScalarTypeExtension {
|
||||
ExtendKeyword ScalarKeyword Name Directives
|
||||
@specialize<Name, "extend"> @specialize<Name, "scalar"> Name Directives
|
||||
}
|
||||
|
||||
ObjectTypeExtension /* precedence: right 0 */ {
|
||||
ExtendKeyword TypeKeyword Name ImplementsInterfaces? Directives? !typeDef FieldsDefinition |
|
||||
ExtendKeyword TypeKeyword Name ImplementsInterfaces? Directives?
|
||||
}
|
||||
|
||||
InterfaceKeyword {
|
||||
@specialize<Name, "interface">
|
||||
@specialize<Name, "extend"> @specialize<Name, "type"> Name ImplementsInterfaces? Directives? !typeDef FieldsDefinition |
|
||||
@specialize<Name, "extend"> @specialize<Name, "type"> Name ImplementsInterfaces? Directives?
|
||||
}
|
||||
|
||||
InterfaceTypeExtension /* precedence: right 0 */ {
|
||||
ExtendKeyword InterfaceKeyword Name ImplementsInterfaces? Directives? FieldsDefinition |
|
||||
ExtendKeyword InterfaceKeyword Name ImplementsInterfaces? Directives?
|
||||
}
|
||||
|
||||
UnionKeyword {
|
||||
@specialize<Name, "union">
|
||||
@specialize<Name, "extend"> @specialize<Name, "interface"> Name ImplementsInterfaces? Directives? FieldsDefinition |
|
||||
@specialize<Name, "extend"> @specialize<Name, "interface"> Name ImplementsInterfaces? Directives?
|
||||
}
|
||||
|
||||
UnionTypeExtension /* precedence: right 0 */ {
|
||||
ExtendKeyword UnionKeyword Name Directives? UnionMemberTypes |
|
||||
ExtendKeyword UnionKeyword Name Directives?
|
||||
}
|
||||
|
||||
EnumKeyword {
|
||||
@specialize<Name, "enum">
|
||||
@specialize<Name, "extend"> @specialize<Name, "union"> Name Directives? UnionMemberTypes |
|
||||
@specialize<Name, "extend"> @specialize<Name, "union"> Name Directives?
|
||||
}
|
||||
|
||||
EnumTypeExtension /* precedence: right 0 */ {
|
||||
ExtendKeyword EnumKeyword Name Directives? !typeDef EnumValuesDefinition |
|
||||
ExtendKeyword EnumKeyword Name Directives?
|
||||
}
|
||||
|
||||
InputKeyword {
|
||||
@specialize<Name, "input">
|
||||
@specialize<Name, "extend"> @specialize<Name, "enum"> Name Directives? !typeDef EnumValuesDefinition |
|
||||
@specialize<Name, "extend"> @specialize<Name, "enum"> Name Directives?
|
||||
}
|
||||
|
||||
InputObjectTypeExtension /* precedence: right 0 */ {
|
||||
ExtendKeyword InputKeyword Name Directives? InputFieldsDefinition+ |
|
||||
ExtendKeyword InputKeyword Name Directives?
|
||||
@specialize<Name, "extend"> @specialize<Name, "input"> Name Directives? InputFieldsDefinition+ |
|
||||
@specialize<Name, "extend"> @specialize<Name, "input"> Name Directives?
|
||||
}
|
||||
|
||||
InputFieldsDefinition {
|
||||
@@ -123,13 +95,9 @@ EnumValueDefinition {
|
||||
Description? EnumValue Directives?
|
||||
}
|
||||
|
||||
ImplementsKeyword {
|
||||
@specialize<Name, "implements">
|
||||
}
|
||||
|
||||
ImplementsInterfaces {
|
||||
ImplementsInterfaces "&" NamedType |
|
||||
ImplementsKeyword "&"? NamedType
|
||||
@specialize<Name, "implements"> "&"? NamedType
|
||||
}
|
||||
|
||||
FieldsDefinition {
|
||||
@@ -176,31 +144,27 @@ TypeDefinition {
|
||||
}
|
||||
|
||||
ScalarTypeDefinition /* precedence: right 0 */ {
|
||||
Description? ScalarKeyword Name Directives?
|
||||
}
|
||||
|
||||
TypeKeyword {
|
||||
@specialize<Name, "type">
|
||||
Description? @specialize<Name, "scalar"> Name Directives?
|
||||
}
|
||||
|
||||
ObjectTypeDefinition /* precedence: right 0 */ {
|
||||
Description? TypeKeyword Name ImplementsInterfaces? Directives? FieldsDefinition?
|
||||
Description? @specialize<Name, "type"> Name ImplementsInterfaces? Directives? FieldsDefinition?
|
||||
}
|
||||
|
||||
InterfaceTypeDefinition /* precedence: right 0 */ {
|
||||
Description? InterfaceKeyword Name ImplementsInterfaces? Directives? FieldsDefinition?
|
||||
Description? @specialize<Name, "interface"> Name ImplementsInterfaces? Directives? FieldsDefinition?
|
||||
}
|
||||
|
||||
UnionTypeDefinition /* precedence: right 0 */ {
|
||||
Description? UnionKeyword Name Directives? UnionMemberTypes?
|
||||
Description? @specialize<Name, "union"> Name Directives? UnionMemberTypes?
|
||||
}
|
||||
|
||||
EnumTypeDefinition /* precedence: right 0 */ {
|
||||
Description? EnumKeyword Name Directives? !typeDef EnumValuesDefinition?
|
||||
Description? @specialize<Name, "enum"> Name Directives? !typeDef EnumValuesDefinition?
|
||||
}
|
||||
|
||||
InputObjectTypeDefinition /* precedence: right 0 */ {
|
||||
Description? InputKeyword Name Directives? !typeDef InputFieldsDefinition?
|
||||
Description? @specialize<Name, "input"> Name Directives? !typeDef InputFieldsDefinition?
|
||||
}
|
||||
|
||||
VariableDefinitions {
|
||||
@@ -273,12 +237,8 @@ FragmentSpread {
|
||||
"..." FragmentName Directives?
|
||||
}
|
||||
|
||||
FragmentKeyword {
|
||||
@specialize<Name, "fragment">
|
||||
}
|
||||
|
||||
FragmentDefinition {
|
||||
FragmentKeyword FragmentName TypeCondition Directives? SelectionSet
|
||||
@specialize<Name, "fragment"> FragmentName TypeCondition Directives? SelectionSet
|
||||
}
|
||||
|
||||
FragmentName {
|
||||
@@ -289,36 +249,20 @@ InlineFragment {
|
||||
"..." TypeCondition? Directives? SelectionSet
|
||||
}
|
||||
|
||||
OnKeyword {
|
||||
@specialize<Name, "on">
|
||||
}
|
||||
|
||||
TypeCondition {
|
||||
OnKeyword NamedType
|
||||
@specialize<Name, "on"> NamedType
|
||||
}
|
||||
|
||||
Directives {
|
||||
Directive+
|
||||
}
|
||||
|
||||
DirectiveName {
|
||||
"@" Name
|
||||
}
|
||||
|
||||
Directive {
|
||||
DirectiveName Arguments?
|
||||
}
|
||||
|
||||
DirectiveKeyword {
|
||||
@specialize<Name, "directive">
|
||||
}
|
||||
|
||||
RepeatableKeyword {
|
||||
@specialize<Name, "repeatable">
|
||||
"@" Name Arguments?
|
||||
}
|
||||
|
||||
DirectiveDefinition /* precedence: right 1 */ {
|
||||
Description? DirectiveKeyword "@" Name ArgumentsDefinition? RepeatableKeyword ? OnKeyword DirectiveLocations
|
||||
Description? @specialize<Name, "directive"> "@" Name ArgumentsDefinition? @specialize<Name, "repeatable"> ? @specialize<Name, "on"> DirectiveLocations
|
||||
}
|
||||
|
||||
DirectiveLocations {
|
||||
@@ -394,14 +338,17 @@ TypeSystemDirectiveLocation {
|
||||
| @specialize<Name, "INPUT_FIELD_DEFINITION">
|
||||
}
|
||||
|
||||
@skip { whitespace | Comment }
|
||||
|
||||
@tokens {
|
||||
whitespace {
|
||||
std.whitespace+
|
||||
}
|
||||
|
||||
StringValue {
|
||||
"\"\"\"" (!["] | "\\n" | "\"" "\""? !["])* "\"\"\"" | "\"" !["\\\n]* "\""
|
||||
}
|
||||
|
||||
IntValue {
|
||||
"-"? "0"
|
||||
| "-"? std.digit+
|
||||
@@ -416,19 +363,14 @@ TypeSystemDirectiveLocation {
|
||||
Name {
|
||||
$[_A-Za-z] $[_0-9A-Za-z]*
|
||||
}
|
||||
|
||||
Comment {
|
||||
"#" ![\n]*
|
||||
}
|
||||
Comma {
|
||||
","
|
||||
}
|
||||
|
||||
Comment {
|
||||
"#" ![\n]*
|
||||
}
|
||||
|
||||
|
||||
"{" "}"
|
||||
"{" "}" "[" "]"
|
||||
}
|
||||
|
||||
@skip { whitespace | Comment }
|
||||
|
||||
@detectDelim
|
||||
|
||||
@@ -16,7 +16,3 @@ 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
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 283 B |
@@ -15,7 +15,6 @@
|
||||
|
||||
::-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 {
|
||||
@@ -28,17 +27,17 @@
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-4;
|
||||
@apply h-0;
|
||||
@apply h-4;
|
||||
}
|
||||
|
||||
// .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,7 +255,6 @@
|
||||
--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;
|
||||
}
|
||||
|
||||
@@ -271,7 +270,6 @@
|
||||
--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;
|
||||
}
|
||||
|
||||
@@ -287,7 +285,6 @@
|
||||
--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 { refAutoReset } from "@vueuse/core"
|
||||
import { ref } from "@nuxtjs/composition-api"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import {
|
||||
useI18n,
|
||||
@@ -45,7 +45,7 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
|
||||
const copyIcon = ref("copy")
|
||||
|
||||
// Copy user auth token to clipboard
|
||||
const copyUserAuthToken = () => {
|
||||
@@ -53,6 +53,7 @@ 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 overflow-x-auto"
|
||||
class="flex items-center justify-between flex-1 px-2 py-2 space-x-2"
|
||||
>
|
||||
<div class="inline-flex items-center space-x-2">
|
||||
<ButtonSecondary
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { ref } from "@nuxtjs/composition-api"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useI18n, useToast } from "~/helpers/utils/composables"
|
||||
|
||||
@@ -60,8 +60,7 @@ 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 = refAutoReset<"copy" | "check">("copy", 1000)
|
||||
|
||||
const copyIcon = ref("copy")
|
||||
const platforms = [
|
||||
{
|
||||
name: "Email",
|
||||
@@ -94,6 +93,7 @@ const copyAppLink = () => {
|
||||
copyToClipboard(url)
|
||||
copyIcon.value = "check"
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
:to="localePath(navigation.target)"
|
||||
class="nav-link"
|
||||
tabindex="0"
|
||||
:exact="navigation.exact"
|
||||
>
|
||||
<div v-if="navigation.svg">
|
||||
<SmartIcon :name="navigation.svg" class="svg-icons" />
|
||||
@@ -41,31 +40,26 @@ 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>
|
||||
@@ -111,20 +105,6 @@ 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,10 +1,6 @@
|
||||
<template>
|
||||
<div v-if="show">
|
||||
<SmartTabs
|
||||
:id="'collections_tab'"
|
||||
v-model="selectedCollectionTab"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTabs :id="'collections_tab'" v-model="selectedCollectionTab">
|
||||
<SmartTab
|
||||
:id="'my-collections'"
|
||||
:label="`${$t('collection.my_collections')}`"
|
||||
|
||||
@@ -244,7 +244,7 @@ const createCollectionGist = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
await getJSONCollection()
|
||||
getJSONCollection()
|
||||
|
||||
try {
|
||||
const res = await axios.$post(
|
||||
@@ -316,8 +316,8 @@ const importToTeams = async (content: HoppCollection<HoppRESTRequest>) => {
|
||||
importingMyCollections.value = false
|
||||
}
|
||||
|
||||
const exportJSON = async () => {
|
||||
await getJSONCollection()
|
||||
const exportJSON = () => {
|
||||
getJSONCollection()
|
||||
|
||||
const dataToWrite = collectionJson.value
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
|
||||
@@ -322,24 +322,20 @@ const setRestReq = (request: any) => {
|
||||
)
|
||||
}
|
||||
|
||||
/** Loads request from the save once, checks for unsaved changes, but ignores default values */
|
||||
const selectRequest = () => {
|
||||
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
|
||||
if (props.saveRequest) {
|
||||
emit("select", {
|
||||
picked: {
|
||||
pickedType: "my-request",
|
||||
collectionIndex: props.collectionIndex,
|
||||
folderPath: props.folderPath,
|
||||
folderName: props.folderName,
|
||||
requestIndex: props.requestIndex,
|
||||
},
|
||||
})
|
||||
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
|
||||
confirmChange.value = false
|
||||
setRestReq(props.request)
|
||||
} else if (!active.value) {
|
||||
if (!active.value) {
|
||||
confirmChange.value = true
|
||||
|
||||
if (props.saveRequest)
|
||||
emit("select", {
|
||||
picked: {
|
||||
pickedType: "my-request",
|
||||
collectionIndex: props.collectionIndex,
|
||||
folderPath: props.folderPath,
|
||||
folderName: props.folderName,
|
||||
requestIndex: props.requestIndex,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const currentReqWithNoChange = active.value.req
|
||||
const currentFullReq = getRESTRequest()
|
||||
@@ -349,6 +345,16 @@ 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
|
||||
}
|
||||
@@ -368,6 +374,16 @@ 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 === "teams-collection" &&
|
||||
props.picked.pickedType === "team-collection" &&
|
||||
props.picked.requestID === props.requestIndex
|
||||
)
|
||||
|
||||
@@ -308,19 +308,16 @@ const setRestReq = (request: HoppRESTRequest) => {
|
||||
}
|
||||
|
||||
const selectRequest = () => {
|
||||
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
|
||||
if (props.saveRequest) {
|
||||
emit("select", {
|
||||
picked: {
|
||||
pickedType: "teams-collection",
|
||||
requestID: props.requestIndex,
|
||||
},
|
||||
})
|
||||
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
|
||||
confirmChange.value = false
|
||||
setRestReq(props.request)
|
||||
} else if (!active.value) {
|
||||
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()
|
||||
@@ -330,6 +327,13 @@ 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
|
||||
}
|
||||
@@ -349,6 +353,13 @@ 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",
|
||||
@@ -356,6 +367,7 @@ const discardRequestChange = () => {
|
||||
req: props.request,
|
||||
})
|
||||
}
|
||||
|
||||
confirmChange.value = false
|
||||
}
|
||||
|
||||
|
||||
@@ -120,11 +120,10 @@ import clone from "lodash/clone"
|
||||
import { computed, ref, watch } from "@nuxtjs/composition-api"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import {
|
||||
createEnvironment,
|
||||
environments$,
|
||||
getEnvironment,
|
||||
getEnviroment,
|
||||
getGlobalVariables,
|
||||
globalEnv$,
|
||||
setCurrentEnvironment,
|
||||
@@ -161,8 +160,7 @@ const emit = defineEmits<{
|
||||
|
||||
const name = ref<string | null>(null)
|
||||
const vars = ref([{ key: "", value: "" }])
|
||||
|
||||
const clearIcon = refAutoReset<"trash-2" | "check">("trash-2", 1000)
|
||||
const clearIcon = ref("trash-2")
|
||||
|
||||
const globalVars = useReadonlyStream(globalEnv$, [])
|
||||
|
||||
@@ -178,7 +176,7 @@ const workingEnv = computed(() => {
|
||||
variables: props.envVars(),
|
||||
}
|
||||
} else if (props.editingEnvironmentIndex !== null) {
|
||||
return getEnvironment(props.editingEnvironmentIndex)
|
||||
return getEnviroment(props.editingEnvironmentIndex)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -227,6 +225,7 @@ const clearContent = () => {
|
||||
vars.value = []
|
||||
clearIcon.value = "check"
|
||||
toast.success(`${t("state.cleared")}`)
|
||||
setTimeout(() => (clearIcon.value = "trash-2"), 1000)
|
||||
}
|
||||
|
||||
const addEnvironmentVariable = () => {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<SmartTabs
|
||||
v-model="selectedOptionTab"
|
||||
styles="sticky bg-primary top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTab
|
||||
:id="'query'"
|
||||
@@ -313,7 +312,6 @@ 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,
|
||||
@@ -614,13 +612,10 @@ useCodemirror(queryEditor, gqlQueryString, {
|
||||
environmentHighlights: false,
|
||||
})
|
||||
|
||||
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 copyQueryIcon = ref("copy")
|
||||
const copyVariablesIcon = ref("copy")
|
||||
const prettifyQueryIcon = ref("wand")
|
||||
const prettifyVariablesIcon = ref("wand")
|
||||
|
||||
const showSaveRequestModal = ref(false)
|
||||
|
||||
@@ -628,6 +623,7 @@ 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)
|
||||
@@ -703,6 +699,7 @@ const prettifyQuery = () => {
|
||||
toast.error(`${t("error.gql_prettify_invalid_query")}`)
|
||||
prettifyQueryIcon.value = "info"
|
||||
}
|
||||
setTimeout(() => (prettifyQueryIcon.value = "wand"), 1000)
|
||||
}
|
||||
|
||||
const saveRequest = () => {
|
||||
@@ -713,6 +710,7 @@ const copyVariables = () => {
|
||||
copyToClipboard(variableString.value)
|
||||
copyVariablesIcon.value = "check"
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const prettifyVariableString = () => {
|
||||
@@ -725,6 +723,7 @@ const prettifyVariableString = () => {
|
||||
prettifyVariablesIcon.value = "info"
|
||||
toast.error(`${t("error.json_prettify_invalid_body")}`)
|
||||
}
|
||||
setTimeout(() => (prettifyVariablesIcon.value = "wand"), 1000)
|
||||
}
|
||||
|
||||
const clearGQLQuery = () => {
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
|
||||
<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 {
|
||||
@@ -112,16 +111,14 @@ useCodemirror(
|
||||
})
|
||||
)
|
||||
|
||||
const downloadResponseIcon = refAutoReset<"download" | "check">(
|
||||
"download",
|
||||
1000
|
||||
)
|
||||
const copyResponseIcon = refAutoReset<"copy" | "check">("copy", 1000)
|
||||
const downloadResponseIcon = ref("download")
|
||||
const copyResponseIcon = ref("copy")
|
||||
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseString.value!)
|
||||
copyResponseIcon.value = "check"
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const downloadResponse = () => {
|
||||
@@ -138,6 +135,7 @@ const downloadResponse = () => {
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadResponseIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
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
|
||||
@@ -65,7 +64,6 @@
|
||||
<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"
|
||||
@@ -195,7 +193,6 @@ 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"
|
||||
@@ -309,8 +306,8 @@ const graphqlTypes = useReadonlyStream(
|
||||
[]
|
||||
)
|
||||
|
||||
const downloadSchemaIcon = refAutoReset<"download" | "check">("download", 1000)
|
||||
const copySchemaIcon = refAutoReset<"copy" | "check">("copy", 1000)
|
||||
const downloadSchemaIcon = ref("download")
|
||||
const copySchemaIcon = ref("copy")
|
||||
|
||||
const graphqlFieldsFilterText = ref("")
|
||||
|
||||
@@ -426,6 +423,7 @@ const downloadSchema = () => {
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadSchemaIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
@@ -434,6 +432,7 @@ const copySchema = () => {
|
||||
|
||||
copyToClipboard(schemaString.value)
|
||||
copySchemaIcon.value = "check"
|
||||
setTimeout(() => (copySchemaIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const handleUseHistory = (entry: GQLHistoryEntry) => {
|
||||
|
||||
@@ -22,10 +22,7 @@
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<div
|
||||
class="flex flex-col space-y-1 divide-y divide-dividerLight"
|
||||
role="menu"
|
||||
>
|
||||
<div class="flex flex-col" role="menu">
|
||||
<SmartItem
|
||||
:label="$t('state.none').toLowerCase()"
|
||||
:info-icon="contentType === null ? 'done' : ''"
|
||||
@@ -37,36 +34,19 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<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>
|
||||
<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>
|
||||
</tippy>
|
||||
<ButtonSecondary
|
||||
@@ -126,7 +106,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 { segmentedContentTypes } from "~/helpers/utils/contenttypes"
|
||||
import { knownContentTypes } from "~/helpers/utils/contenttypes"
|
||||
import {
|
||||
restContentType$,
|
||||
restHeaders$,
|
||||
@@ -139,6 +119,7 @@ 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="({ id, entry }, index) in workingParams"
|
||||
:key="`param=${id}-${index}`"
|
||||
v-for="(param, index) in workingParams"
|
||||
:key="`param-${index}`"
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
>
|
||||
<span>
|
||||
@@ -54,21 +54,21 @@
|
||||
/>
|
||||
</span>
|
||||
<SmartEnvInput
|
||||
v-model="entry.key"
|
||||
v-model="param.key"
|
||||
:placeholder="`${$t('count.parameter', { count: index + 1 })}`"
|
||||
@change="
|
||||
updateBodyParam(index, {
|
||||
key: $event,
|
||||
value: entry.value,
|
||||
active: entry.active,
|
||||
isFile: entry.isFile,
|
||||
value: param.value,
|
||||
active: param.active,
|
||||
isFile: param.isFile,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<div v-if="entry.isFile" class="file-chips-container hide-scrollbar">
|
||||
<div v-if="param.isFile" class="file-chips-container hide-scrollbar">
|
||||
<div class="space-x-2 file-chips-wrapper">
|
||||
<SmartFileChip
|
||||
v-for="(file, fileIndex) in entry.value"
|
||||
v-for="(file, fileIndex) in param.value"
|
||||
:key="`param-${index}-file-${fileIndex}`"
|
||||
>{{ file.name }}</SmartFileChip
|
||||
>
|
||||
@@ -76,14 +76,14 @@
|
||||
</div>
|
||||
<span v-else class="flex flex-1">
|
||||
<SmartEnvInput
|
||||
v-model="entry.value"
|
||||
v-model="param.value"
|
||||
:placeholder="`${$t('count.value', { count: index + 1 })}`"
|
||||
@change="
|
||||
updateBodyParam(index, {
|
||||
key: entry.key,
|
||||
key: param.key,
|
||||
value: $event,
|
||||
active: entry.active,
|
||||
isFile: entry.isFile,
|
||||
active: param.active,
|
||||
isFile: param.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, entry, $event)"
|
||||
@change="setRequestAttachment(index, param, $event)"
|
||||
/>
|
||||
</label>
|
||||
</span>
|
||||
@@ -105,15 +105,15 @@
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
entry.hasOwnProperty('active')
|
||||
? entry.active
|
||||
param.hasOwnProperty('active')
|
||||
? param.active
|
||||
? $t('action.turn_off')
|
||||
: $t('action.turn_on')
|
||||
: $t('action.turn_off')
|
||||
"
|
||||
:svg="
|
||||
entry.hasOwnProperty('active')
|
||||
? entry.active
|
||||
param.hasOwnProperty('active')
|
||||
? param.active
|
||||
? 'check-circle'
|
||||
: 'circle'
|
||||
: 'check-circle'
|
||||
@@ -121,10 +121,10 @@
|
||||
color="green"
|
||||
@click.native="
|
||||
updateBodyParam(index, {
|
||||
key: entry.key,
|
||||
value: entry.value,
|
||||
active: entry.hasOwnProperty('active') ? !entry.active : false,
|
||||
isFile: entry.isFile,
|
||||
key: param.key,
|
||||
value: param.value,
|
||||
active: param.hasOwnProperty('active') ? !param.active : false,
|
||||
isFile: param.isFile,
|
||||
})
|
||||
"
|
||||
/>
|
||||
@@ -164,9 +164,6 @@
|
||||
|
||||
<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"
|
||||
@@ -174,14 +171,10 @@ 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<
|
||||
@@ -189,32 +182,23 @@ const bodyParams = pluckRef<any, any>(useRESTRequestBody(), "body") as Ref<
|
||||
>
|
||||
|
||||
// The UI representation of the parameters list (has the empty end param)
|
||||
const workingParams = ref<WorkingFormDataKeyValue[]>([
|
||||
const workingParams = ref<FormDataKeyValue[]>([
|
||||
{
|
||||
id: idTicker.value++,
|
||||
entry: {
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
isFile: false,
|
||||
},
|
||||
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].entry.key !== ""
|
||||
) {
|
||||
if (paramsList.length > 0 && paramsList[paramsList.length - 1].key !== "") {
|
||||
workingParams.value.push({
|
||||
id: idTicker.value++,
|
||||
entry: {
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
isFile: false,
|
||||
},
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
isFile: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -224,37 +208,19 @@ watch(
|
||||
bodyParams,
|
||||
(newParamsList) => {
|
||||
// Sync should overwrite working params
|
||||
const filteredWorkingParams = pipe(
|
||||
workingParams.value,
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((e) => e.entry.key !== ""),
|
||||
O.map((e) => e.entry)
|
||||
)
|
||||
)
|
||||
const filteredWorkingParams = workingParams.value.filter(
|
||||
(e) => e.key !== ""
|
||||
)
|
||||
|
||||
if (!isEqual(newParamsList, filteredWorkingParams)) {
|
||||
workingParams.value = pipe(
|
||||
newParamsList,
|
||||
A.map((x) => ({ id: idTicker.value++, entry: x }))
|
||||
)
|
||||
workingParams.value = newParamsList
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(workingParams, (newWorkingParams) => {
|
||||
const fixedParams = pipe(
|
||||
newWorkingParams,
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((e) => e.entry.key !== ""),
|
||||
O.map((e) => e.entry)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const fixedParams = newWorkingParams.filter((e) => e.key !== "")
|
||||
if (!isEqual(bodyParams.value, fixedParams)) {
|
||||
bodyParams.value = fixedParams
|
||||
}
|
||||
@@ -262,19 +228,16 @@ watch(workingParams, (newWorkingParams) => {
|
||||
|
||||
const addBodyParam = () => {
|
||||
workingParams.value.push({
|
||||
id: idTicker.value++,
|
||||
entry: {
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
isFile: false,
|
||||
},
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
isFile: false,
|
||||
})
|
||||
}
|
||||
|
||||
const updateBodyParam = (index: number, entry: FormDataKeyValue) => {
|
||||
const updateBodyParam = (index: number, param: FormDataKeyValue) => {
|
||||
workingParams.value = workingParams.value.map((h, i) =>
|
||||
i === index ? { id: h.id, entry } : h
|
||||
i === index ? param : h
|
||||
)
|
||||
}
|
||||
|
||||
@@ -317,13 +280,10 @@ const clearContent = () => {
|
||||
// set params list to the initial state
|
||||
workingParams.value = [
|
||||
{
|
||||
id: idTicker.value++,
|
||||
entry: {
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
isFile: false,
|
||||
},
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
isFile: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
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 {
|
||||
@@ -119,10 +118,9 @@ 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 = {
|
||||
@@ -186,6 +184,7 @@ const copyRequestCode = () => {
|
||||
copyToClipboard(requestCode.value)
|
||||
copyIcon.value = "check"
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const searchQuery = ref("")
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
|
||||
<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"
|
||||
@@ -96,7 +95,7 @@ const handleImport = () => {
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const pasteIcon = refAutoReset<"clipboard" | "check">("clipboard", 1000)
|
||||
const pasteIcon = ref("clipboard")
|
||||
|
||||
const handlePaste = async () => {
|
||||
try {
|
||||
@@ -104,6 +103,7 @@ const handlePaste = async () => {
|
||||
if (text) {
|
||||
curl.value = text
|
||||
pasteIcon.value = "check"
|
||||
setTimeout(() => (pasteIcon.value = "clipboard"), 1000)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to copy: ", e)
|
||||
|
||||
@@ -61,7 +61,6 @@ 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"
|
||||
@@ -92,8 +91,7 @@ const rawParamsBody = pluckRef(
|
||||
>,
|
||||
"body"
|
||||
)
|
||||
|
||||
const prettifyIcon = refAutoReset<"wand" | "check" | "info">("wand", 1000)
|
||||
const prettifyIcon = ref("wand")
|
||||
|
||||
const rawInputEditorLang = computed(() =>
|
||||
getEditorLangForMimeType(props.contentType)
|
||||
@@ -150,5 +148,6 @@ const prettifyRequestBody = () => {
|
||||
prettifyIcon.value = "info"
|
||||
toast.error(`${t("error.json_prettify_invalid_body")}`)
|
||||
}
|
||||
setTimeout(() => (prettifyIcon.value = "wand"), 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -215,7 +215,6 @@ 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$,
|
||||
@@ -394,11 +393,7 @@ const clearContent = () => {
|
||||
resetRESTRequest()
|
||||
}
|
||||
|
||||
const copyLinkIcon = refAutoReset<"share-2" | "copy" | "check">(
|
||||
hasNavigatorShare ? "share-2" : "copy",
|
||||
1000
|
||||
)
|
||||
|
||||
const copyLinkIcon = hasNavigatorShare ? ref("share-2") : ref("copy")
|
||||
const shareLink = ref<string | null>("")
|
||||
const fetchingShareLink = ref(false)
|
||||
|
||||
@@ -453,6 +448,7 @@ 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,7 +2,6 @@
|
||||
<SmartTabs
|
||||
v-model="selectedRealtimeTab"
|
||||
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTab
|
||||
:id="'params'"
|
||||
|
||||
@@ -117,21 +117,9 @@
|
||||
<span class="text-secondary"> {{ t("response.time") }}: </span>
|
||||
{{ `${response.meta.responseDuration} ms` }}
|
||||
</span>
|
||||
<span
|
||||
v-if="response.meta && response.meta.responseSize"
|
||||
v-tippy="
|
||||
readableResponseSize
|
||||
? { theme: 'tooltip' }
|
||||
: { onShow: () => false }
|
||||
"
|
||||
:title="`${response.meta.responseSize} B`"
|
||||
>
|
||||
<span v-if="response.meta && response.meta.responseSize">
|
||||
<span class="text-secondary"> {{ t("response.size") }}: </span>
|
||||
{{
|
||||
readableResponseSize
|
||||
? readableResponseSize
|
||||
: `${response.meta.responseSize} B`
|
||||
}}
|
||||
{{ `${response.meta.responseSize} B` }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,29 +141,6 @@ 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,7 +3,6 @@
|
||||
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,13 +12,10 @@
|
||||
<span class="text-secondaryDark">
|
||||
{{ env.key }}
|
||||
</span>
|
||||
<span class="text-secondaryDark pl-2 break-all">
|
||||
<span class="text-secondaryDark">
|
||||
{{ ` \xA0 — \xA0 ${env.value}` }}
|
||||
</span>
|
||||
<span
|
||||
v-if="status === 'updations'"
|
||||
class="text-secondaryLight px-2 break-all"
|
||||
>
|
||||
<span v-if="status === 'updations'" class="text-secondaryLight">
|
||||
{{ ` \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,11 +39,12 @@ const props = defineProps<{
|
||||
headers: Array<HoppRESTHeader>
|
||||
}>()
|
||||
|
||||
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
|
||||
const copyIcon = ref("copy")
|
||||
|
||||
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 { refAutoReset } from "@vueuse/core"
|
||||
import { ref } from "@nuxtjs/composition-api"
|
||||
import { HoppRESTHeader } from "~/../hoppscotch-data/dist"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useI18n, useToast } from "~/helpers/utils/composables"
|
||||
@@ -41,11 +41,12 @@ defineProps<{
|
||||
header: HoppRESTHeader
|
||||
}>()
|
||||
|
||||
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
|
||||
const copyIcon = ref("copy")
|
||||
|
||||
const copyHeader = (headerValue: string) => {
|
||||
copyToClipboard(headerValue)
|
||||
copyIcon.value = "check"
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
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,15 +1,12 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="response.type === 'success' || response.type === 'fail'"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ t("response.body") }}
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -18,14 +15,6 @@
|
||||
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"
|
||||
@@ -44,47 +33,7 @@
|
||||
/>
|
||||
</div>
|
||||
</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 ref="jsonResponse" class="flex flex-col flex-1"></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"
|
||||
@@ -193,10 +142,8 @@
|
||||
<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"
|
||||
@@ -218,51 +165,16 @@ const props = defineProps<{
|
||||
|
||||
const { responseBodyText } = useResponseBody(props.response)
|
||||
|
||||
const toggleFilter = ref(false)
|
||||
const filterQueryText = ref("")
|
||||
const { copyIcon, copyResponse } = useCopyResponse(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 { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
"application/json",
|
||||
responseBodyText
|
||||
)
|
||||
|
||||
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(
|
||||
jsonResponseBodyText.value,
|
||||
E.getOrElse(() => responseBodyText.value),
|
||||
responseBodyText.value,
|
||||
O.tryCatchK(LJSON.parse),
|
||||
O.map((val) => LJSON.stringify(val, undefined, 2)),
|
||||
O.getOrElse(() => responseBodyText.value)
|
||||
@@ -277,38 +189,6 @@ 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)
|
||||
@@ -347,11 +227,6 @@ const outlinePath = computed(() =>
|
||||
O.getOrElseW(() => null)
|
||||
)
|
||||
)
|
||||
|
||||
const toggleFilterState = () => {
|
||||
filterQueryText.value = ""
|
||||
toggleFilter.value = !toggleFilter.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -65,7 +65,6 @@ 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"
|
||||
@@ -94,8 +93,7 @@ const requestMethodLabels = {
|
||||
} as const
|
||||
|
||||
const timeStampRef = ref()
|
||||
|
||||
const copyIconRefs = refAutoReset<"copy" | "check">("copy", 1000)
|
||||
const copyIconRefs = ref<"copy" | "check">("copy")
|
||||
|
||||
const parseShortcodeRequest = computed(() =>
|
||||
pipe(props.shortcode.request, JSON.parse, translateToNewRequest)
|
||||
@@ -120,6 +118,7 @@ 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-upperSecondaryStickyFold"
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
@@ -113,7 +113,6 @@ 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"
|
||||
@@ -146,8 +145,7 @@ const toast = useToast()
|
||||
|
||||
const linewrapEnabled = ref(true)
|
||||
const wsCommunicationBody = ref<HTMLElement>()
|
||||
|
||||
const prettifyIcon = refAutoReset<"wand" | "check" | "info">("wand", 1000)
|
||||
const prettifyIcon = ref<"wand" | "check" | "info">("wand")
|
||||
|
||||
const knownContentTypes = {
|
||||
JSON: "application/ld+json",
|
||||
@@ -218,5 +216,6 @@ const prettifyRequestBody = () => {
|
||||
prettifyIcon.value = "info"
|
||||
toast.error(`${t("error.json_prettify_invalid_body")}`)
|
||||
}
|
||||
setTimeout(() => (prettifyIcon.value = "wand"), 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
{{ title }}
|
||||
</label>
|
||||
<div>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.search')"
|
||||
svg="search"
|
||||
@click.native="toggleSearch = !toggleSearch"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.delete')"
|
||||
@@ -37,6 +43,26 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="toggleSearch"
|
||||
class="w-full p-2 sticky top-0 z-10 text-center border-b border-dividerLight"
|
||||
>
|
||||
<span
|
||||
class="bg-primaryLight border-divider text-secondaryDark rounded inline-flex"
|
||||
>
|
||||
<ButtonSecondary svg="search" class="item-center" />
|
||||
|
||||
<input
|
||||
id=""
|
||||
v-model="pattern"
|
||||
type="text"
|
||||
placeholder="Enter search pattern"
|
||||
class="rounded w-64 bg-primaryLight text-secondaryDark text-center"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="log.length !== 0"
|
||||
ref="logs"
|
||||
@@ -46,9 +72,10 @@
|
||||
class="flex flex-col h-auto h-full border-r divide-y divide-dividerLight border-dividerLight"
|
||||
>
|
||||
<RealtimeLogEntry
|
||||
v-for="(entry, index) in log"
|
||||
v-for="(entry, index) in logEntries"
|
||||
:key="`entry-${index}`"
|
||||
:entry="entry"
|
||||
:highlight-regex="pattern === '' ? undefined : patternRegex"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,8 +83,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, PropType, computed, watch } from "@nuxtjs/composition-api"
|
||||
import { ref, computed, watch } from "@nuxtjs/composition-api"
|
||||
import { useThrottleFn, useScroll } from "@vueuse/core"
|
||||
import { regexEscape } from "~/helpers/functional/regex"
|
||||
import { useI18n } from "~/helpers/utils/composables"
|
||||
|
||||
export type LogEntryData = {
|
||||
@@ -68,13 +96,7 @@ export type LogEntryData = {
|
||||
event: "connecting" | "connected" | "disconnected" | "error"
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
log: { type: Array as PropType<LogEntryData[]>, default: () => [] },
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
})
|
||||
const props = defineProps<{ log: LogEntryData[]; title: string }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete"): void
|
||||
@@ -121,6 +143,19 @@ const toggleAutoscroll = () => {
|
||||
autoScrollEnabled.value = !autoScrollEnabled.value
|
||||
}
|
||||
|
||||
const pattern = ref("")
|
||||
const toggleSearch = ref(false)
|
||||
|
||||
const patternRegex = computed(
|
||||
() => new RegExp(regexEscape(pattern.value), "gi")
|
||||
)
|
||||
|
||||
const logEntries = computed(() => {
|
||||
if (patternRegex.value) {
|
||||
return props.log.filter((entry) => entry.payload.match(patternRegex.value))
|
||||
} else return props.log
|
||||
})
|
||||
|
||||
const toggleAutoscrollColor = computed(() =>
|
||||
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
|
||||
)
|
||||
|
||||
@@ -31,7 +31,13 @@
|
||||
<span v-if="entry.prefix !== undefined" class="!inline">{{
|
||||
entry.prefix
|
||||
}}</span>
|
||||
{{ entry.payload }}
|
||||
<span
|
||||
v-for="(section, index) in highlightingSections"
|
||||
:key="index"
|
||||
class="!inline"
|
||||
:class="section.mode === 'highlight' ? 'highlight' : ''"
|
||||
>{{ section.text }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,11 +57,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!minimized" class="overflow-hidden bg-primaryLight">
|
||||
<SmartTabs
|
||||
v-model="selectedTab"
|
||||
styles="bg-primaryLight"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<SmartTabs v-model="selectedTab" styles="bg-primaryLight">
|
||||
<SmartTab v-if="isJSON(entry.payload)" id="json" label="JSON" />
|
||||
<SmartTab id="raw" label="Raw" />
|
||||
</SmartTabs>
|
||||
@@ -207,11 +209,12 @@ 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 { refAutoReset, useTimeAgo } from "@vueuse/core"
|
||||
import { useTimeAgo } from "@vueuse/core"
|
||||
import { LogEntryData } from "./Log.vue"
|
||||
import { useI18n } from "~/helpers/utils/composables"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { isJSON } from "~/helpers/functional/json"
|
||||
import { regexFindAllMatches } from "~/helpers/functional/regex"
|
||||
import useCopyResponse from "~/helpers/lenses/composables/useCopyResponse"
|
||||
import useDownloadResponse from "~/helpers/lenses/composables/useDownloadResponse"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
@@ -224,12 +227,55 @@ import {
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{ entry: LogEntryData }>()
|
||||
const props = defineProps<{
|
||||
entry: LogEntryData
|
||||
highlightRegex?: RegExp
|
||||
}>()
|
||||
const outlineOptions = ref<any | null>(null)
|
||||
const editor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
const logPayload = computed(() => props.entry.payload)
|
||||
|
||||
type HighlightSection = {
|
||||
mode: "normal" | "highlight"
|
||||
text: string
|
||||
}
|
||||
|
||||
const highlightingSections = computed<HighlightSection[]>(() => {
|
||||
if (!props.highlightRegex)
|
||||
return [{ mode: "normal", text: props.entry.payload }]
|
||||
|
||||
const line = props.entry.payload.split("\n")[0]
|
||||
|
||||
const ranges = pipe(line, regexFindAllMatches(props.highlightRegex))
|
||||
|
||||
const result: HighlightSection[] = []
|
||||
let point = 0
|
||||
|
||||
ranges.forEach(({ startIndex, endIndex }) => {
|
||||
if (point < startIndex)
|
||||
result.push({
|
||||
mode: "normal",
|
||||
text: line.slice(point, startIndex),
|
||||
})
|
||||
|
||||
result.push({
|
||||
mode: "highlight",
|
||||
text: line.slice(startIndex, endIndex + 1),
|
||||
})
|
||||
|
||||
point = endIndex + 1
|
||||
})
|
||||
|
||||
if (point < line.length)
|
||||
result.push({
|
||||
mode: "normal",
|
||||
text: line.slice(point, line.length),
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const selectedTab = ref<"json" | "raw">(
|
||||
isJSON(props.entry.payload) ? "json" : "raw"
|
||||
)
|
||||
@@ -314,11 +360,11 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
logPayload
|
||||
)
|
||||
|
||||
const copyQueryIcon = refAutoReset<"copy" | "check">("copy", 1000)
|
||||
|
||||
const copyQueryIcon = ref("copy")
|
||||
const copyQuery = (entry: string) => {
|
||||
copyToClipboard(entry)
|
||||
copyQueryIcon.value = "check"
|
||||
setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
// Relative Time
|
||||
@@ -389,4 +435,8 @@ const iconName = computed(() => ICONS[props.entry.source].iconName)
|
||||
.ts-font {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: yellow;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -85,13 +85,11 @@
|
||||
|
||||
<SmartTabs
|
||||
v-model="selectedTab"
|
||||
styles="sticky bg-primary top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||
>
|
||||
<SmartTab
|
||||
:id="'communication'"
|
||||
:label="`${t('websocket.communication')}`"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<RealtimeCommunication
|
||||
:show-event-field="true"
|
||||
@@ -101,7 +99,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-upperSecondaryStickyFold"
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
@@ -37,8 +37,7 @@
|
||||
</div>
|
||||
<SmartTabs
|
||||
v-model="selectedTab"
|
||||
styles="sticky bg-primary top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||
>
|
||||
<SmartTab
|
||||
:id="'communication'"
|
||||
@@ -51,7 +50,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-upperSecondaryStickyFold"
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ t("websocket.protocols") }}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="shouldRender" v-show="active" class="flex flex-col flex-1">
|
||||
<div v-show="active" class="flex flex-col flex-1">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -33,24 +33,11 @@ const tabMeta = computed<TabMeta>(() => ({
|
||||
label: props.label,
|
||||
}))
|
||||
|
||||
const {
|
||||
activeTabID,
|
||||
renderInactive,
|
||||
addTabEntry,
|
||||
updateTabEntry,
|
||||
removeTabEntry,
|
||||
} = inject<TabProvider>("tabs-system")!
|
||||
const { activeTabID, 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,8 +80,6 @@ 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
|
||||
@@ -93,10 +91,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
renderInactiveTabs: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -150,7 +144,6 @@ 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,
|
||||
getEnvironment,
|
||||
getEnviroment,
|
||||
getGlobalVariables,
|
||||
setGlobalEnvVariables,
|
||||
updateEnvironment,
|
||||
@@ -97,7 +97,7 @@ export const runRESTRequest$ = (): TaskEither<
|
||||
setGlobalEnvVariables(runResult.right.envs.global)
|
||||
|
||||
if (environmentsStore.value.currentEnvironmentIndex !== -1) {
|
||||
const env = getEnvironment(
|
||||
const env = getEnviroment(
|
||||
environmentsStore.value.currentEnvironmentIndex
|
||||
)
|
||||
updateEnvironment(
|
||||
|
||||
@@ -45,23 +45,28 @@ import {
|
||||
} from "~/helpers/fb/auth"
|
||||
|
||||
const BACKEND_GQL_URL =
|
||||
process.env.BACKEND_GQL_URL ?? "https://api.hoppscotch.io/graphql"
|
||||
const BACKEND_WS_URL =
|
||||
process.env.BACKEND_WS_URL ?? "wss://api.hoppscotch.io/graphql"
|
||||
process.env.context === "production"
|
||||
? "https://api.hoppscotch.io/graphql"
|
||||
: "https://api.hoppscotch.io/graphql"
|
||||
|
||||
// const storage = makeDefaultStorage({
|
||||
// idbName: "hoppcache-v1",
|
||||
// maxAge: 7,
|
||||
// })
|
||||
|
||||
const subscriptionClient = new SubscriptionClient(BACKEND_WS_URL, {
|
||||
reconnect: true,
|
||||
connectionParams: () => {
|
||||
return {
|
||||
authorization: `Bearer ${authIdToken$.value}`,
|
||||
}
|
||||
},
|
||||
})
|
||||
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}`,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
authIdToken$.subscribe(() => {
|
||||
subscriptionClient.client?.close()
|
||||
|
||||
@@ -158,11 +158,14 @@ const getXMLBody = (rawData: string) =>
|
||||
O.alt(() => O.some(rawData))
|
||||
)
|
||||
|
||||
const getFormattedJSON = flow(
|
||||
safeParseJSON,
|
||||
O.map((parsedJSON) => JSON.stringify(parsedJSON, null, 2)),
|
||||
O.getOrElse(() => "{ }")
|
||||
)
|
||||
const getFormattedJSON = (jsonString: string) =>
|
||||
pipe(
|
||||
jsonString.replaceAll('\\"', '"'),
|
||||
safeParseJSON,
|
||||
O.map((parsedJSON) => JSON.stringify(parsedJSON, null, 2)),
|
||||
O.getOrElse(() => "{ }"),
|
||||
O.of
|
||||
)
|
||||
|
||||
const getXWWWFormUrlEncodedBody = flow(
|
||||
decodeURIComponent,
|
||||
@@ -188,7 +191,7 @@ export function parseBody(
|
||||
case "application/ld+json":
|
||||
case "application/vnd.api+json":
|
||||
case "application/json":
|
||||
return O.some(getFormattedJSON(rawData))
|
||||
return getFormattedJSON(rawData)
|
||||
|
||||
case "application/x-www-form-urlencoded":
|
||||
return getXWWWFormUrlEncodedBody(rawData)
|
||||
|
||||
@@ -38,6 +38,7 @@ 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 = {
|
||||
@@ -237,7 +238,7 @@ export function useCodemirror(
|
||||
),
|
||||
lineWrapping.of(
|
||||
options.extendedEditorConfig.lineWrapping
|
||||
? [EditorView.lineWrapping]
|
||||
? [IndentedLineWrapPlugin]
|
||||
: []
|
||||
),
|
||||
keymap.of([
|
||||
@@ -324,7 +325,7 @@ export function useCodemirror(
|
||||
(newMode) => {
|
||||
view.value?.dispatch({
|
||||
effects: lineWrapping.reconfigure(
|
||||
newMode ? [EditorView.lineWrapping] : []
|
||||
newMode ? [EditorView.lineWrapping, IndentedLineWrapPlugin] : []
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
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,
|
||||
]
|
||||
@@ -9,14 +9,6 @@ import { flow } from "fp-ts/function"
|
||||
export const safeParseJSON = (str: string): O.Option<object> =>
|
||||
O.tryCatch(() => JSON.parse(str))
|
||||
|
||||
/**
|
||||
* Generates a prettified JSON representation of an object
|
||||
* @param obj The object to get the representation of
|
||||
* @returns The prettified JSON string of the object
|
||||
*/
|
||||
export const prettyPrintJSON = (obj: unknown): O.Option<string> =>
|
||||
O.tryCatch(() => JSON.stringify(obj, null, "\t"))
|
||||
|
||||
/**
|
||||
* Checks if given string is a JSON string
|
||||
* @param str Raw string to be checked
|
||||
|
||||
33
packages/hoppscotch-app/helpers/functional/regex.ts
Normal file
33
packages/hoppscotch-app/helpers/functional/regex.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Escapes special regex characters in a string.
|
||||
* @param text The string to transform
|
||||
* @returns Escaped string
|
||||
*/
|
||||
export const regexEscape = (text: string) =>
|
||||
text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")
|
||||
|
||||
export type RegexMatch = {
|
||||
matchString: string
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the regex match ranges for a given input
|
||||
* @param regex The Regular Expression to find from
|
||||
* @param input The input string to get match ranges from
|
||||
* @returns An array of `RegexMatch` objects giving info about the matches
|
||||
*/
|
||||
export const regexFindAllMatches = (regex: RegExp) => (input: string) => {
|
||||
const matches: RegexMatch[] = []
|
||||
|
||||
// eslint-disable-next-line no-cond-assign, prettier/prettier
|
||||
for (let match; match = regex.exec(input); match !== null)
|
||||
matches.push({
|
||||
matchString: match[0],
|
||||
startIndex: match.index,
|
||||
endIndex: match.index + match[0].length - 1,
|
||||
})
|
||||
|
||||
return matches
|
||||
}
|
||||
@@ -24,12 +24,8 @@ import * as S from "fp-ts/string"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as RA from "fp-ts/ReadonlyArray"
|
||||
import { step } from "../../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "../"
|
||||
import { generateRequestBodyExampleFromMediaObject as generateExampleV31 } from "./exampleV31"
|
||||
import { generateRequestBodyExampleFromMediaObject as generateExampleV3 } from "./exampleV3"
|
||||
import { generateRequestBodyExampleFromOpenAPIV2Body } from "./exampleV2"
|
||||
import { prettyPrintJSON } from "~/helpers/functional/json"
|
||||
import { step } from "../steps"
|
||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
export const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const
|
||||
|
||||
@@ -118,12 +114,8 @@ const parseOpenAPIV2Body = (op: OpenAPIV2.OperationObject): HoppRESTReqBody => {
|
||||
if (
|
||||
obj !== "multipart/form-data" &&
|
||||
obj !== "application/x-www-form-urlencoded"
|
||||
) {
|
||||
return {
|
||||
contentType: obj as any,
|
||||
body: generateRequestBodyExampleFromOpenAPIV2Body(op),
|
||||
}
|
||||
}
|
||||
)
|
||||
return { contentType: obj as any, body: "" }
|
||||
|
||||
const formDataValues = pipe(
|
||||
(op.parameters ?? []) as OpenAPIV2.Parameter[],
|
||||
@@ -186,8 +178,7 @@ const parseOpenAPIV3BodyFormData = (
|
||||
}
|
||||
|
||||
const parseOpenAPIV3Body = (
|
||||
op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject,
|
||||
isV31Request: boolean
|
||||
op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject
|
||||
): HoppRESTReqBody => {
|
||||
const objs = Object.entries(
|
||||
(
|
||||
@@ -206,20 +197,11 @@ const parseOpenAPIV3Body = (
|
||||
OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject
|
||||
] = objs[0]
|
||||
|
||||
const exampleBody = pipe(
|
||||
prettyPrintJSON(
|
||||
isV31Request
|
||||
? generateExampleV31(media as OpenAPIV31.MediaTypeObject)
|
||||
: generateExampleV3(media as OpenAPIV3.MediaTypeObject)
|
||||
),
|
||||
O.getOrElse(() => "")
|
||||
)
|
||||
|
||||
return contentType in knownContentTypes
|
||||
? contentType === "multipart/form-data" ||
|
||||
contentType === "application/x-www-form-urlencoded"
|
||||
? parseOpenAPIV3BodyFormData(contentType, media)
|
||||
: { contentType: contentType as any, body: exampleBody }
|
||||
: { contentType: contentType as any, body: "" }
|
||||
: { contentType: null, body: null }
|
||||
}
|
||||
|
||||
@@ -231,20 +213,12 @@ const isOpenAPIV3Operation = (
|
||||
typeof doc.openapi === "string" &&
|
||||
doc.openapi.startsWith("3.")
|
||||
|
||||
const isOpenAPIV31Operation = (
|
||||
doc: OpenAPI.Document,
|
||||
op: OpenAPIOperationType
|
||||
): op is OpenAPIV31.OperationObject =>
|
||||
objectHasProperty(doc, "openapi") &&
|
||||
typeof doc.openapi === "string" &&
|
||||
doc.openapi.startsWith("3.1")
|
||||
|
||||
const parseOpenAPIBody = (
|
||||
doc: OpenAPI.Document,
|
||||
op: OpenAPIOperationType
|
||||
): HoppRESTReqBody =>
|
||||
isOpenAPIV3Operation(doc, op)
|
||||
? parseOpenAPIV3Body(op, isOpenAPIV31Operation(doc, op))
|
||||
? parseOpenAPIV3Body(op)
|
||||
: parseOpenAPIV2Body(op)
|
||||
|
||||
const resolveOpenAPIV3SecurityObj = (
|
||||
@@ -1,185 +0,0 @@
|
||||
import { OpenAPIV2 } from "openapi-types"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import * as A from "fp-ts/Array"
|
||||
import { prettyPrintJSON } from "~/helpers/functional/json"
|
||||
|
||||
type PrimitiveSchemaType = "string" | "integer" | "number" | "boolean"
|
||||
|
||||
type SchemaType = "array" | "object" | PrimitiveSchemaType
|
||||
|
||||
type PrimitiveRequestBodyExample = number | string | boolean
|
||||
|
||||
type RequestBodyExample =
|
||||
| { [name: string]: RequestBodyExample }
|
||||
| Array<RequestBodyExample>
|
||||
| PrimitiveRequestBodyExample
|
||||
|
||||
const getPrimitiveTypePlaceholder = (
|
||||
schemaType: PrimitiveSchemaType
|
||||
): PrimitiveRequestBodyExample => {
|
||||
switch (schemaType) {
|
||||
case "string":
|
||||
return "string"
|
||||
case "integer":
|
||||
case "number":
|
||||
return 1
|
||||
case "boolean":
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const getSchemaTypeFromSchemaObject = (
|
||||
schema: OpenAPIV2.SchemaObject
|
||||
): O.Option<SchemaType> =>
|
||||
pipe(
|
||||
schema.type,
|
||||
O.fromNullable,
|
||||
O.map(
|
||||
(schemaType) =>
|
||||
(Array.isArray(schemaType) ? schemaType[0] : schemaType) as SchemaType
|
||||
)
|
||||
)
|
||||
|
||||
const isSchemaTypePrimitive = (
|
||||
schemaType: string
|
||||
): schemaType is PrimitiveSchemaType =>
|
||||
["string", "integer", "number", "boolean"].includes(schemaType)
|
||||
|
||||
const isSchemaTypeArray = (schemaType: string): schemaType is "array" =>
|
||||
schemaType === "array"
|
||||
|
||||
const isSchemaTypeObject = (schemaType: string): schemaType is "object" =>
|
||||
schemaType === "object"
|
||||
|
||||
const getSampleEnumValueOrPlaceholder = (
|
||||
schema: OpenAPIV2.SchemaObject
|
||||
): RequestBodyExample =>
|
||||
pipe(
|
||||
schema.enum,
|
||||
O.fromNullable,
|
||||
O.map((enums) => enums[0] as RequestBodyExample),
|
||||
O.altW(() =>
|
||||
pipe(
|
||||
schema,
|
||||
getSchemaTypeFromSchemaObject,
|
||||
O.filter(isSchemaTypePrimitive),
|
||||
O.map(getPrimitiveTypePlaceholder)
|
||||
)
|
||||
),
|
||||
O.getOrElseW(() => "")
|
||||
)
|
||||
|
||||
const generateExampleArrayFromOpenAPIV2ItemsObject = (
|
||||
items: OpenAPIV2.ItemsObject
|
||||
): RequestBodyExample =>
|
||||
// ItemsObject can not hold type "object"
|
||||
// https://swagger.io/specification/v2/#itemsObject
|
||||
|
||||
// TODO : Handle array of objects
|
||||
// https://stackoverflow.com/questions/60490974/how-to-define-an-array-of-objects-in-openapi-2-0
|
||||
|
||||
pipe(
|
||||
items,
|
||||
O.fromPredicate(
|
||||
flow((items) => items.type as SchemaType, isSchemaTypePrimitive)
|
||||
),
|
||||
O.map(
|
||||
flow(getSampleEnumValueOrPlaceholder, (arrayItem) => [
|
||||
arrayItem,
|
||||
arrayItem,
|
||||
])
|
||||
),
|
||||
O.getOrElse(() =>
|
||||
// If the type is not primitive, it is "array"
|
||||
// items property is required if type is array
|
||||
[
|
||||
generateExampleArrayFromOpenAPIV2ItemsObject(
|
||||
items.items as OpenAPIV2.ItemsObject
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
const generateRequestBodyExampleFromOpenAPIV2BodySchema = (
|
||||
schema: OpenAPIV2.SchemaObject
|
||||
): RequestBodyExample => {
|
||||
if (schema.example) return schema.example as RequestBodyExample
|
||||
|
||||
const primitiveTypeExample = pipe(
|
||||
schema,
|
||||
O.fromPredicate(
|
||||
flow(
|
||||
getSchemaTypeFromSchemaObject,
|
||||
O.map(isSchemaTypePrimitive),
|
||||
O.getOrElseW(() => false) // No schema type found in the schema object, assume non-primitive
|
||||
)
|
||||
),
|
||||
O.map(getSampleEnumValueOrPlaceholder) // Use enum or placeholder to populate primitive field
|
||||
)
|
||||
|
||||
if (O.isSome(primitiveTypeExample)) return primitiveTypeExample.value
|
||||
|
||||
const arrayTypeExample = pipe(
|
||||
schema,
|
||||
O.fromPredicate(
|
||||
flow(
|
||||
getSchemaTypeFromSchemaObject,
|
||||
O.map(isSchemaTypeArray),
|
||||
O.getOrElseW(() => false) // No schema type found in the schema object, assume type to be different from array
|
||||
)
|
||||
),
|
||||
O.map((schema) => schema.items as OpenAPIV2.ItemsObject),
|
||||
O.map(generateExampleArrayFromOpenAPIV2ItemsObject)
|
||||
)
|
||||
|
||||
if (O.isSome(arrayTypeExample)) return arrayTypeExample.value
|
||||
|
||||
return pipe(
|
||||
schema,
|
||||
O.fromPredicate(
|
||||
flow(
|
||||
getSchemaTypeFromSchemaObject,
|
||||
O.map(isSchemaTypeObject),
|
||||
O.getOrElseW(() => false)
|
||||
)
|
||||
),
|
||||
O.chain((schema) =>
|
||||
pipe(
|
||||
schema.properties,
|
||||
O.fromNullable,
|
||||
O.map(
|
||||
(properties) =>
|
||||
Object.entries(properties) as [string, OpenAPIV2.SchemaObject][]
|
||||
)
|
||||
)
|
||||
),
|
||||
O.getOrElseW(() => [] as [string, OpenAPIV2.SchemaObject][]),
|
||||
A.reduce(
|
||||
{} as { [name: string]: RequestBodyExample },
|
||||
(aggregatedExample, property) => {
|
||||
const example = generateRequestBodyExampleFromOpenAPIV2BodySchema(
|
||||
property[1]
|
||||
)
|
||||
aggregatedExample[property[0]] = example
|
||||
return aggregatedExample
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export const generateRequestBodyExampleFromOpenAPIV2Body = (
|
||||
op: OpenAPIV2.OperationObject
|
||||
): string =>
|
||||
pipe(
|
||||
(op.parameters ?? []) as OpenAPIV2.Parameter[],
|
||||
A.findFirst((param) => param.in === "body"),
|
||||
O.map(
|
||||
flow(
|
||||
(parameter) => parameter.schema,
|
||||
generateRequestBodyExampleFromOpenAPIV2BodySchema
|
||||
)
|
||||
),
|
||||
O.chain(prettyPrintJSON),
|
||||
O.getOrElse(() => "")
|
||||
)
|
||||
@@ -1,109 +0,0 @@
|
||||
import { OpenAPIV3 } from "openapi-types"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { tupleToRecord } from "~/helpers/functional/record"
|
||||
|
||||
type SchemaType =
|
||||
| OpenAPIV3.ArraySchemaObjectType
|
||||
| OpenAPIV3.NonArraySchemaObjectType
|
||||
|
||||
type PrimitiveSchemaType = Exclude<SchemaType, "array" | "object">
|
||||
|
||||
type PrimitiveRequestBodyExample = string | number | boolean | null
|
||||
|
||||
type RequestBodyExample =
|
||||
| PrimitiveRequestBodyExample
|
||||
| Array<RequestBodyExample>
|
||||
| { [name: string]: RequestBodyExample }
|
||||
|
||||
const isSchemaTypePrimitive = (
|
||||
schemaType: SchemaType
|
||||
): schemaType is PrimitiveSchemaType =>
|
||||
!["array", "object"].includes(schemaType)
|
||||
|
||||
const getPrimitiveTypePlaceholder = (
|
||||
primitiveType: PrimitiveSchemaType
|
||||
): PrimitiveRequestBodyExample => {
|
||||
switch (primitiveType) {
|
||||
case "number":
|
||||
return 0.0
|
||||
case "integer":
|
||||
return 0
|
||||
case "string":
|
||||
return "string"
|
||||
case "boolean":
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Use carefully, call only when type is primitive
|
||||
// TODO(agarwal): Use Enum values, if any
|
||||
const generatePrimitiveRequestBodyExample = (
|
||||
schemaObject: OpenAPIV3.NonArraySchemaObject
|
||||
): RequestBodyExample =>
|
||||
getPrimitiveTypePlaceholder(schemaObject.type as PrimitiveSchemaType)
|
||||
|
||||
// Use carefully, call only when type is object
|
||||
const generateObjectRequestBodyExample = (
|
||||
schemaObject: OpenAPIV3.NonArraySchemaObject
|
||||
): RequestBodyExample =>
|
||||
pipe(
|
||||
schemaObject.properties,
|
||||
O.fromNullable,
|
||||
O.map(Object.entries),
|
||||
O.getOrElseW(() => [] as [string, OpenAPIV3.SchemaObject][]),
|
||||
tupleToRecord
|
||||
)
|
||||
|
||||
const generateArrayRequestBodyExample = (
|
||||
schemaObject: OpenAPIV3.ArraySchemaObject
|
||||
): RequestBodyExample => [
|
||||
generateRequestBodyExampleFromSchemaObject(
|
||||
schemaObject.items as OpenAPIV3.SchemaObject
|
||||
),
|
||||
]
|
||||
|
||||
const generateRequestBodyExampleFromSchemaObject = (
|
||||
schemaObject: OpenAPIV3.SchemaObject
|
||||
): RequestBodyExample => {
|
||||
// TODO: Handle schema objects with allof
|
||||
if (schemaObject.example) return schemaObject.example as RequestBodyExample
|
||||
|
||||
// If request body can be oneof or allof several schema, choose the first schema to generate an example
|
||||
if (schemaObject.oneOf)
|
||||
return generateRequestBodyExampleFromSchemaObject(
|
||||
schemaObject.oneOf[0] as OpenAPIV3.SchemaObject
|
||||
)
|
||||
if (schemaObject.anyOf)
|
||||
return generateRequestBodyExampleFromSchemaObject(
|
||||
schemaObject.anyOf[0] as OpenAPIV3.SchemaObject
|
||||
)
|
||||
|
||||
if (!schemaObject.type) return ""
|
||||
|
||||
if (isSchemaTypePrimitive(schemaObject.type))
|
||||
return generatePrimitiveRequestBodyExample(
|
||||
schemaObject as OpenAPIV3.NonArraySchemaObject
|
||||
)
|
||||
|
||||
if (schemaObject.type === "object")
|
||||
return generateObjectRequestBodyExample(
|
||||
schemaObject as OpenAPIV3.NonArraySchemaObject
|
||||
)
|
||||
|
||||
return generateArrayRequestBodyExample(
|
||||
schemaObject as OpenAPIV3.ArraySchemaObject
|
||||
)
|
||||
}
|
||||
|
||||
export const generateRequestBodyExampleFromMediaObject = (
|
||||
mediaObject: OpenAPIV3.MediaTypeObject
|
||||
): RequestBodyExample => {
|
||||
if (mediaObject.example) return mediaObject.example as RequestBodyExample
|
||||
if (mediaObject.examples) return mediaObject.examples[0] as RequestBodyExample
|
||||
return mediaObject.schema
|
||||
? generateRequestBodyExampleFromSchemaObject(
|
||||
mediaObject.schema as OpenAPIV3.SchemaObject
|
||||
)
|
||||
: ""
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import { OpenAPIV3_1 as OpenAPIV31 } from "openapi-types"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as A from "fp-ts/Array"
|
||||
|
||||
type MixedArraySchemaType = (
|
||||
| OpenAPIV31.ArraySchemaObjectType
|
||||
| OpenAPIV31.NonArraySchemaObjectType
|
||||
)[]
|
||||
|
||||
type SchemaType =
|
||||
| OpenAPIV31.ArraySchemaObjectType
|
||||
| OpenAPIV31.NonArraySchemaObjectType
|
||||
| MixedArraySchemaType
|
||||
|
||||
type PrimitiveSchemaType = Exclude<
|
||||
OpenAPIV31.NonArraySchemaObjectType,
|
||||
"object"
|
||||
>
|
||||
|
||||
type PrimitiveRequestBodyExample = string | number | boolean | null
|
||||
|
||||
type RequestBodyExample =
|
||||
| PrimitiveRequestBodyExample
|
||||
| Array<RequestBodyExample>
|
||||
| { [name: string]: RequestBodyExample }
|
||||
|
||||
const isSchemaTypePrimitive = (
|
||||
schemaType: SchemaType
|
||||
): schemaType is PrimitiveSchemaType =>
|
||||
!Array.isArray(schemaType) && !["array", "object"].includes(schemaType)
|
||||
|
||||
const getPrimitiveTypePlaceholder = (
|
||||
primitiveType: PrimitiveSchemaType
|
||||
): PrimitiveRequestBodyExample => {
|
||||
switch (primitiveType) {
|
||||
case "number":
|
||||
return 0.0
|
||||
case "integer":
|
||||
return 0
|
||||
case "string":
|
||||
return "string"
|
||||
case "boolean":
|
||||
return true
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Use carefully, the schema type should necessarily be primitive
|
||||
// TODO(agarwal): Use Enum values, if any
|
||||
const generatePrimitiveRequestBodyExample = (
|
||||
schemaObject: OpenAPIV31.NonArraySchemaObject
|
||||
): RequestBodyExample =>
|
||||
getPrimitiveTypePlaceholder(schemaObject.type as PrimitiveSchemaType)
|
||||
|
||||
// Use carefully, the schema type should necessarily be object
|
||||
const generateObjectRequestBodyExample = (
|
||||
schemaObject: OpenAPIV31.NonArraySchemaObject
|
||||
): RequestBodyExample =>
|
||||
pipe(
|
||||
schemaObject.properties,
|
||||
O.fromNullable,
|
||||
O.map(
|
||||
(properties) =>
|
||||
Object.entries(properties) as [string, OpenAPIV31.SchemaObject][]
|
||||
),
|
||||
O.getOrElseW(() => [] as [string, OpenAPIV31.SchemaObject][]),
|
||||
A.reduce(
|
||||
{} as { [name: string]: RequestBodyExample },
|
||||
(aggregatedExample, property) => {
|
||||
aggregatedExample[property[0]] =
|
||||
generateRequestBodyExampleFromSchemaObject(property[1])
|
||||
return aggregatedExample
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// Use carefully, the schema type should necessarily be mixed array
|
||||
const generateMixedArrayRequestBodyEcample = (
|
||||
schemaObject: OpenAPIV31.SchemaObject
|
||||
): RequestBodyExample =>
|
||||
pipe(
|
||||
schemaObject,
|
||||
(schemaObject) => schemaObject.type as MixedArraySchemaType,
|
||||
A.reduce([] as Array<RequestBodyExample>, (aggregatedExample, itemType) => {
|
||||
// TODO: Figure out how to include non-primitive types as well
|
||||
if (isSchemaTypePrimitive(itemType)) {
|
||||
aggregatedExample.push(getPrimitiveTypePlaceholder(itemType))
|
||||
}
|
||||
return aggregatedExample
|
||||
})
|
||||
)
|
||||
|
||||
const generateArrayRequestBodyExample = (
|
||||
schemaObject: OpenAPIV31.ArraySchemaObject
|
||||
): RequestBodyExample => [
|
||||
generateRequestBodyExampleFromSchemaObject(
|
||||
schemaObject.items as OpenAPIV31.SchemaObject
|
||||
),
|
||||
]
|
||||
|
||||
const generateRequestBodyExampleFromSchemaObject = (
|
||||
schemaObject: OpenAPIV31.SchemaObject
|
||||
): RequestBodyExample => {
|
||||
// TODO: Handle schema objects with oneof or anyof
|
||||
if (schemaObject.example) return schemaObject.example as RequestBodyExample
|
||||
if (schemaObject.examples)
|
||||
return schemaObject.examples[0] as RequestBodyExample
|
||||
if (!schemaObject.type) return ""
|
||||
if (isSchemaTypePrimitive(schemaObject.type))
|
||||
return generatePrimitiveRequestBodyExample(
|
||||
schemaObject as OpenAPIV31.NonArraySchemaObject
|
||||
)
|
||||
if (schemaObject.type === "object")
|
||||
return generateObjectRequestBodyExample(schemaObject)
|
||||
if (schemaObject.type === "array")
|
||||
return generateArrayRequestBodyExample(schemaObject)
|
||||
return generateMixedArrayRequestBodyEcample(schemaObject)
|
||||
}
|
||||
|
||||
export const generateRequestBodyExampleFromMediaObject = (
|
||||
mediaObject: OpenAPIV31.MediaTypeObject
|
||||
): RequestBodyExample => {
|
||||
if (mediaObject.example) return mediaObject.example as RequestBodyExample
|
||||
if (mediaObject.examples) return mediaObject.examples[0] as RequestBodyExample
|
||||
return mediaObject.schema
|
||||
? generateRequestBodyExampleFromSchemaObject(mediaObject.schema)
|
||||
: ""
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Ref } from "@nuxtjs/composition-api"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { Ref, ref } from "@nuxtjs/composition-api"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useI18n, useToast } from "~/helpers/utils/composables"
|
||||
|
||||
@@ -9,13 +8,13 @@ export default function useCopyResponse(responseBodyText: Ref<any>): {
|
||||
} {
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
|
||||
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
|
||||
const copyIcon = ref("copy")
|
||||
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseBodyText.value)
|
||||
copyIcon.value = "check"
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as S from "fp-ts/string"
|
||||
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { Ref } from "@nuxtjs/composition-api"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { Ref, ref } from "@nuxtjs/composition-api"
|
||||
import { useI18n, useToast } from "~/helpers/utils/composables"
|
||||
|
||||
export type downloadResponseReturnType = (() => void) | Ref<any>
|
||||
@@ -14,8 +13,7 @@ export default function useDownloadResponse(
|
||||
downloadIcon: Ref<string>
|
||||
downloadResponse: () => void
|
||||
} {
|
||||
const downloadIcon = refAutoReset<"download" | "check">("download", 1000)
|
||||
|
||||
const downloadIcon = ref("download")
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
|
||||
@@ -44,6 +42,7 @@ export default function useDownloadResponse(
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -14,37 +14,6 @@ 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": "不保存",
|
||||
"dont_save": "Don't 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": "复制认证 Token",
|
||||
"developer_option": "开发者选项",
|
||||
"developer_option_description": "开发者工具,有助于开发和维护 Hoppscotch。",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||
"discord": "Discord",
|
||||
"documentation": "帮助文档",
|
||||
"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": "检查网站状态",
|
||||
"status_description": "Check the status of the website",
|
||||
"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": "使用 Microsoft 登录",
|
||||
"continue_with_microsoft": "Continue with Microsoft",
|
||||
"email": "电子邮箱地址",
|
||||
"logged_out": "登出",
|
||||
"login": "登录",
|
||||
@@ -106,32 +106,32 @@
|
||||
"username": "用户名"
|
||||
},
|
||||
"collection": {
|
||||
"created": "集合已创建",
|
||||
"edit": "编辑集合",
|
||||
"invalid_name": "请提供有效的集合名称",
|
||||
"my_collections": "我的集合",
|
||||
"name": "我的新集合",
|
||||
"name_length_insufficient": "集合名字至少需要 3 个字符",
|
||||
"new": "新建集合",
|
||||
"renamed": "集合已更名",
|
||||
"request_in_use": "请求正在使用中",
|
||||
"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",
|
||||
"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": "你确定你要放弃当前的请求,未保存的修改将被丢失。",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
"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": "添加到全局环境",
|
||||
"added": "环境已添加",
|
||||
"create_new": "创建新环境",
|
||||
"created": "环境已创建",
|
||||
"deleted": "环境已删除",
|
||||
"add_to_global": "Add to Global",
|
||||
"added": "Environment addition",
|
||||
"create_new": "已创建新环境",
|
||||
"created": "Environment created",
|
||||
"deleted": "Environment deletion",
|
||||
"edit": "编辑环境",
|
||||
"invalid_name": "请提供有效的环境名称",
|
||||
"nested_overflow": "环境嵌套深度超过限制(10层)",
|
||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||
"new": "新建环境",
|
||||
"no_environment": "无环境",
|
||||
"no_environment_description": "没有选择环境。选择如何处理以下变量。",
|
||||
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
||||
"select": "选择环境",
|
||||
"title": "环境",
|
||||
"updated": "环境已更新",
|
||||
"updated": "Environment updation",
|
||||
"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": "配置文件中的 URL 无效",
|
||||
"incomplete_config_urls": "Incomplete configuration URLs",
|
||||
"incorrect_email": "电子邮箱错误",
|
||||
"invalid_link": "无效链接",
|
||||
"invalid_link_description": "你点击的链接无效或已过期。",
|
||||
@@ -202,7 +202,7 @@
|
||||
"no_duration": "无持续时间",
|
||||
"script_fail": "无法执行预请求脚本",
|
||||
"something_went_wrong": "发生了一些错误",
|
||||
"test_script_fail": "无法执行请求脚本"
|
||||
"test_script_fail": "Could not execute post-request script"
|
||||
},
|
||||
"export": {
|
||||
"as_json": "导出为 JSON",
|
||||
@@ -215,7 +215,7 @@
|
||||
"created": "已创建文件夹",
|
||||
"edit": "编辑文件夹",
|
||||
"invalid_name": "请提供文件夹的名称",
|
||||
"name_length_insufficient": "文件夹名称应至少为 3 个字符",
|
||||
"name_length_insufficient": "Folder name should be at least 3 characters long",
|
||||
"new": "新文件夹",
|
||||
"renamed": "文件夹已更名"
|
||||
},
|
||||
@@ -238,46 +238,46 @@
|
||||
"post_request_tests": "测试脚本使用 JavaScript 编写,并在收到响应后执行。",
|
||||
"pre_request_script": "预请求脚本使用 JavaScript 编写,并在请求发送前执行。",
|
||||
"script_fail": "预请求脚本中似乎存在故障。 检查下面的错误并相应地修复脚本。",
|
||||
"test_script_fail": "测试脚本似乎有一个错误。请修复错误并再次运行测试",
|
||||
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
|
||||
"tests": "编写测试脚本以自动调试。"
|
||||
},
|
||||
"hide": {
|
||||
"collection": "隐藏集合",
|
||||
"collection": "Collapse Collection Panel",
|
||||
"more": "隐藏更多",
|
||||
"preview": "隐藏预览",
|
||||
"sidebar": "隐藏侧边栏"
|
||||
},
|
||||
"import": {
|
||||
"collections": "导入集合",
|
||||
"collections": "导入组合",
|
||||
"curl": "导入 cURL",
|
||||
"failed": "导入失败",
|
||||
"from_gist": "从 Gist 导入",
|
||||
"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 导入",
|
||||
"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",
|
||||
"gist_url": "输入 Gist URL",
|
||||
"json_description": "从 Hoppscotch 的集合文件导入(JSON)",
|
||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||
"title": "导入"
|
||||
},
|
||||
"layout": {
|
||||
"collapse_collection": "折叠/展开集合",
|
||||
"collapse_sidebar": "折叠/展开边栏",
|
||||
"collapse_collection": "Collapse or Expand Collections",
|
||||
"collapse_sidebar": "Collapse or Expand the sidebar",
|
||||
"column": "垂直布局",
|
||||
"name": "布局",
|
||||
"name": "Layout",
|
||||
"row": "水平布局",
|
||||
"zen_mode": "ZEN 模式"
|
||||
"zen_mode": "禅意模式"
|
||||
},
|
||||
"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": "新请求",
|
||||
"override": "覆盖",
|
||||
"override_help": "设置 <xmp>Content-Type</xmp> 头",
|
||||
"overriden": "覆盖",
|
||||
"new": "New Request",
|
||||
"override": "Override",
|
||||
"override_help": "Set <xmp>Content-Type</xmp> in Headers",
|
||||
"overriden": "Overridden",
|
||||
"parameter_list": "查询参数",
|
||||
"parameters": "参数",
|
||||
"path": "路径",
|
||||
@@ -356,7 +356,7 @@
|
||||
"save_as": "另存为",
|
||||
"saved": "请求已保存",
|
||||
"share": "分享",
|
||||
"share_description": "分享 Hoppscotch 给你的朋友",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"title": "请求",
|
||||
"type": "请求类型",
|
||||
"url": "URL",
|
||||
@@ -396,7 +396,7 @@
|
||||
"extension_version": "扩展版本",
|
||||
"extensions": "扩展",
|
||||
"extensions_use_toggle": "使用浏览器扩展发送请求(如果存在)",
|
||||
"follow": "关注我们",
|
||||
"follow": "Follow Us",
|
||||
"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": "切换为黑色主题",
|
||||
"dark": "切换为深色主题",
|
||||
"light": "切换为浅色主题",
|
||||
"system": "切换为系统主题",
|
||||
"title": "主题"
|
||||
"black": "Switch theme to black mode",
|
||||
"dark": "Switch theme to dark mode",
|
||||
"light": "Switch theme to light mode",
|
||||
"system": "Switch theme to system mode",
|
||||
"title": "Theme"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"code": "显示代码",
|
||||
"collection": "展开集合",
|
||||
"collection": "Expand Collection Panel",
|
||||
"more": "显示更多",
|
||||
"sidebar": "显示侧边栏"
|
||||
},
|
||||
@@ -525,7 +525,7 @@
|
||||
"community": "提问与互助",
|
||||
"documentation": "阅读更多有关 Hoppscotch 的内容",
|
||||
"forum": "答疑解惑",
|
||||
"github": "在 Github 关注我们",
|
||||
"github": "Follow us on 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": "没有找到团队,请联系您的团队所有者。",
|
||||
"not_valid_viewer": "你不是有效的查看者。请联系你的团队者。",
|
||||
"no_access": "你没有编辑组合的权限",
|
||||
"no_invite_found": "未找到邀请。请联系你的团队所有人。",
|
||||
"not_found": "Team not found. Contact your team owner.",
|
||||
"not_valid_viewer": "你不是有效的阅览者。请联系你的团队所有人。",
|
||||
"pending_invites": "待办邀请",
|
||||
"permissions": "权限",
|
||||
"saved": "团队已保存",
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"download_file": "Download file",
|
||||
"duplicate": "Duplicate",
|
||||
"edit": "Edit",
|
||||
"filter_response": "Filter response",
|
||||
"go_back": "Go back",
|
||||
"label": "Label",
|
||||
"learn_more": "Learn more",
|
||||
@@ -203,11 +202,9 @@
|
||||
"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"
|
||||
@@ -343,11 +340,6 @@
|
||||
"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",
|
||||
@@ -382,7 +374,6 @@
|
||||
},
|
||||
"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": "Limpar",
|
||||
"clear": "Claro",
|
||||
"clear_all": "Limpar tudo",
|
||||
"connect": "Conectar",
|
||||
"copy": "Copiar",
|
||||
"delete": "Excluir",
|
||||
"disconnect": "Desconectar",
|
||||
"disconnect": "desconectar",
|
||||
"dismiss": "Dispensar",
|
||||
"dont_save": "Não Salvar",
|
||||
"dont_save": "Don't save",
|
||||
"download_file": "⇬ Fazer download do arquivo",
|
||||
"duplicate": "Duplicar",
|
||||
"duplicate": "Duplicate",
|
||||
"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": "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.",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||
"discord": "Discord",
|
||||
"documentation": "Documentaçã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": "Opções",
|
||||
"options": "Options",
|
||||
"proxy_privacy_policy": "Política de privacidade do proxy",
|
||||
"reload": "Recarregar",
|
||||
"reload": "recarregar",
|
||||
"search": "Procurar",
|
||||
"share": "Compartilhado",
|
||||
"shortcuts": "Atalhos",
|
||||
"spotlight": "Holofote",
|
||||
"status": "Estado",
|
||||
"status_description": "Cheque o estado do website.",
|
||||
"status": "Status",
|
||||
"status_description": "Check the status of the 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": "Requisição em uso",
|
||||
"request_in_use": "Request in use",
|
||||
"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": "Tem certeza que deseja descartar a requisição atual? Alterações não salvas serão perdidas.",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
"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": "Se conecte à um endpoint GraphQL para ver a documentação",
|
||||
"endpoint": "O endpoint não pode ser vazio",
|
||||
"documentation": "Connect to a GraphQL endpoint to view documentation",
|
||||
"endpoint": "Endpoint cannot be empty",
|
||||
"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": "Ambiente criado",
|
||||
"created": "Environment created",
|
||||
"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": "O link que você clicou é inválido ou já expirou.",
|
||||
"invalid_link_description": "The link you clicked is invalid or expired.",
|
||||
"json_prettify_invalid_body": "Não foi possível embelezar um corpo inválido, resolver erros de sintaxe json e tentar novamente",
|
||||
"network_error": "Parece que houve um problema de rede. Por favor, tente novamente.",
|
||||
"network_error": "There seems to be a network error. Please try again.",
|
||||
"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": "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_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": "Importar de minhas coleções",
|
||||
"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",
|
||||
"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",
|
||||
"title": "Importar"
|
||||
},
|
||||
"layout": {
|
||||
"collapse_collection": "Encolher ou expandir coleções",
|
||||
"collapse_sidebar": "Encolher ou Expandir a barra lateral",
|
||||
"collapse_collection": "Collapse or Expand Collections",
|
||||
"collapse_sidebar": "Collapse or Expand the sidebar",
|
||||
"column": "Layout vertical",
|
||||
"name": "Layout",
|
||||
"row": "Layout horizontal",
|
||||
@@ -311,16 +311,16 @@
|
||||
"profile": {
|
||||
"app_settings": "App Settings",
|
||||
"editor": "Editor",
|
||||
"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."
|
||||
"editor_description": "Editors can add, edit, and delete requests.",
|
||||
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
|
||||
"no_permission": "You do not have permission to perform this action.",
|
||||
"owner": "Owner",
|
||||
"owner_description": "Owners can add, edit, and delete requests, collections and team members.",
|
||||
"roles": "Roles",
|
||||
"roles_description": "Roles are used to control access to the shared collections.",
|
||||
"updated": "Profile updated",
|
||||
"viewer": "Viewer",
|
||||
"viewer_description": "Viewers can only view and use requests."
|
||||
},
|
||||
"remove": {
|
||||
"star": "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": "Nova requisição",
|
||||
"override": "Substituir",
|
||||
"override_help": "Substituir <xmp>Content-Type</xmp> em Headers",
|
||||
"overriden": "Substituído",
|
||||
"new": "New Request",
|
||||
"override": "Override",
|
||||
"override_help": "Set <xmp>Content-Type</xmp> in Headers",
|
||||
"overriden": "Overridden",
|
||||
"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": "Compartilhe o Hoppscotch com seus amigos",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"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": "Nos siga",
|
||||
"follow": "Follow Us",
|
||||
"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": "Atualize os detalhes de seu perfil",
|
||||
"profile_description": "Update your profile details",
|
||||
"profile_email": "Endereço de email",
|
||||
"profile_name": "Nome do perfil",
|
||||
"proxy": "Proxy",
|
||||
|
||||
@@ -540,6 +540,6 @@ export function updateEnvironmentVariable(
|
||||
})
|
||||
}
|
||||
|
||||
export function getEnvironment(index: number) {
|
||||
export function getEnviroment(index: number) {
|
||||
return environmentsStore.value.environments[index]
|
||||
}
|
||||
|
||||
@@ -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", { systemvars: true }],
|
||||
"@nuxtjs/dotenv",
|
||||
// https://github.com/nuxt-community/composition-api
|
||||
"@nuxtjs/composition-api/module",
|
||||
"~/modules/emit-volar-types.ts",
|
||||
@@ -339,8 +339,6 @@ 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,7 +57,7 @@
|
||||
"@codemirror/text": "^0.19.6",
|
||||
"@codemirror/tooltip": "^0.19.16",
|
||||
"@codemirror/view": "^0.19.48",
|
||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.2.0",
|
||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.1.0",
|
||||
"@hoppscotch/data": "workspace:^0.4.2",
|
||||
"@hoppscotch/js-sandbox": "workspace:^2.0.0",
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
@@ -88,7 +88,6 @@
|
||||
"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",
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<FirebaseLogout outline />
|
||||
</div>
|
||||
</div>
|
||||
<SmartTabs v-model="selectedProfileTab" render-inactive-tabs>
|
||||
<SmartTabs v-model="selectedProfileTab">
|
||||
<SmartTab :id="'sync'" :label="t('settings.account')">
|
||||
<div class="grid grid-cols-1">
|
||||
<section class="p-4">
|
||||
|
||||
@@ -1,67 +1,53 @@
|
||||
<template>
|
||||
<SmartTabs v-model="currentTab">
|
||||
<SmartTabs
|
||||
v-model="selectedNavigationTab"
|
||||
class="h-full !overflow-hidden"
|
||||
styles="sticky bg-primary top-0 z-10 border-b border-dividerLight !overflow-visible"
|
||||
>
|
||||
<SmartTab
|
||||
v-for="{ target, title } in REALTIME_NAVIGATION"
|
||||
:id="target"
|
||||
:key="target"
|
||||
:label="title"
|
||||
id="websocket"
|
||||
:label="$t('tab.websocket')"
|
||||
style="height: calc(100% - var(--sidebar-primary-sticky-fold))"
|
||||
>
|
||||
<NuxtChild />
|
||||
<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 />
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, ref, useRouter, useRoute } from "@nuxtjs/composition-api"
|
||||
import { useI18n } from "~/helpers/utils/composables"
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
|
||||
const t = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const REALTIME_NAVIGATION = [
|
||||
{
|
||||
target: "websocket",
|
||||
title: t("tab.websocket"),
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
selectedNavigationTab: "websocket",
|
||||
}
|
||||
},
|
||||
{
|
||||
target: "sse",
|
||||
title: t("tab.sse"),
|
||||
head() {
|
||||
return {
|
||||
title: `${this.$t("navigation.realtime")} • Hoppscotch`,
|
||||
}
|
||||
},
|
||||
{
|
||||
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(`/realtime/${newTab}`)
|
||||
})
|
||||
|
||||
// Update the tab when router is upgrad
|
||||
watch(
|
||||
route,
|
||||
(updateRoute) => {
|
||||
if (updateRoute.path === "/realtime") router.replace("/realtime/websocket")
|
||||
|
||||
const destination: string | undefined =
|
||||
updateRoute.path.split("/realtime/")[1]
|
||||
|
||||
const target = REALTIME_NAVIGATION.find(
|
||||
({ target }) => target === destination
|
||||
)?.target
|
||||
|
||||
if (target) currentTab.value = target
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -236,7 +236,6 @@
|
||||
|
||||
<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,
|
||||
@@ -277,7 +276,7 @@ const hasFirefoxExtInstalled = computed(
|
||||
() => browserIsFirefox() && currentExtensionStatus.value === "available"
|
||||
)
|
||||
|
||||
const clearIcon = refAutoReset<"rotate-ccw" | "check">("rotate-ccw", 1000)
|
||||
const clearIcon = ref("rotate-ccw")
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
@@ -323,6 +322,7 @@ 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,7 +10,6 @@
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
@@ -30,7 +29,6 @@
|
||||
},
|
||||
"exclude": ["node_modules", ".nuxt", "dist"],
|
||||
"vueCompilerOptions": {
|
||||
"target": 2,
|
||||
"experimentalCompatMode": 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { JSONPathOptions } from "jsonpath-plus"
|
||||
|
||||
declare module "jsonpath-plus" {
|
||||
export type JSONPathType = (options: JSONPathOptions) => unknown
|
||||
export const JSONPath: JSONPathType
|
||||
}
|
||||
@@ -18,7 +18,6 @@ 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,26 +24,13 @@ hopp [options or commands] arguments
|
||||
|
||||
- Displays the help text
|
||||
|
||||
3. #### **`hopp test [options] <file_path>`**
|
||||
3. #### **`hopp test <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.2.1",
|
||||
"version": "0.1.14",
|
||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||
"homepage": "https://hoppscotch.io",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("Test 'hopp test <file>' command:", () => {
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const out = getErrorCode(stdout);
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
expect(out).toBe<HoppErrorCode>("NO_FILE_PATH");
|
||||
});
|
||||
|
||||
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>("INVALID_FILE_TYPE");
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_JSON");
|
||||
});
|
||||
|
||||
test("Some errors occured (exit code 1).", async () => {
|
||||
@@ -62,42 +62,3 @@ 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();
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { HoppCLIError } from "../../../types/errors";
|
||||
import { checkFile } from "../../../utils/checks";
|
||||
import { checkFilePath } from "../../../utils/checks";
|
||||
|
||||
import "@relmify/jest-fp-ts";
|
||||
|
||||
describe("checkFile", () => {
|
||||
describe("checkFilePath", () => {
|
||||
test("File doesn't exists.", () => {
|
||||
return expect(
|
||||
checkFile("./src/samples/this-file-not-exists.json")()
|
||||
checkFilePath("./src/samples/this-file-not-exists.json")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
code: "FILE_NOT_FOUND",
|
||||
});
|
||||
@@ -14,15 +12,15 @@ describe("checkFile", () => {
|
||||
|
||||
test("File not of JSON type.", () => {
|
||||
return expect(
|
||||
checkFile("./src/__tests__/samples/notjson.txt")()
|
||||
checkFilePath("./src/__tests__/samples/notjson.txt")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
code: "INVALID_FILE_TYPE",
|
||||
code: "FILE_NOT_JSON",
|
||||
});
|
||||
});
|
||||
|
||||
test("Existing JSON file.", () => {
|
||||
return expect(
|
||||
checkFile("./src/__tests__/samples/passes.json")()
|
||||
checkFilePath("./src/__tests__/samples/passes.json")()
|
||||
).resolves.toBeRight();
|
||||
});
|
||||
});
|
||||
@@ -37,8 +37,6 @@ const SAMPLE_RESOLVED_RESPONSE = <AxiosResponse>{
|
||||
headers: [],
|
||||
};
|
||||
|
||||
const SAMPLE_ENVS = { global: [], selected: [] };
|
||||
|
||||
describe("collectionsRunner", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -49,24 +47,19 @@ describe("collectionsRunner", () => {
|
||||
});
|
||||
|
||||
test("Empty HoppCollection.", () => {
|
||||
return expect(
|
||||
collectionsRunner({ collections: [], envs: SAMPLE_ENVS })()
|
||||
).resolves.toStrictEqual([]);
|
||||
return expect(collectionsRunner([])()).resolves.toStrictEqual([]);
|
||||
});
|
||||
|
||||
test("Empty requests and folders in collection.", () => {
|
||||
return expect(
|
||||
collectionsRunner({
|
||||
collections: [
|
||||
{
|
||||
v: 1,
|
||||
name: "name",
|
||||
folders: [],
|
||||
requests: [],
|
||||
},
|
||||
],
|
||||
envs: SAMPLE_ENVS,
|
||||
})()
|
||||
collectionsRunner([
|
||||
{
|
||||
v: 1,
|
||||
name: "name",
|
||||
folders: [],
|
||||
requests: [],
|
||||
},
|
||||
])()
|
||||
).resolves.toMatchObject([]);
|
||||
});
|
||||
|
||||
@@ -74,17 +67,14 @@ describe("collectionsRunner", () => {
|
||||
(axios as unknown as jest.Mock).mockResolvedValue(SAMPLE_RESOLVED_RESPONSE);
|
||||
|
||||
return expect(
|
||||
collectionsRunner({
|
||||
collections: [
|
||||
{
|
||||
v: 1,
|
||||
name: "collection",
|
||||
folders: [],
|
||||
requests: [SAMPLE_HOPP_REQUEST],
|
||||
},
|
||||
],
|
||||
envs: SAMPLE_ENVS,
|
||||
})()
|
||||
collectionsRunner([
|
||||
{
|
||||
v: 1,
|
||||
name: "collection",
|
||||
folders: [],
|
||||
requests: [SAMPLE_HOPP_REQUEST],
|
||||
},
|
||||
])()
|
||||
).resolves.toMatchObject([
|
||||
{
|
||||
path: "collection/request",
|
||||
@@ -99,24 +89,21 @@ describe("collectionsRunner", () => {
|
||||
(axios as unknown as jest.Mock).mockResolvedValue(SAMPLE_RESOLVED_RESPONSE);
|
||||
|
||||
return expect(
|
||||
collectionsRunner({
|
||||
collections: [
|
||||
{
|
||||
v: 1,
|
||||
name: "collection",
|
||||
folders: [
|
||||
{
|
||||
v: 1,
|
||||
name: "folder",
|
||||
folders: [],
|
||||
requests: [SAMPLE_HOPP_REQUEST],
|
||||
},
|
||||
],
|
||||
requests: [],
|
||||
},
|
||||
],
|
||||
envs: SAMPLE_ENVS,
|
||||
})()
|
||||
collectionsRunner([
|
||||
{
|
||||
v: 1,
|
||||
name: "collection",
|
||||
folders: [
|
||||
{
|
||||
v: 1,
|
||||
name: "folder",
|
||||
folders: [],
|
||||
requests: [SAMPLE_HOPP_REQUEST],
|
||||
},
|
||||
],
|
||||
requests: [],
|
||||
},
|
||||
])()
|
||||
).resolves.toMatchObject([
|
||||
{
|
||||
path: "collection/folder/request",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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,14 +1,12 @@
|
||||
import { HoppCLIError } from "../../../types/errors";
|
||||
import { parseCollectionData } from "../../../utils/mutators";
|
||||
|
||||
import "@relmify/jest-fp-ts";
|
||||
|
||||
describe("parseCollectionData", () => {
|
||||
test("Reading non-existing file.", () => {
|
||||
return expect(
|
||||
parseCollectionData("./src/__tests__/samples/notexist.json")()
|
||||
parseCollectionData("./src/__tests__/samples/notexist.txt")()
|
||||
).resolves.toSubsetEqualLeft(<HoppCLIError>{
|
||||
code: "FILE_NOT_FOUND",
|
||||
code: "UNKNOWN_ERROR",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ 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: [
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"URL": "https://echo.hoppscotch.io",
|
||||
"HOST": "echo.hoppscotch.io",
|
||||
"X-COUNTRY": "IN",
|
||||
"BODY_VALUE": "body_value",
|
||||
"BODY_KEY": "body_key"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"v": 1,
|
||||
"name": "env-flag-tests",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "1",
|
||||
"endpoint": "<<URL>>",
|
||||
"name": "test1",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"method": "POST",
|
||||
"auth": { "authType": "none", "authActive": true },
|
||||
"preRequestScript": "",
|
||||
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,15 +6,14 @@ import {
|
||||
collectionsRunnerResult,
|
||||
} from "../utils/collections";
|
||||
import { handleError } from "../handlers/error";
|
||||
import { checkFilePath } from "../utils/checks";
|
||||
import { parseCollectionData } from "../utils/mutators";
|
||||
import { parseEnvsData } from "../options/test/env";
|
||||
import { TestCmdOptions } from "../types/commands";
|
||||
|
||||
export const test = (path: string, options: TestCmdOptions) => async () => {
|
||||
export const test = (path: string) => async () => {
|
||||
await pipe(
|
||||
TE.Do,
|
||||
TE.bind("envs", () => parseEnvsData(options.env)),
|
||||
TE.bind("collections", () => parseCollectionData(path)),
|
||||
path,
|
||||
checkFilePath,
|
||||
TE.chain(parseCollectionData),
|
||||
TE.chainTaskK(collectionsRunner),
|
||||
TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)),
|
||||
TE.mapLeft((e) => {
|
||||
|
||||
@@ -48,7 +48,9 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
case "UNKNOWN_COMMAND":
|
||||
ERROR_MSG = `Unavailable command: ${error.command}`;
|
||||
break;
|
||||
case "MALFORMED_ENV_FILE":
|
||||
case "FILE_NOT_JSON":
|
||||
ERROR_MSG = `Please check file type: ${error.path}`;
|
||||
break;
|
||||
case "MALFORMED_COLLECTION":
|
||||
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
|
||||
break;
|
||||
@@ -58,9 +60,6 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
case "PARSING_ERROR":
|
||||
ERROR_MSG = `Unable to parse -\n${error.data}`;
|
||||
break;
|
||||
case "INVALID_FILE_TYPE":
|
||||
ERROR_MSG = `Please provide file of extension type: ${error.data}`;
|
||||
break;
|
||||
case "REQUEST_ERROR":
|
||||
case "TEST_SCRIPT_ERROR":
|
||||
case "PRE_REQUEST_SCRIPT_ERROR":
|
||||
|
||||
@@ -5,16 +5,14 @@ import { version } from "../package.json";
|
||||
import { test } from "./commands/test";
|
||||
import { handleError } from "./handlers/error";
|
||||
|
||||
const accent = chalk.greenBright;
|
||||
const accent = chalk.greenBright
|
||||
|
||||
/**
|
||||
* * Program Default Configuration
|
||||
*/
|
||||
const CLI_BEFORE_ALL_TXT = `hopp: The ${accent(
|
||||
"Hoppscotch"
|
||||
)} CLI - Version ${version} (${accent(
|
||||
"https://hoppscotch.io"
|
||||
)}) ${chalk.black.bold.bgYellowBright(" ALPHA ")} \n`;
|
||||
)} CLI - Version ${version} (${accent("https://hoppscotch.io")}) ${chalk.black.bold.bgYellowBright(" ALPHA ")} \n`;
|
||||
|
||||
const CLI_AFTER_ALL_TXT = `\nFor more help, head on to ${accent(
|
||||
"https://docs.hoppscotch.io/cli"
|
||||
@@ -46,18 +44,14 @@ program.exitOverride().configureOutput({
|
||||
program
|
||||
.command("test")
|
||||
.argument(
|
||||
"<file_path>",
|
||||
"[file]",
|
||||
"path to a hoppscotch collection.json file for CI testing"
|
||||
)
|
||||
.option("-e, --env <file_path>", "path to an environment variables json file")
|
||||
.allowExcessArguments(false)
|
||||
.allowUnknownOption(false)
|
||||
.description("running hoppscotch collection.json file")
|
||||
.addHelpText(
|
||||
"after",
|
||||
`\nFor help, head on to ${accent("https://docs.hoppscotch.io/cli#test")}`
|
||||
)
|
||||
.action(async (path, options) => await test(path, options)());
|
||||
.addHelpText("after", `\nFor help, head on to ${accent("https://docs.hoppscotch.io/cli#test")}`)
|
||||
.action(async (path) => await test(path)());
|
||||
|
||||
export const cli = async (args: string[]) => {
|
||||
try {
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import fs from "fs/promises";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as J from "fp-ts/Json";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as S from "fp-ts/string";
|
||||
import isArray from "lodash/isArray";
|
||||
import { HoppCLIError, error } from "../../types/errors";
|
||||
import { HoppEnvs, HoppEnvPair } from "../../types/request";
|
||||
import { checkFile } from "../../utils/checks";
|
||||
|
||||
/**
|
||||
* Parses env json file for given path and validates the parsed env json object.
|
||||
* @param path Path of env.json file to be parsed.
|
||||
* @returns For successful parsing we get HoppEnvs object.
|
||||
*/
|
||||
export const parseEnvsData = (
|
||||
path: unknown
|
||||
): TE.TaskEither<HoppCLIError, HoppEnvs> =>
|
||||
!S.isString(path)
|
||||
? TE.right({ global: [], selected: [] })
|
||||
: pipe(
|
||||
// Checking if the env.json file exists or not.
|
||||
checkFile(path),
|
||||
|
||||
// Trying to read given env json file path.
|
||||
TE.chainW((checkedPath) =>
|
||||
TE.tryCatch(
|
||||
() => fs.readFile(checkedPath),
|
||||
(reason) =>
|
||||
error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
||||
)
|
||||
),
|
||||
|
||||
// Trying to JSON parse the read file data and mapping the entries to HoppEnvPairs.
|
||||
TE.chainEitherKW((data) =>
|
||||
pipe(
|
||||
data.toString(),
|
||||
J.parse,
|
||||
E.map((jsonData) =>
|
||||
jsonData && typeof jsonData === "object" && !isArray(jsonData)
|
||||
? pipe(
|
||||
jsonData,
|
||||
Object.entries,
|
||||
A.map(
|
||||
([key, value]) =>
|
||||
<HoppEnvPair>{
|
||||
key,
|
||||
value: S.isString(value)
|
||||
? value
|
||||
: JSON.stringify(value),
|
||||
}
|
||||
)
|
||||
)
|
||||
: []
|
||||
),
|
||||
E.map((envPairs) => <HoppEnvs>{ global: [], selected: envPairs }),
|
||||
E.mapLeft((e) =>
|
||||
error({ code: "MALFORMED_ENV_FILE", path, data: E.toError(e) })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1,9 +0,0 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { HoppEnvs } from "./request";
|
||||
|
||||
export type CollectionRunnerParam = {
|
||||
collections: HoppCollection<HoppRESTRequest>[];
|
||||
envs: HoppEnvs;
|
||||
};
|
||||
|
||||
export type HoppCollectionFileExt = "json";
|
||||
@@ -1,5 +0,0 @@
|
||||
export type TestCmdOptions = {
|
||||
env: string;
|
||||
};
|
||||
|
||||
export type HoppEnvFileExt = "json";
|
||||
@@ -15,6 +15,7 @@ type HoppErrors = {
|
||||
FILE_NOT_FOUND: HoppErrorPath;
|
||||
UNKNOWN_COMMAND: HoppErrorCmd;
|
||||
MALFORMED_COLLECTION: HoppErrorPath & HoppErrorData;
|
||||
FILE_NOT_JSON: HoppErrorPath;
|
||||
NO_FILE_PATH: {};
|
||||
PRE_REQUEST_SCRIPT_ERROR: HoppErrorData;
|
||||
PARSING_ERROR: HoppErrorData;
|
||||
@@ -23,8 +24,6 @@ type HoppErrors = {
|
||||
SYNTAX_ERROR: HoppErrorData;
|
||||
REQUEST_ERROR: HoppErrorData;
|
||||
INVALID_ARGUMENT: HoppErrorData;
|
||||
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||
INVALID_FILE_TYPE: HoppErrorData;
|
||||
};
|
||||
|
||||
export type HoppErrorCode = keyof HoppErrors;
|
||||
|
||||
@@ -7,11 +7,15 @@ export type FormDataEntry = {
|
||||
value: string | Blob;
|
||||
};
|
||||
|
||||
export type HoppEnvPair = { key: string; value: string };
|
||||
|
||||
export type HoppEnvs = {
|
||||
global: HoppEnvPair[];
|
||||
selected: HoppEnvPair[];
|
||||
global: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
selected: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type CollectionStack = {
|
||||
|
||||
@@ -9,12 +9,8 @@ import {
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as S from "fp-ts/string";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as E from "fp-ts/Either";
|
||||
import curryRight from "lodash/curryRight";
|
||||
import { CommanderError } from "commander";
|
||||
import { error, HoppCLIError, HoppErrnoException } from "../types/errors";
|
||||
import { HoppCollectionFileExt } from "../types/collections";
|
||||
import { HoppEnvFileExt } from "../types/commands";
|
||||
import { CommanderError } from "commander";
|
||||
|
||||
/**
|
||||
* Determines whether an object has a property with given name.
|
||||
@@ -72,56 +68,42 @@ export const isRESTCollection = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the file path matches the requried file type with of required extension.
|
||||
* @param path The input file path to check.
|
||||
* @param extension The required extension for input file path.
|
||||
* @returns Absolute path for valid file extension OR HoppCLIError in case of error.
|
||||
*/
|
||||
export const checkFileExt = curryRight(
|
||||
(
|
||||
path: unknown,
|
||||
extension: HoppCollectionFileExt | HoppEnvFileExt
|
||||
): E.Either<HoppCLIError, string> =>
|
||||
pipe(
|
||||
path,
|
||||
E.fromPredicate(S.isString, (_) => error({ code: "NO_FILE_PATH" })),
|
||||
E.chainW(
|
||||
E.fromPredicate(S.endsWith(`.${extension}`), (_) =>
|
||||
error({ code: "INVALID_FILE_TYPE", data: extension })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks if the given file path exists and is of given type.
|
||||
* Checks if the given file path exists and is of JSON type.
|
||||
* @param path The input file path to check.
|
||||
* @returns Absolute path for valid file path OR HoppCLIError in case of error.
|
||||
*/
|
||||
export const checkFile = (path: unknown): TE.TaskEither<HoppCLIError, string> =>
|
||||
export const checkFilePath = (
|
||||
path: string
|
||||
): TE.TaskEither<HoppCLIError, string> =>
|
||||
pipe(
|
||||
path,
|
||||
|
||||
// Checking if path is string.
|
||||
TE.fromPredicate(S.isString, () => error({ code: "NO_FILE_PATH" })),
|
||||
|
||||
/**
|
||||
* After checking file path, we map file path to absolute path and check
|
||||
* if file is of given extension type.
|
||||
* Check the path type and returns string if passes else HoppCLIError.
|
||||
*/
|
||||
TE.map(join),
|
||||
TE.chainEitherK(checkFileExt("json")),
|
||||
TE.fromPredicate(S.isString, () => error({ code: "NO_FILE_PATH" })),
|
||||
|
||||
/**
|
||||
* Trying to access given file path.
|
||||
* If successfully accessed, we return the path from predicate step.
|
||||
* Else return HoppCLIError with code FILE_NOT_FOUND.
|
||||
*/
|
||||
TE.chainFirstW((checkedPath) =>
|
||||
TE.chainFirstW(
|
||||
TE.tryCatchK(
|
||||
() => fs.access(checkedPath),
|
||||
() => error({ code: "FILE_NOT_FOUND", path: checkedPath })
|
||||
)()
|
||||
() => pipe(path, join, fs.access),
|
||||
() => error({ code: "FILE_NOT_FOUND", path: path })
|
||||
)
|
||||
),
|
||||
|
||||
/**
|
||||
* On successfully accessing given file path, we map file path to
|
||||
* absolute path and return abs file path if file is JSON type.
|
||||
*/
|
||||
TE.map(join),
|
||||
TE.chainW(
|
||||
TE.fromPredicate(S.endsWith(".json"), (absPath) =>
|
||||
error({ code: "FILE_NOT_JSON", path: absPath })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -27,24 +27,21 @@ import {
|
||||
import { getTestMetrics } from "./test";
|
||||
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
||||
import { getPreRequestMetrics } from "./pre-request";
|
||||
import { CollectionRunnerParam } from "../types/collections";
|
||||
|
||||
const { WARN, FAIL } = exceptionColors;
|
||||
|
||||
/**
|
||||
* Processes each requests within collections to prints details of subsequent requests,
|
||||
* tests and to display complete errors-report, failed-tests-report and test-metrics.
|
||||
* @param param Data of hopp-collection with hopp-requests, envs to be processed.
|
||||
* @param collections Array of hopp-collection with hopp-requests to be processed.
|
||||
* @returns List of report for each processed request.
|
||||
*/
|
||||
export const collectionsRunner =
|
||||
(param: CollectionRunnerParam): T.Task<RequestReport[]> =>
|
||||
(collections: HoppCollection<HoppRESTRequest>[]): T.Task<RequestReport[]> =>
|
||||
async () => {
|
||||
const envs: HoppEnvs = param.envs;
|
||||
const envs: HoppEnvs = { global: [], selected: [] };
|
||||
const requestsReport: RequestReport[] = [];
|
||||
const collectionStack: CollectionStack[] = getCollectionStack(
|
||||
param.collections
|
||||
);
|
||||
const collectionStack: CollectionStack[] = getCollectionStack(collections);
|
||||
|
||||
while (collectionStack.length) {
|
||||
// Pop out top-most collection from stack to be processed.
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as J from "fp-ts/Json";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import { FormDataEntry } from "../types/request";
|
||||
import { error, HoppCLIError } from "../types/errors";
|
||||
import { isRESTCollection, isHoppErrnoException, checkFile } from "./checks";
|
||||
import { isRESTCollection, isHoppErrnoException } from "./checks";
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
|
||||
/**
|
||||
@@ -49,17 +49,10 @@ export const parseCollectionData = (
|
||||
path: string
|
||||
): TE.TaskEither<HoppCLIError, HoppCollection<HoppRESTRequest>[]> =>
|
||||
pipe(
|
||||
TE.of(path),
|
||||
|
||||
// Checking if given file path exists or not.
|
||||
TE.chain(checkFile),
|
||||
|
||||
// Trying to read give collection json path.
|
||||
TE.chainW((checkedPath) =>
|
||||
TE.tryCatch(
|
||||
() => fs.readFile(checkedPath),
|
||||
(reason) => error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
||||
)
|
||||
TE.tryCatch(
|
||||
() => pipe(path, fs.readFile),
|
||||
(reason) => error({ code: "UNKNOWN_ERROR", data: E.toError(reason) })
|
||||
),
|
||||
|
||||
// Checking if parsed file data is array.
|
||||
|
||||
811
pnpm-lock.yaml
generated
811
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user