Compare commits

..

2 Commits

Author SHA1 Message Date
Joel Jacob Stephen
d13b381097 feat: added highlighting of search pattern and new search bar design 2022-05-30 22:27:21 +05:30
Joel Jacob Stephen
663da34e08 feat: realtime search through logs 2022-05-30 22:24:41 +05:30
94 changed files with 826 additions and 2434 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
}),
],
}),

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<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

View File

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

View File

@@ -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;
}

View File

@@ -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")}`)
}

View File

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

View File

@@ -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 = () => {

View File

@@ -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;

View File

@@ -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')}`"

View File

@@ -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" })

View File

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

View File

@@ -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
}

View File

@@ -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 = () => {

View File

@@ -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 = () => {

View File

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

View File

@@ -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) => {

View File

@@ -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)

View File

@@ -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,
},
]
}

View File

@@ -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("")

View File

@@ -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)

View File

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

View File

@@ -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)
}
}

View File

@@ -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'"

View File

@@ -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" ||

View File

@@ -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'" />

View File

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

View File

@@ -26,8 +26,8 @@
</template>
<script setup lang="ts">
import { ref } from "@nuxtjs/composition-api"
import { HoppRESTHeader } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n, useToast } from "~/helpers/utils/composables"
@@ -39,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>

View File

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

View File

@@ -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"

View File

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

View File

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

View File

@@ -12,7 +12,7 @@
/>
</div>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-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>

View File

@@ -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"
)

View File

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

View File

@@ -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">

View File

@@ -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") }}

View File

@@ -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)
})

View File

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

View File

@@ -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(

View File

@@ -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()

View File

@@ -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)

View File

@@ -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] : []
),
})
}

View File

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

View File

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

View 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
}

View File

@@ -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 = (

View File

@@ -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(() => "")
)

View File

@@ -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
)
: ""
}

View File

@@ -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)
: ""
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -1,7 +1,7 @@
{
"action": {
"cancel": "取消",
"choose_file": "选择文件",
"choose_file": "选择一个文件",
"clear": "清除",
"clear_all": "全部清除",
"connect": "连接",
@@ -9,18 +9,18 @@
"delete": "删除",
"disconnect": "断开连接",
"dismiss": "忽略",
"dont_save": "不保存",
"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": "团队已保存",

View File

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

View File

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

View File

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

View File

@@ -132,7 +132,7 @@ export default {
// https://github.com/nuxt/typescript
["@nuxt/typescript-build", { typeCheck: false }],
// https://github.com/nuxt-community/dotenv-module
["@nuxtjs/dotenv", { 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: {

View File

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

View File

@@ -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">

View File

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

View File

@@ -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) => {

View File

@@ -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
}
}

View File

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

View File

@@ -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: {

View File

@@ -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:

View File

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

View File

@@ -8,7 +8,7 @@ describe("Test 'hopp test <file>' command:", () => {
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("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();
// });
});

View File

@@ -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();
});
});

View File

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

View File

@@ -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" }],

View File

@@ -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",
});
});

View File

@@ -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: [

View File

@@ -1,7 +0,0 @@
{
"URL": "https://echo.hoppscotch.io",
"HOST": "echo.hoppscotch.io",
"X-COUNTRY": "IN",
"BODY_VALUE": "body_value",
"BODY_KEY": "body_key"
}

View File

@@ -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}"
}
}
]
}

View File

@@ -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) => {

View File

@@ -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":

View File

@@ -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 {

View File

@@ -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) })
)
)
)
);

View File

@@ -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";

View File

@@ -1,5 +0,0 @@
export type TestCmdOptions = {
env: string;
};
export type HoppEnvFileExt = "json";

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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 })
)
)
);

View File

@@ -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.

View File

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

File diff suppressed because it is too large Load Diff