diff --git a/.github/workflows/deploy-netlify.yml b/.github/workflows/deploy-netlify.yml index 2f024b73b..e141db6e0 100644 --- a/.github/workflows/deploy-netlify.yml +++ b/.github/workflows/deploy-netlify.yml @@ -12,11 +12,11 @@ jobs: - name: Checkout Repository uses: actions/checkout@v2 - - name: Install pnpm - run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6 - - - name: Install Dependencies - run: pnpm install + - name: Setup and run pnpm install + uses: pnpm/action-setup@v2.2.2 + with: + version: 7 + run_install: true - name: Setup Environment run: mv packages/hoppscotch-app/.env.example packages/hoppscotch-app/.env @@ -24,11 +24,11 @@ jobs: - name: Build Site run: pnpm run generate - # Deploy the site with netlify-cli - - name: Deploy to Netlify + # Deploy the production site with netlify-cli + - name: Deploy to Netlify (production) uses: netlify/actions/cli@master env: - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_PRODUCTION_SITE_ID }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} with: args: deploy --dir=packages/hoppscotch-app/dist --prod diff --git a/.github/workflows/deploy-staging-netlify.yml b/.github/workflows/deploy-staging-netlify.yml new file mode 100644 index 000000000..d9e311ce2 --- /dev/null +++ b/.github/workflows/deploy-staging-netlify.yml @@ -0,0 +1,45 @@ +name: Deploy to Staging Netlify + +on: + push: + # TODO: Migrate to staging branch only + branches: [main] + +jobs: + build: + name: Push build files to Netlify + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Setup and run pnpm install + uses: pnpm/action-setup@v2.2.2 + with: + version: 7 + run_install: true + + - name: Build Site + env: + GA_ID: ${{ secrets.STAGING_GA_ID }} + GTM_ID: ${{ secrets.STAGING_GTM_ID }} + API_KEY: ${{ secrets.STAGING_FB_API_KEY }} + AUTH_DOMAIN: ${{ secrets.STAGING_FB_AUTH_DOMAIN }} + DATABASE_URL: ${{ secrets.STAGING_FB_DATABASE_URL }} + PROJECT_ID: ${{ secrets.STAGING_FB_PROJECT_ID }} + STORAGE_BUCKET: ${{ secrets.STAGING_FB_STORAGE_BUCKET }} + MESSAGING_SENDER_ID: ${{ secrets.STAGING_FB_MESSAGING_SENDER_ID }} + APP_ID: ${{ secrets.STAGING_FB_APP_ID }} + BASE_URL: ${{ secrets.STAGING_BASE_URL }} + BACKEND_GQL_URL: ${{ secrets.STAGING_BACKEND_GQL_URL }} + BACKEND_WS_URL: ${{ secrets.STAGING_BACKEND_WS_URL }} + run: pnpm run generate + + # Deploy the staging site with netlify-cli + - name: Deploy to Netlify (staging) + uses: netlify/actions/cli@master + env: + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_STAGING_SITE_ID }} + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + with: + args: deploy --dir=packages/hoppscotch-app/dist --prod diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f76db860f..55aa9a834 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,12 +17,15 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 - - name: Install pnpm - run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6 + - name: Setup and run pnpm install + uses: pnpm/action-setup@v2.2.2 + with: + version: 7 + run_install: true - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} cache: pnpm - name: Run tests - run: pnpm i && pnpm -r test + run: pnpm test diff --git a/packages/codemirror-lang-graphql/package.json b/packages/codemirror-lang-graphql/package.json index 8f8280dc3..71f2734b5 100644 --- a/packages/codemirror-lang-graphql/package.json +++ b/packages/codemirror-lang-graphql/package.json @@ -1,6 +1,6 @@ { "name": "@hoppscotch/codemirror-lang-graphql", - "version": "0.1.0", + "version": "0.2.0", "description": "GraphQL language support for CodeMirror", "author": "Hoppscotch (support@hoppscotch.io)", "license": "MIT", diff --git a/packages/codemirror-lang-graphql/src/index.js b/packages/codemirror-lang-graphql/src/index.js index bb35c455c..e1c901837 100644 --- a/packages/codemirror-lang-graphql/src/index.js +++ b/packages/codemirror-lang-graphql/src/index.js @@ -27,16 +27,22 @@ export const GQLLanguage = LRLanguage.define({ }, }), styleTags({ - Name: t.definition(t.variableName), - "OperationDefinition/Name": t.definition(t.function(t.variableName)), - OperationType: t.keyword, - BooleanValue: t.bool, - StringValue: t.string, - IntValue: t.number, - FloatValue: t.number, - NullValue: t.null, - ObjectValue: t.brace, Comment: t.lineComment, + Name: t.propertyName, + StringValue: t.string, + IntValue: t.integer, + FloatValue: t.float, + NullValue: t.null, + BooleanValue: t.bool, + Comma: t.separator, + "OperationDefinition/Name": t.definition(t.function(t.variableName)), + "OperationType TypeKeyword SchemaKeyword FragmentKeyword OnKeyword DirectiveKeyword RepeatableKeyword SchemaKeyword ExtendKeyword ScalarKeyword InterfaceKeyword UnionKeyword EnumKeyword InputKeyword ImplementsKeyword": t.keyword, + "ExecutableDirectiveLocation TypeSystemDirectiveLocation": t.atom, + "DirectiveName!": t.annotation, + "\"{\" \"}\"": t.brace, + "\"(\" \")\"": t.paren, + "\"[\" \"]\"": t.squareBracket, + "Type! NamedType": t.typeName, }), ], }), diff --git a/packages/codemirror-lang-graphql/src/syntax.grammar b/packages/codemirror-lang-graphql/src/syntax.grammar index dd3f27346..1f9ad128b 100644 --- a/packages/codemirror-lang-graphql/src/syntax.grammar +++ b/packages/codemirror-lang-graphql/src/syntax.grammar @@ -33,16 +33,24 @@ TypeSystemExtension { TypeExtension } +SchemaKeyword { + @specialize +} + SchemaDefinition { - Description? @specialize Directives? RootTypeDef + Description? SchemaKeyword Directives? RootTypeDef } RootTypeDef { "{" RootOperationTypeDefinition+ "}" } +ExtendKeyword { + @specialize +} + SchemaExtension { - @specialize @specialize Directives? RootTypeDef + ExtendKeyword SchemaKeyword Directives? RootTypeDef } TypeExtension { @@ -54,33 +62,53 @@ TypeExtension { InputObjectTypeExtension } +ScalarKeyword { + @specialize +} + ScalarTypeExtension { - @specialize @specialize Name Directives + ExtendKeyword ScalarKeyword Name Directives } ObjectTypeExtension /* precedence: right 0 */ { - @specialize @specialize Name ImplementsInterfaces? Directives? !typeDef FieldsDefinition | - @specialize @specialize Name ImplementsInterfaces? Directives? + ExtendKeyword TypeKeyword Name ImplementsInterfaces? Directives? !typeDef FieldsDefinition | + ExtendKeyword TypeKeyword Name ImplementsInterfaces? Directives? +} + +InterfaceKeyword { + @specialize } InterfaceTypeExtension /* precedence: right 0 */ { - @specialize @specialize Name ImplementsInterfaces? Directives? FieldsDefinition | - @specialize @specialize Name ImplementsInterfaces? Directives? + ExtendKeyword InterfaceKeyword Name ImplementsInterfaces? Directives? FieldsDefinition | + ExtendKeyword InterfaceKeyword Name ImplementsInterfaces? Directives? +} + +UnionKeyword { + @specialize } UnionTypeExtension /* precedence: right 0 */ { - @specialize @specialize Name Directives? UnionMemberTypes | - @specialize @specialize Name Directives? + ExtendKeyword UnionKeyword Name Directives? UnionMemberTypes | + ExtendKeyword UnionKeyword Name Directives? +} + +EnumKeyword { + @specialize } EnumTypeExtension /* precedence: right 0 */ { - @specialize @specialize Name Directives? !typeDef EnumValuesDefinition | - @specialize @specialize Name Directives? + ExtendKeyword EnumKeyword Name Directives? !typeDef EnumValuesDefinition | + ExtendKeyword EnumKeyword Name Directives? +} + +InputKeyword { + @specialize } InputObjectTypeExtension /* precedence: right 0 */ { - @specialize @specialize Name Directives? InputFieldsDefinition+ | - @specialize @specialize Name Directives? + ExtendKeyword InputKeyword Name Directives? InputFieldsDefinition+ | + ExtendKeyword InputKeyword Name Directives? } InputFieldsDefinition { @@ -95,9 +123,13 @@ EnumValueDefinition { Description? EnumValue Directives? } +ImplementsKeyword { + @specialize +} + ImplementsInterfaces { ImplementsInterfaces "&" NamedType | - @specialize "&"? NamedType + ImplementsKeyword "&"? NamedType } FieldsDefinition { @@ -144,27 +176,31 @@ TypeDefinition { } ScalarTypeDefinition /* precedence: right 0 */ { - Description? @specialize Name Directives? + Description? ScalarKeyword Name Directives? +} + +TypeKeyword { + @specialize } ObjectTypeDefinition /* precedence: right 0 */ { - Description? @specialize Name ImplementsInterfaces? Directives? FieldsDefinition? + Description? TypeKeyword Name ImplementsInterfaces? Directives? FieldsDefinition? } InterfaceTypeDefinition /* precedence: right 0 */ { - Description? @specialize Name ImplementsInterfaces? Directives? FieldsDefinition? + Description? InterfaceKeyword Name ImplementsInterfaces? Directives? FieldsDefinition? } UnionTypeDefinition /* precedence: right 0 */ { - Description? @specialize Name Directives? UnionMemberTypes? + Description? UnionKeyword Name Directives? UnionMemberTypes? } EnumTypeDefinition /* precedence: right 0 */ { - Description? @specialize Name Directives? !typeDef EnumValuesDefinition? + Description? EnumKeyword Name Directives? !typeDef EnumValuesDefinition? } InputObjectTypeDefinition /* precedence: right 0 */ { - Description? @specialize Name Directives? !typeDef InputFieldsDefinition? + Description? InputKeyword Name Directives? !typeDef InputFieldsDefinition? } VariableDefinitions { @@ -237,8 +273,12 @@ FragmentSpread { "..." FragmentName Directives? } +FragmentKeyword { + @specialize +} + FragmentDefinition { - @specialize FragmentName TypeCondition Directives? SelectionSet + FragmentKeyword FragmentName TypeCondition Directives? SelectionSet } FragmentName { @@ -249,20 +289,36 @@ InlineFragment { "..." TypeCondition? Directives? SelectionSet } +OnKeyword { + @specialize +} + TypeCondition { - @specialize NamedType + OnKeyword NamedType } Directives { Directive+ } +DirectiveName { + "@" Name +} + Directive { - "@" Name Arguments? + DirectiveName Arguments? +} + +DirectiveKeyword { + @specialize +} + +RepeatableKeyword { + @specialize } DirectiveDefinition /* precedence: right 1 */ { - Description? @specialize "@" Name ArgumentsDefinition? @specialize ? @specialize DirectiveLocations + Description? DirectiveKeyword "@" Name ArgumentsDefinition? RepeatableKeyword ? OnKeyword DirectiveLocations } DirectiveLocations { @@ -299,8 +355,8 @@ Description { } OperationType { - @specialize - | @specialize + @specialize + | @specialize | @specialize } @@ -317,7 +373,7 @@ ExecutableDirectiveLocation { @specialize | @specialize | @specialize - | @specialize + | @specialize | @specialize | @specialize | @specialize @@ -338,10 +394,9 @@ TypeSystemDirectiveLocation { | @specialize } -@skip { Whitespace | Comment } @tokens { - Whitespace { + whitespace { std.whitespace+ } StringValue { @@ -353,7 +408,7 @@ TypeSystemDirectiveLocation { } FloatValue { - IntValue ("." std.digit+ | ("e" | "E") IntValue+) + IntValue ("." std.digit+ | ("e" | "E") IntValue+) } @precedence { IntValue, FloatValue } @@ -361,12 +416,19 @@ TypeSystemDirectiveLocation { Name { $[_A-Za-z] $[_0-9A-Za-z]* } - Comment { - "#" ![\n]* - } + Comma { "," } + + Comment { + "#" ![\n]* + } + + + "{" "}" } -@detectDelim \ No newline at end of file +@skip { whitespace | Comment } + +@detectDelim diff --git a/packages/hoppscotch-app/.env.example b/packages/hoppscotch-app/.env.example index 7c67ad4b6..2f2d14276 100644 --- a/packages/hoppscotch-app/.env.example +++ b/packages/hoppscotch-app/.env.example @@ -16,3 +16,7 @@ MEASUREMENT_ID=G-BBJ3R80PJT # Base URL BASE_URL=https://hoppscotch.io + +# Backend URLs +BACKEND_GQL_URL=https://api.hoppscotch.io/graphql +BACKEND_WS_URL=wss://api.hoppscotch.io/graphql diff --git a/packages/hoppscotch-app/assets/icons/arrow-down-left.svg b/packages/hoppscotch-app/assets/icons/arrow-down-left.svg new file mode 100644 index 000000000..583988208 --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/arrow-down-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/hoppscotch-app/assets/icons/arrow-down.svg b/packages/hoppscotch-app/assets/icons/arrow-down.svg new file mode 100644 index 000000000..ad365d5fc --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/arrow-down.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/hoppscotch-app/assets/icons/arrow-up-right.svg b/packages/hoppscotch-app/assets/icons/arrow-up-right.svg new file mode 100644 index 000000000..6ac911836 --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/arrow-up-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/hoppscotch-app/assets/icons/arrow-up.svg b/packages/hoppscotch-app/assets/icons/arrow-up.svg new file mode 100644 index 000000000..9966fa544 --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/arrow-up.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/hoppscotch-app/assets/icons/chevrons-down.svg b/packages/hoppscotch-app/assets/icons/chevrons-down.svg new file mode 100644 index 000000000..b46abe3a6 --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/chevrons-down.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/hoppscotch-app/assets/icons/chevrons-up.svg b/packages/hoppscotch-app/assets/icons/chevrons-up.svg new file mode 100644 index 000000000..d79fc4952 --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/chevrons-up.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/hoppscotch-app/assets/icons/filter.svg b/packages/hoppscotch-app/assets/icons/filter.svg new file mode 100644 index 000000000..7a4f7d09c --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/filter.svg @@ -0,0 +1,13 @@ + + + diff --git a/packages/hoppscotch-app/assets/icons/info-disconnect.svg b/packages/hoppscotch-app/assets/icons/info-disconnect.svg new file mode 100644 index 000000000..ab55c86fd --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/info-disconnect.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/hoppscotch-app/assets/icons/info-realtime.svg b/packages/hoppscotch-app/assets/icons/info-realtime.svg new file mode 100644 index 000000000..ab55c86fd --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/info-realtime.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/hoppscotch-app/assets/icons/send.svg b/packages/hoppscotch-app/assets/icons/send.svg new file mode 100644 index 000000000..b1de9a99e --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/send.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/hoppscotch-app/assets/scss/styles.scss b/packages/hoppscotch-app/assets/scss/styles.scss index 6bf32b093..79f665443 100644 --- a/packages/hoppscotch-app/assets/scss/styles.scss +++ b/packages/hoppscotch-app/assets/scss/styles.scss @@ -15,6 +15,7 @@ ::-webkit-scrollbar-track { @apply bg-transparent; + @apply border-solid border-l border-t-0 border-b-0 border-r-0 border-dividerLight; } ::-webkit-scrollbar-thumb { @@ -27,17 +28,17 @@ ::-webkit-scrollbar { @apply w-4; - @apply h-4; + @apply h-0; } -.hide-scrollbar { - -ms-overflow-style: none; - scrollbar-width: none; -} +// .hide-scrollbar { +// -ms-overflow-style: none; +// scrollbar-width: none; +// } -.hide-scrollbar::-webkit-scrollbar { - @apply hidden; -} +// .hide-scrollbar::-webkit-scrollbar { +// @apply hidden; +// } input::placeholder, textarea::placeholder, diff --git a/packages/hoppscotch-app/assets/scss/themes.scss b/packages/hoppscotch-app/assets/scss/themes.scss index a1b025231..f641e8fcb 100644 --- a/packages/hoppscotch-app/assets/scss/themes.scss +++ b/packages/hoppscotch-app/assets/scss/themes.scss @@ -255,6 +255,7 @@ --upper-mobile-raw-tertiary-sticky-fold: 8.188rem; --lower-primary-sticky-fold: 3rem; --lower-secondary-sticky-fold: 5rem; + --lower-tertiary-sticky-fold: 7.05rem; --sidebar-primary-sticky-fold: 2rem; } @@ -270,6 +271,7 @@ --upper-mobile-raw-tertiary-sticky-fold: 8.938rem; --lower-primary-sticky-fold: 3.25rem; --lower-secondary-sticky-fold: 5.5rem; + --lower-tertiary-sticky-fold: 7.8rem; --sidebar-primary-sticky-fold: 2.25rem; } @@ -285,6 +287,7 @@ --upper-mobile-raw-tertiary-sticky-fold: 9.688rem; --lower-primary-sticky-fold: 3.5rem; --lower-secondary-sticky-fold: 6rem; + --lower-tertiary-sticky-fold: 8.55rem; --sidebar-primary-sticky-fold: 2.5rem; } diff --git a/packages/hoppscotch-app/components/app/DeveloperOptions.vue b/packages/hoppscotch-app/components/app/DeveloperOptions.vue index a75d80ac0..4f5e06a7c 100644 --- a/packages/hoppscotch-app/components/app/DeveloperOptions.vue +++ b/packages/hoppscotch-app/components/app/DeveloperOptions.vue @@ -22,7 +22,7 @@ diff --git a/packages/hoppscotch-app/components/app/PaneLayout.vue b/packages/hoppscotch-app/components/app/PaneLayout.vue index 0fbb92388..e5b0f7079 100644 --- a/packages/hoppscotch-app/components/app/PaneLayout.vue +++ b/packages/hoppscotch-app/components/app/PaneLayout.vue @@ -28,7 +28,7 @@ !!slots.sidebar) diff --git a/packages/hoppscotch-app/components/app/Share.vue b/packages/hoppscotch-app/components/app/Share.vue index bcc72ce1c..b11c30370 100644 --- a/packages/hoppscotch-app/components/app/Share.vue +++ b/packages/hoppscotch-app/components/app/Share.vue @@ -36,7 +36,7 @@ @@ -105,6 +111,20 @@ const primaryNavigation = [ @apply text-tiny; } + &.active-link { + @apply text-secondaryDark; + @apply bg-primaryLight; + @apply hover:text-secondaryDark; + + .material-icons, + .svg-icons { + @apply opacity-100; + } + + &::after { + @apply bg-accent; + } + } &.exact-active-link { @apply text-secondaryDark; @apply bg-primaryLight; diff --git a/packages/hoppscotch-app/components/collections/ChooseType.vue b/packages/hoppscotch-app/components/collections/ChooseType.vue index 4ecdb7fc4..bd9cce100 100644 --- a/packages/hoppscotch-app/components/collections/ChooseType.vue +++ b/packages/hoppscotch-app/components/collections/ChooseType.vue @@ -1,6 +1,10 @@ - -
- - - {{ header.key }} - - - - - {{ header.value }} - - -
+ :key="index" + :header="header" + /> diff --git a/packages/hoppscotch-app/components/lenses/HeadersRendererEntry.vue b/packages/hoppscotch-app/components/lenses/HeadersRendererEntry.vue new file mode 100644 index 000000000..70c075cab --- /dev/null +++ b/packages/hoppscotch-app/components/lenses/HeadersRendererEntry.vue @@ -0,0 +1,51 @@ + + + diff --git a/packages/hoppscotch-app/components/lenses/ResponseBodyRenderer.vue b/packages/hoppscotch-app/components/lenses/ResponseBodyRenderer.vue index 84f940a7b..0e9bdf782 100644 --- a/packages/hoppscotch-app/components/lenses/ResponseBodyRenderer.vue +++ b/packages/hoppscotch-app/components/lenses/ResponseBodyRenderer.vue @@ -3,6 +3,7 @@ v-if="response" v-model="selectedLensTab" styles="sticky z-10 bg-primary top-lowerPrimaryStickyFold" + render-inactive-tabs > -
+
-
+
+
-
+
+
+ + + + +
+ + {{ filterResponseError.error }} +
+ +
+
+
import * as LJSON from "lossless-json" import * as O from "fp-ts/Option" +import * as E from "fp-ts/Either" import { pipe } from "fp-ts/function" import { computed, ref, reactive } from "@nuxtjs/composition-api" +import { JSONPath } from "jsonpath-plus" import { useCodemirror } from "~/helpers/editor/codemirror" import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse" import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse" @@ -165,16 +218,51 @@ const props = defineProps<{ const { responseBodyText } = useResponseBody(props.response) -const { copyIcon, copyResponse } = useCopyResponse(responseBodyText) +const toggleFilter = ref(false) +const filterQueryText = ref("") -const { downloadIcon, downloadResponse } = useDownloadResponse( - "application/json", - responseBodyText +type BodyParseError = + | { type: "JSON_PARSE_FAILED" } + | { type: "JSON_PATH_QUERY_FAILED"; error: Error } + +const responseJsonObject = computed(() => + pipe( + responseBodyText.value, + E.tryCatchK( + LJSON.parse, + (): BodyParseError => ({ type: "JSON_PARSE_FAILED" }) + ) + ) ) +const jsonResponseBodyText = computed(() => { + if (filterQueryText.value.length > 0) { + return pipe( + responseJsonObject.value, + E.chain((parsedJSON) => + E.tryCatch( + () => + JSONPath({ + path: filterQueryText.value, + json: parsedJSON, + }) as undefined, + (err): BodyParseError => ({ + type: "JSON_PATH_QUERY_FAILED", + error: err as Error, + }) + ) + ), + E.map(JSON.stringify) + ) + } else { + return E.right(responseBodyText.value) + } +}) + const jsonBodyText = computed(() => pipe( - responseBodyText.value, + jsonResponseBodyText.value, + E.getOrElse(() => responseBodyText.value), O.tryCatchK(LJSON.parse), O.map((val) => LJSON.stringify(val, undefined, 2)), O.getOrElse(() => responseBodyText.value) @@ -189,6 +277,38 @@ const ast = computed(() => ) ) +const filterResponseError = computed(() => + pipe( + jsonResponseBodyText.value, + E.match( + (e) => { + switch (e.type) { + case "JSON_PATH_QUERY_FAILED": + return { type: "JSON_PATH_QUERY_ERROR", error: e.error.message } + case "JSON_PARSE_FAILED": + return { + type: "JSON_PARSE_FAILED", + error: t("error.json_parsing_failed").toString(), + } + } + }, + (result) => + result === "[]" + ? { + type: "RESPONSE_EMPTY", + error: t("error.no_results_found").toString(), + } + : undefined + ) + ) +) + +const { copyIcon, copyResponse } = useCopyResponse(jsonBodyText) +const { downloadIcon, downloadResponse } = useDownloadResponse( + "application/json", + jsonBodyText +) + const outlineOptions = ref(null) const jsonResponse = ref(null) const linewrapEnabled = ref(true) @@ -227,6 +347,11 @@ const outlinePath = computed(() => O.getOrElseW(() => null) ) ) + +const toggleFilterState = () => { + filterQueryText.value = "" + toggleFilter.value = !toggleFilter.value +} diff --git a/packages/hoppscotch-app/components/realtime/Communication.vue b/packages/hoppscotch-app/components/realtime/Communication.vue new file mode 100644 index 000000000..6750965c7 --- /dev/null +++ b/packages/hoppscotch-app/components/realtime/Communication.vue @@ -0,0 +1,222 @@ + + diff --git a/packages/hoppscotch-app/components/realtime/Log.vue b/packages/hoppscotch-app/components/realtime/Log.vue index 348a2fab7..eb7ec006a 100644 --- a/packages/hoppscotch-app/components/realtime/Log.vue +++ b/packages/hoppscotch-app/components/realtime/Log.vue @@ -1,77 +1,129 @@ - + diff --git a/packages/hoppscotch-app/components/realtime/LogEntry.vue b/packages/hoppscotch-app/components/realtime/LogEntry.vue new file mode 100644 index 000000000..bfbbed2cc --- /dev/null +++ b/packages/hoppscotch-app/components/realtime/LogEntry.vue @@ -0,0 +1,392 @@ + + + + + diff --git a/packages/hoppscotch-app/components/realtime/Mqtt.vue b/packages/hoppscotch-app/components/realtime/Mqtt.vue deleted file mode 100644 index 6cd482c20..000000000 --- a/packages/hoppscotch-app/components/realtime/Mqtt.vue +++ /dev/null @@ -1,382 +0,0 @@ - - - diff --git a/packages/hoppscotch-app/components/realtime/Socketio.vue b/packages/hoppscotch-app/components/realtime/Socketio.vue deleted file mode 100644 index 358941160..000000000 --- a/packages/hoppscotch-app/components/realtime/Socketio.vue +++ /dev/null @@ -1,521 +0,0 @@ - - - diff --git a/packages/hoppscotch-app/components/realtime/Sse.vue b/packages/hoppscotch-app/components/realtime/Sse.vue deleted file mode 100644 index b5c5faf66..000000000 --- a/packages/hoppscotch-app/components/realtime/Sse.vue +++ /dev/null @@ -1,222 +0,0 @@ - - - diff --git a/packages/hoppscotch-app/components/realtime/Websocket.vue b/packages/hoppscotch-app/components/realtime/Websocket.vue deleted file mode 100644 index cd1ce8ee5..000000000 --- a/packages/hoppscotch-app/components/realtime/Websocket.vue +++ /dev/null @@ -1,433 +0,0 @@ - - - diff --git a/packages/hoppscotch-app/components/smart/EnvInput.vue b/packages/hoppscotch-app/components/smart/EnvInput.vue index 196f0fafa..761cc4564 100644 --- a/packages/hoppscotch-app/components/smart/EnvInput.vue +++ b/packages/hoppscotch-app/components/smart/EnvInput.vue @@ -47,6 +47,7 @@ const props = withDefaults( styles: string envs: { key: string; value: string; source: string }[] | null focus: boolean + readonly: boolean }>(), { value: "", @@ -54,6 +55,7 @@ const props = withDefaults( styles: "", envs: null, focus: false, + readonly: false, } ) @@ -123,7 +125,23 @@ const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view) const initView = (el: any) => { const extensions: Extension = [ EditorView.contentAttributes.of({ "aria-label": props.placeholder }), + EditorView.updateListener.of((update) => { + if (props.readonly) { + update.view.contentDOM.inputMode = "none" + } + }), + EditorState.changeFilter.of(() => !props.readonly), inputTheme, + props.readonly + ? EditorView.theme({ + ".cm-content": { + caretColor: "var(--secondary-dark-color) !important", + color: "var(--secondary-dark-color) !important", + backgroundColor: "var(--divider-color) !important", + opacity: 0.25, + }, + }) + : EditorView.theme({}), tooltips({ position: "absolute", }), @@ -141,6 +159,8 @@ const initView = (el: any) => { ViewPlugin.fromClass( class { update(update: ViewUpdate) { + if (props.readonly) return + if (update.docChanged) { const prevValue = clone(cachedValue.value) diff --git a/packages/hoppscotch-app/components/smart/Radio.vue b/packages/hoppscotch-app/components/smart/Radio.vue index 226904dc4..de1ced5f3 100644 --- a/packages/hoppscotch-app/components/smart/Radio.vue +++ b/packages/hoppscotch-app/components/smart/Radio.vue @@ -1,33 +1,31 @@ - diff --git a/packages/hoppscotch-app/components/smart/RadioGroup.vue b/packages/hoppscotch-app/components/smart/RadioGroup.vue index f0363078b..1b1027819 100644 --- a/packages/hoppscotch-app/components/smart/RadioGroup.vue +++ b/packages/hoppscotch-app/components/smart/RadioGroup.vue @@ -5,18 +5,22 @@ :key="`radio-${index}`" :value="radio.value" :label="radio.label" - :selected="selected" - @change="$emit('change', radio.value)" + :selected="value === radio.value" + @change="emit('input', radio.value)" />
diff --git a/packages/hoppscotch-app/components/smart/Tab.vue b/packages/hoppscotch-app/components/smart/Tab.vue index 18d79fcdc..8990ffbfa 100644 --- a/packages/hoppscotch-app/components/smart/Tab.vue +++ b/packages/hoppscotch-app/components/smart/Tab.vue @@ -1,5 +1,5 @@ @@ -33,11 +33,24 @@ const tabMeta = computed(() => ({ label: props.label, })) -const { activeTabID, addTabEntry, updateTabEntry, removeTabEntry } = - inject("tabs-system")! +const { + activeTabID, + renderInactive, + addTabEntry, + updateTabEntry, + removeTabEntry, +} = inject("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) }) diff --git a/packages/hoppscotch-app/components/smart/Tabs.vue b/packages/hoppscotch-app/components/smart/Tabs.vue index 54f5a4e4e..214c1dfc1 100644 --- a/packages/hoppscotch-app/components/smart/Tabs.vue +++ b/packages/hoppscotch-app/components/smart/Tabs.vue @@ -80,6 +80,8 @@ export type TabMeta = { } export type TabProvider = { + // Whether inactive tabs should remain rendered + renderInactive: ComputedRef activeTabID: ComputedRef addTabEntry: (tabID: string, meta: TabMeta) => void updateTabEntry: (tabID: string, newMeta: TabMeta) => void @@ -91,6 +93,10 @@ const props = defineProps({ type: String, default: "", }, + renderInactiveTabs: { + type: Boolean, + default: false, + }, vertical: { type: Boolean, default: false, @@ -144,6 +150,7 @@ const removeTabEntry = (tabID: string) => { } provide("tabs-system", { + renderInactive: computed(() => props.renderInactiveTabs), activeTabID: computed(() => props.value), addTabEntry, updateTabEntry, diff --git a/packages/hoppscotch-app/helpers/RequestRunner.ts b/packages/hoppscotch-app/helpers/RequestRunner.ts index 5265e4142..a6e38ce81 100644 --- a/packages/hoppscotch-app/helpers/RequestRunner.ts +++ b/packages/hoppscotch-app/helpers/RequestRunner.ts @@ -25,7 +25,7 @@ import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession" import { environmentsStore, getCurrentEnvironment, - getEnviroment, + getEnvironment, getGlobalVariables, setGlobalEnvVariables, updateEnvironment, @@ -97,7 +97,7 @@ export const runRESTRequest$ = (): TaskEither< setGlobalEnvVariables(runResult.right.envs.global) if (environmentsStore.value.currentEnvironmentIndex !== -1) { - const env = getEnviroment( + const env = getEnvironment( environmentsStore.value.currentEnvironmentIndex ) updateEnvironment( diff --git a/packages/hoppscotch-app/helpers/backend/GQLClient.ts b/packages/hoppscotch-app/helpers/backend/GQLClient.ts index 69d323180..ae8aa012f 100644 --- a/packages/hoppscotch-app/helpers/backend/GQLClient.ts +++ b/packages/hoppscotch-app/helpers/backend/GQLClient.ts @@ -45,28 +45,23 @@ import { } from "~/helpers/fb/auth" const BACKEND_GQL_URL = - process.env.context === "production" - ? "https://api.hoppscotch.io/graphql" - : "https://api.hoppscotch.io/graphql" + process.env.BACKEND_GQL_URL ?? "https://api.hoppscotch.io/graphql" +const BACKEND_WS_URL = + process.env.BACKEND_WS_URL ?? "wss://api.hoppscotch.io/graphql" // const storage = makeDefaultStorage({ // idbName: "hoppcache-v1", // maxAge: 7, // }) -const subscriptionClient = new SubscriptionClient( - process.env.context === "production" - ? "wss://api.hoppscotch.io/graphql" - : "wss://api.hoppscotch.io/graphql", - { - reconnect: true, - connectionParams: () => { - return { - authorization: `Bearer ${authIdToken$.value}`, - } - }, - } -) +const subscriptionClient = new SubscriptionClient(BACKEND_WS_URL, { + reconnect: true, + connectionParams: () => { + return { + authorization: `Bearer ${authIdToken$.value}`, + } + }, +}) authIdToken$.subscribe(() => { subscriptionClient.client?.close() diff --git a/packages/hoppscotch-app/helpers/backend/gql/mutations/DeleteShortcode.graphql b/packages/hoppscotch-app/helpers/backend/gql/mutations/DeleteShortcode.graphql new file mode 100644 index 000000000..38935eb18 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/mutations/DeleteShortcode.graphql @@ -0,0 +1,3 @@ +mutation DeleteShortcode($code: ID!) { + revokeShortcode(code: $code) +} \ No newline at end of file diff --git a/packages/hoppscotch-app/helpers/backend/gql/queries/GetMyShortcodes.graphql b/packages/hoppscotch-app/helpers/backend/gql/queries/GetMyShortcodes.graphql new file mode 100644 index 000000000..da986ca69 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/queries/GetMyShortcodes.graphql @@ -0,0 +1,7 @@ +query GetUserShortcodes($cursor: ID) { + myShortcodes(cursor: $cursor) { + id + request + createdOn + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeCreated.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeCreated.graphql new file mode 100644 index 000000000..557b90fa2 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeCreated.graphql @@ -0,0 +1,7 @@ +subscription ShortcodeCreated { + myShortcodesCreated { + id + request + createdOn + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeDeleted.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeDeleted.graphql new file mode 100644 index 000000000..6c6506447 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeDeleted.graphql @@ -0,0 +1,5 @@ +subscription ShortcodeDeleted { + myShortcodesRevoked { + id + } +} diff --git a/packages/hoppscotch-app/helpers/backend/helpers.ts b/packages/hoppscotch-app/helpers/backend/helpers.ts index 9982374f9..3b1eac7bc 100644 --- a/packages/hoppscotch-app/helpers/backend/helpers.ts +++ b/packages/hoppscotch-app/helpers/backend/helpers.ts @@ -17,7 +17,7 @@ import { GetCollectionTitleDocument, } from "./graphql" -const BACKEND_PAGE_SIZE = 10 +export const BACKEND_PAGE_SIZE = 10 const getCollectionChildrenIDs = async (collID: string) => { const collsList: string[] = [] diff --git a/packages/hoppscotch-app/helpers/backend/mutations/Shortcode.ts b/packages/hoppscotch-app/helpers/backend/mutations/Shortcode.ts index 5ae84bc7c..e02759ab9 100644 --- a/packages/hoppscotch-app/helpers/backend/mutations/Shortcode.ts +++ b/packages/hoppscotch-app/helpers/backend/mutations/Shortcode.ts @@ -4,8 +4,13 @@ import { CreateShortcodeDocument, CreateShortcodeMutation, CreateShortcodeMutationVariables, + DeleteShortcodeDocument, + DeleteShortcodeMutation, + DeleteShortcodeMutationVariables, } from "../graphql" +type DeleteShortcodeErrors = "shortcode/not_found" + export const createShortcode = (request: HoppRESTRequest) => runMutation( CreateShortcodeDocument, @@ -13,3 +18,12 @@ export const createShortcode = (request: HoppRESTRequest) => request: JSON.stringify(request), } ) + +export const deleteShortcode = (code: string) => + runMutation< + DeleteShortcodeMutation, + DeleteShortcodeMutationVariables, + DeleteShortcodeErrors + >(DeleteShortcodeDocument, { + code, + }) diff --git a/packages/hoppscotch-app/helpers/curl/__tests__/curlparser.spec.js b/packages/hoppscotch-app/helpers/curl/__tests__/curlparser.spec.js index 2a9fd18dd..0d649a2cf 100644 --- a/packages/hoppscotch-app/helpers/curl/__tests__/curlparser.spec.js +++ b/packages/hoppscotch-app/helpers/curl/__tests__/curlparser.spec.js @@ -768,11 +768,52 @@ const samples = [ testScript: "", }), }, + { + command: `curl \` + google.com -H "content-type: application/json"`, + response: makeRESTRequest({ + method: "GET", + name: "Untitled request", + endpoint: "https://google.com/", + auth: { + authType: "none", + authActive: true, + }, + body: { + contentType: null, + body: null, + }, + params: [], + headers: [], + preRequestScript: "", + testScript: "", + }), + }, + { + command: `curl 192.168.0.24:8080/ping`, + response: makeRESTRequest({ + method: "GET", + name: "Untitled request", + endpoint: "http://192.168.0.24:8080/ping", + auth: { + authType: "none", + authActive: true, + }, + body: { + contentType: null, + body: null, + }, + params: [], + headers: [], + preRequestScript: "", + testScript: "", + }), + }, ] -describe("parseCurlToHoppRESTReq", () => { +describe("Parse curl command to Hopp REST Request", () => { for (const [i, { command, response }] of samples.entries()) { - test(`matches expectation for sample #${i + 1}`, () => { + test(`for sample #${i + 1}:\n\n${command}`, () => { expect(parseCurlToHoppRESTReq(command)).toEqual(response) }) } diff --git a/packages/hoppscotch-app/helpers/curl/curlparser.ts b/packages/hoppscotch-app/helpers/curl/curlparser.ts index 5b54a46fe..21b0b48b1 100644 --- a/packages/hoppscotch-app/helpers/curl/curlparser.ts +++ b/packages/hoppscotch-app/helpers/curl/curlparser.ts @@ -12,7 +12,7 @@ import { getHeaders, recordToHoppHeaders } from "./sub_helpers/headers" // import { getCookies } from "./sub_helpers/cookies" import { getQueries } from "./sub_helpers/queries" import { getMethod } from "./sub_helpers/method" -import { concatParams, parseURL } from "./sub_helpers/url" +import { concatParams, getURLObject } from "./sub_helpers/url" import { preProcessCurlCommand } from "./sub_helpers/preproc" import { getBody, getFArgumentMultipartData } from "./sub_helpers/body" import { getDefaultRESTRequest } from "~/newstore/RESTSession" @@ -42,7 +42,7 @@ export const parseCurlCommand = (curlCommand: string) => { const method = getMethod(parsedArguments) // const cookies = getCookies(parsedArguments) - const urlObject = parseURL(parsedArguments) + const urlObject = getURLObject(parsedArguments) const auth = getAuthObject(parsedArguments, headers, urlObject) let rawData: string | string[] = pipe( diff --git a/packages/hoppscotch-app/helpers/curl/sub_helpers/contentParser.ts b/packages/hoppscotch-app/helpers/curl/sub_helpers/contentParser.ts index b8e7a8947..6ca7206fd 100644 --- a/packages/hoppscotch-app/helpers/curl/sub_helpers/contentParser.ts +++ b/packages/hoppscotch-app/helpers/curl/sub_helpers/contentParser.ts @@ -161,8 +161,7 @@ const getXMLBody = (rawData: string) => const getFormattedJSON = flow( safeParseJSON, O.map((parsedJSON) => JSON.stringify(parsedJSON, null, 2)), - O.getOrElse(() => "{}"), - O.of + O.getOrElse(() => "{ }") ) const getXWWWFormUrlEncodedBody = flow( @@ -189,7 +188,7 @@ export function parseBody( case "application/ld+json": case "application/vnd.api+json": case "application/json": - return getFormattedJSON(rawData) + return O.some(getFormattedJSON(rawData)) case "application/x-www-form-urlencoded": return getXWWWFormUrlEncodedBody(rawData) diff --git a/packages/hoppscotch-app/helpers/curl/sub_helpers/preproc.ts b/packages/hoppscotch-app/helpers/curl/sub_helpers/preproc.ts index 456270a1b..e79d8a164 100644 --- a/packages/hoppscotch-app/helpers/curl/sub_helpers/preproc.ts +++ b/packages/hoppscotch-app/helpers/curl/sub_helpers/preproc.ts @@ -19,10 +19,11 @@ const replaceables: { [key: string]: string } = { const paperCuts = flow( // remove '\' and newlines S.replace(/ ?\\ ?$/gm, " "), - S.replace(/\n/g, ""), + S.replace(/\n/g, " "), // remove all $ symbols from start of argument values S.replace(/\$'/g, "'"), - S.replace(/\$"/g, '"') + S.replace(/\$"/g, '"'), + S.trim ) // replace --zargs option with -Z diff --git a/packages/hoppscotch-app/helpers/curl/sub_helpers/url.ts b/packages/hoppscotch-app/helpers/curl/sub_helpers/url.ts index 2fdb1d9ce..ef352280f 100644 --- a/packages/hoppscotch-app/helpers/curl/sub_helpers/url.ts +++ b/packages/hoppscotch-app/helpers/curl/sub_helpers/url.ts @@ -1,48 +1,80 @@ import parser from "yargs-parser" import { pipe } from "fp-ts/function" import * as O from "fp-ts/Option" +import * as A from "fp-ts/Array" import { getDefaultRESTRequest } from "~/newstore/RESTSession" import { stringArrayJoin } from "~/helpers/functional/array" const defaultRESTReq = getDefaultRESTRequest() -const getProtocolForBaseURL = (baseURL: string) => +const getProtocolFromURL = (url: string) => pipe( // get the base URL - /^([^\s:@]+:[^\s:@]+@)?([^:/\s]+)([:]*)/.exec(baseURL), + /^([^\s:@]+:[^\s:@]+@)?([^:/\s]+)([:]*)/.exec(url), O.fromNullable, O.filter((burl) => burl.length > 1), O.map((burl) => burl[2]), // set protocol to http for local URLs O.map((burl) => - burl === "localhost" || burl === "127.0.0.1" - ? "http://" + baseURL - : "https://" + baseURL + burl === "localhost" || + burl === "2130706433" || + /127(\.0){0,2}\.1/.test(burl) || + /0177(\.0){0,2}\.1/.test(burl) || + /0x7f(\.0){0,2}\.1/.test(burl) || + /192\.168(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){2}/.test(burl) || + /10(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/.test(burl) + ? "http://" + url + : "https://" + url ) ) +/** + * Checks if the URL is valid using the URL constructor + * @param urlString URL string (with protocol) + * @returns boolean whether the URL is valid using the inbuilt URL class + */ +const isURLValid = (urlString: string) => + pipe( + O.tryCatch(() => new URL(urlString)), + O.isSome + ) + +/** + * Checks and returns URL object for the valid URL + * @param urlText Raw URL string provided by argument parser + * @returns Option of URL object + */ +const parseURL = (urlText: string | number) => + pipe( + urlText, + O.fromNullable, + // preprocess url string + O.map((u) => u.toString().replaceAll(/[^a-zA-Z0-9_\-./?&=:@%+#,;\s]/g, "")), + O.filter((u) => u.length > 0), + O.chain((u) => + pipe( + u, + // check if protocol is available + O.fromPredicate( + (url: string) => /^[^:\s]+(?=:\/\/)/.exec(url) !== null + ), + O.alt(() => getProtocolFromURL(u)) + ) + ), + O.filter(isURLValid), + O.map((u) => new URL(u)) + ) + /** * Processes URL string and returns the URL object * @param parsedArguments Parsed Arguments object * @returns URL object */ -export function parseURL(parsedArguments: parser.Arguments) { +export function getURLObject(parsedArguments: parser.Arguments) { return pipe( - // contains raw url string - parsedArguments._[1], - O.fromNullable, - // preprocess url string - O.map((u) => u.toString().replace(/["']/g, "").trim()), - O.chain((u) => - pipe( - // check if protocol is available - /^[^:\s]+(?=:\/\/)/.exec(u), - O.fromNullable, - O.map((_) => u), - O.alt(() => getProtocolForBaseURL(u)) - ) - ), - O.map((u) => new URL(u)), + // contains raw url strings + parsedArguments._.slice(1), + A.findFirstMap(parseURL), // no url found O.getOrElse(() => new URL(defaultRESTReq.endpoint)) ) diff --git a/packages/hoppscotch-app/helpers/editor/codemirror.ts b/packages/hoppscotch-app/helpers/editor/codemirror.ts index fd7b25c1d..e591dbde5 100644 --- a/packages/hoppscotch-app/helpers/editor/codemirror.ts +++ b/packages/hoppscotch-app/helpers/editor/codemirror.ts @@ -28,8 +28,6 @@ import { javascriptLanguage } from "@codemirror/lang-javascript" import { xmlLanguage } from "@codemirror/lang-xml" import { jsonLanguage } from "@codemirror/lang-json" import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql" -import { pipe } from "fp-ts/function" -import * as O from "fp-ts/Option" import { StreamLanguage } from "@codemirror/stream-parser" import { html } from "@codemirror/legacy-modes/mode/xml" import { shell } from "@codemirror/legacy-modes/mode/shell" @@ -40,7 +38,6 @@ import { Completer } from "./completion" import { LinterDefinition } from "./linting/linter" import { basicSetup, baseTheme, baseHighlightStyle } from "./themes/baseTheme" import { HoppEnvironmentPlugin } from "./extensions/HoppEnvironment" -import { IndentedLineWrapPlugin } from "./extensions/IndentedLineWrap" // TODO: Migrate from legacy mode type ExtendedEditorConfig = { @@ -96,8 +93,10 @@ const hoppCompleterExt = (completer: Completer): Extension => { }) } -const hoppLinterExt = (hoppLinter: LinterDefinition): Extension => { +const hoppLinterExt = (hoppLinter: LinterDefinition | undefined): Extension => { return linter(async (view) => { + if (!hoppLinter) return [] + // Requires full document scan, hence expensive on big files, force disable on big files ? const linterResult = await hoppLinter( view.state.doc.toJSON().join(view.state.lineBreak) @@ -119,16 +118,16 @@ const hoppLinterExt = (hoppLinter: LinterDefinition): Extension => { } const hoppLang = ( - language: Language, + language: Language | undefined, linter?: LinterDefinition | undefined, completer?: Completer | undefined -) => { +): Extension | LanguageSupport => { const exts: Extension[] = [] - if (linter) exts.push(hoppLinterExt(linter)) + exts.push(hoppLinterExt(linter)) if (completer) exts.push(hoppCompleterExt(completer)) - return new LanguageSupport(language, exts) + return language ? new LanguageSupport(language, exts) : exts } const getLanguage = (langMime: string): Language | null => { @@ -156,12 +155,7 @@ const getEditorLanguage = ( langMime: string, linter: LinterDefinition | undefined, completer: Completer | undefined -): Extension => - pipe( - O.fromNullable(getLanguage(langMime)), - O.map((lang) => hoppLang(lang, linter, completer)), - O.getOrElseW(() => []) - ) +): Extension => hoppLang(getLanguage(langMime) ?? undefined, linter, completer) export function useCodemirror( el: Ref, @@ -243,7 +237,7 @@ export function useCodemirror( ), lineWrapping.of( options.extendedEditorConfig.lineWrapping - ? [IndentedLineWrapPlugin] + ? [EditorView.lineWrapping] : [] ), keymap.of([ @@ -330,7 +324,7 @@ export function useCodemirror( (newMode) => { view.value?.dispatch({ effects: lineWrapping.reconfigure( - newMode ? [EditorView.lineWrapping, IndentedLineWrapPlugin] : [] + newMode ? [EditorView.lineWrapping] : [] ), }) } diff --git a/packages/hoppscotch-app/helpers/editor/extensions/IndentedLineWrap.ts b/packages/hoppscotch-app/helpers/editor/extensions/IndentedLineWrap.ts deleted file mode 100644 index 5ad2519cb..000000000 --- a/packages/hoppscotch-app/helpers/editor/extensions/IndentedLineWrap.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { EditorView } from "@codemirror/view" - -const WrappedLineIndenter = EditorView.updateListener.of((update) => { - const view = update.view - const charWidth = view.defaultCharacterWidth - const lineHeight = view.defaultLineHeight - const basePadding = 10 - - view.viewportLines((line) => { - const domAtPos = view.domAtPos(line.from) - - const lineCount = (line.bottom - line.top) / lineHeight - - if (lineCount <= 1) return - - const belowPadding = basePadding * charWidth - - const node = domAtPos.node as HTMLElement - node.style.textIndent = `-${belowPadding - charWidth + 1}px` - node.style.paddingLeft = `${belowPadding}px` - }) -}) - -export const IndentedLineWrapPlugin = [ - EditorView.lineWrapping, - WrappedLineIndenter, -] diff --git a/packages/hoppscotch-app/helpers/functional/debug.ts b/packages/hoppscotch-app/helpers/functional/debug.ts index fc4024c62..9f886e1e1 100644 --- a/packages/hoppscotch-app/helpers/functional/debug.ts +++ b/packages/hoppscotch-app/helpers/functional/debug.ts @@ -17,6 +17,6 @@ export const trace = (x: T) => { export const namedTrace = (name: string) => (x: T) => { - console.log(`${name}: `, x) + console.log(`${name}:`, x) return x } diff --git a/packages/hoppscotch-app/helpers/functional/json.ts b/packages/hoppscotch-app/helpers/functional/json.ts index db5931932..1f263e7f6 100644 --- a/packages/hoppscotch-app/helpers/functional/json.ts +++ b/packages/hoppscotch-app/helpers/functional/json.ts @@ -1,4 +1,5 @@ import * as O from "fp-ts/Option" +import { flow } from "fp-ts/function" /** * Checks and Parses JSON string @@ -15,3 +16,10 @@ export const safeParseJSON = (str: string): O.Option => */ export const prettyPrintJSON = (obj: unknown): O.Option => O.tryCatch(() => JSON.stringify(obj, null, "\t")) + +/** + * Checks if given string is a JSON string + * @param str Raw string to be checked + * @returns If string is a JSON string + */ +export const isJSON = flow(safeParseJSON, O.isSome) diff --git a/packages/hoppscotch-app/helpers/lenses/composables/useCopyResponse.ts b/packages/hoppscotch-app/helpers/lenses/composables/useCopyResponse.ts index bad75c68f..a7ca65917 100644 --- a/packages/hoppscotch-app/helpers/lenses/composables/useCopyResponse.ts +++ b/packages/hoppscotch-app/helpers/lenses/composables/useCopyResponse.ts @@ -1,4 +1,5 @@ -import { Ref, ref } from "@nuxtjs/composition-api" +import { Ref } from "@nuxtjs/composition-api" +import { refAutoReset } from "@vueuse/core" import { copyToClipboard } from "~/helpers/utils/clipboard" import { useI18n, useToast } from "~/helpers/utils/composables" @@ -8,13 +9,13 @@ export default function useCopyResponse(responseBodyText: Ref): { } { const toast = useToast() const t = useI18n() - const copyIcon = ref("copy") + + const copyIcon = refAutoReset<"copy" | "check">("copy", 1000) const copyResponse = () => { copyToClipboard(responseBodyText.value) copyIcon.value = "check" toast.success(`${t("state.copied_to_clipboard")}`) - setTimeout(() => (copyIcon.value = "copy"), 1000) } return { diff --git a/packages/hoppscotch-app/helpers/lenses/composables/useDownloadResponse.ts b/packages/hoppscotch-app/helpers/lenses/composables/useDownloadResponse.ts index 95a070169..8e9582b27 100644 --- a/packages/hoppscotch-app/helpers/lenses/composables/useDownloadResponse.ts +++ b/packages/hoppscotch-app/helpers/lenses/composables/useDownloadResponse.ts @@ -1,7 +1,8 @@ import * as S from "fp-ts/string" import * as RNEA from "fp-ts/ReadonlyNonEmptyArray" import { pipe } from "fp-ts/function" -import { Ref, ref } from "@nuxtjs/composition-api" +import { Ref } from "@nuxtjs/composition-api" +import { refAutoReset } from "@vueuse/core" import { useI18n, useToast } from "~/helpers/utils/composables" export type downloadResponseReturnType = (() => void) | Ref @@ -13,7 +14,8 @@ export default function useDownloadResponse( downloadIcon: Ref downloadResponse: () => void } { - const downloadIcon = ref("download") + const downloadIcon = refAutoReset<"download" | "check">("download", 1000) + const toast = useToast() const t = useI18n() @@ -42,7 +44,6 @@ export default function useDownloadResponse( setTimeout(() => { document.body.removeChild(a) URL.revokeObjectURL(url) - downloadIcon.value = "download" }, 1000) } return { diff --git a/packages/hoppscotch-app/helpers/realtime/MQTTConnection.ts b/packages/hoppscotch-app/helpers/realtime/MQTTConnection.ts new file mode 100644 index 000000000..978663d57 --- /dev/null +++ b/packages/hoppscotch-app/helpers/realtime/MQTTConnection.ts @@ -0,0 +1,223 @@ +import Paho, { ConnectionOptions } from "paho-mqtt" +import { BehaviorSubject, Subject } from "rxjs" +import { logHoppRequestRunToAnalytics } from "../fb/analytics" + +export type MQTTMessage = { topic: string; message: string } +export type MQTTError = + | { type: "CONNECTION_NOT_ESTABLISHED"; value: unknown } + | { type: "CONNECTION_LOST" } + | { type: "CONNECTION_FAILED" } + | { type: "SUBSCRIPTION_FAILED"; topic: string } + | { type: "PUBLISH_ERROR"; topic: string; message: string } + +export type MQTTEvent = { time: number } & ( + | { type: "CONNECTING" } + | { type: "CONNECTED" } + | { type: "MESSAGE_SENT"; message: MQTTMessage } + | { type: "SUBSCRIBED"; topic: string } + | { type: "SUBSCRIPTION_FAILED"; topic: string } + | { type: "MESSAGE_RECEIVED"; message: MQTTMessage } + | { type: "DISCONNECTED"; manual: boolean } + | { type: "ERROR"; error: MQTTError } +) + +export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED" + +export class MQTTConnection { + subscriptionState$ = new BehaviorSubject(false) + connectionState$ = new BehaviorSubject("DISCONNECTED") + event$: Subject = new Subject() + + private mqttClient: Paho.Client | undefined + private manualDisconnect = false + + private addEvent(event: MQTTEvent) { + this.event$.next(event) + } + + connect(url: string, username: string, password: string) { + try { + this.connectionState$.next("CONNECTING") + + this.addEvent({ + time: Date.now(), + type: "CONNECTING", + }) + + const parseUrl = new URL(url) + const { hostname, pathname, port } = parseUrl + this.mqttClient = new Paho.Client( + `${hostname + (pathname !== "/" ? pathname : "")}`, + port !== "" ? Number(port) : 8081, + "hoppscotch" + ) + const connectOptions: ConnectionOptions = { + onSuccess: this.onConnectionSuccess.bind(this), + onFailure: this.onConnectionFailure.bind(this), + useSSL: parseUrl.protocol !== "ws:", + } + if (username !== "") { + connectOptions.userName = username + } + if (password !== "") { + connectOptions.password = password + } + this.mqttClient.connect(connectOptions) + this.mqttClient.onConnectionLost = this.onConnectionLost.bind(this) + this.mqttClient.onMessageArrived = this.onMessageArrived.bind(this) + } catch (e) { + this.handleError(e) + } + + logHoppRequestRunToAnalytics({ + platform: "mqtt", + }) + } + + onConnectionFailure() { + this.connectionState$.next("DISCONNECTED") + this.addEvent({ + time: Date.now(), + type: "ERROR", + error: { + type: "CONNECTION_FAILED", + }, + }) + } + + onConnectionSuccess() { + this.connectionState$.next("CONNECTED") + this.addEvent({ + type: "CONNECTED", + time: Date.now(), + }) + } + + onConnectionLost() { + this.connectionState$.next("DISCONNECTED") + if (this.manualDisconnect) { + this.addEvent({ + time: Date.now(), + type: "DISCONNECTED", + manual: this.manualDisconnect, + }) + } else { + this.addEvent({ + time: Date.now(), + type: "ERROR", + error: { + type: "CONNECTION_LOST", + }, + }) + } + this.manualDisconnect = false + this.subscriptionState$.next(false) + } + + onMessageArrived({ + payloadString: message, + destinationName: topic, + }: { + payloadString: string + destinationName: string + }) { + this.addEvent({ + time: Date.now(), + type: "MESSAGE_RECEIVED", + message: { + topic, + message, + }, + }) + } + + private handleError(error: unknown) { + this.disconnect() + this.addEvent({ + time: Date.now(), + type: "ERROR", + error: { + type: "CONNECTION_NOT_ESTABLISHED", + value: error, + }, + }) + } + + publish(topic: string, message: string) { + if (this.connectionState$.value === "DISCONNECTED") return + + try { + // it was publish + this.mqttClient?.send(topic, message, 0, false) + this.addEvent({ + time: Date.now(), + type: "MESSAGE_SENT", + message: { + topic, + message, + }, + }) + } catch (e) { + this.addEvent({ + time: Date.now(), + type: "ERROR", + error: { + type: "PUBLISH_ERROR", + topic, + message, + }, + }) + } + } + + subscribe(topic: string) { + try { + this.mqttClient?.subscribe(topic, { + onSuccess: this.usubSuccess.bind(this, topic), + onFailure: this.usubFailure.bind(this, topic), + }) + } catch (e) { + this.addEvent({ + time: Date.now(), + type: "ERROR", + error: { + type: "SUBSCRIPTION_FAILED", + topic, + }, + }) + } + } + + usubSuccess(topic: string) { + this.subscriptionState$.next(!this.subscriptionState$.value) + this.addEvent({ + time: Date.now(), + type: "SUBSCRIBED", + topic, + }) + } + + usubFailure(topic: string) { + this.addEvent({ + time: Date.now(), + type: "ERROR", + error: { + type: "SUBSCRIPTION_FAILED", + topic, + }, + }) + } + + unsubscribe(topic: string) { + this.mqttClient?.unsubscribe(topic, { + onSuccess: this.usubSuccess.bind(this, topic), + onFailure: this.usubFailure.bind(this, topic), + }) + } + + disconnect() { + this.manualDisconnect = true + this.mqttClient?.disconnect() + this.connectionState$.next("DISCONNECTED") + } +} diff --git a/packages/hoppscotch-app/helpers/realtime/SIOClients.ts b/packages/hoppscotch-app/helpers/realtime/SIOClients.ts new file mode 100644 index 000000000..c0a9fc20e --- /dev/null +++ b/packages/hoppscotch-app/helpers/realtime/SIOClients.ts @@ -0,0 +1,84 @@ +import wildcard from "socketio-wildcard" +import ClientV2 from "socket.io-client-v2" +import { io as ClientV4, Socket as SocketV4 } from "socket.io-client-v4" +import { io as ClientV3, Socket as SocketV3 } from "socket.io-client-v3" + +type Options = { + path: string + auth: { + token: string | undefined + } +} + +type PossibleEvent = + | "connect" + | "connect_error" + | "reconnect_error" + | "error" + | "disconnect" + | "*" + +export interface SIOClient { + connect(url: string, opts?: Options): void + on(event: PossibleEvent, cb: (data: any) => void): void + emit(event: string, data: any, cb: (data: any) => void): void + close(): void +} + +export class SIOClientV4 implements SIOClient { + private client: SocketV4 | undefined + connect(url: string, opts?: Options) { + this.client = ClientV4(url, opts) + } + + on(event: PossibleEvent, cb: (data: any) => void) { + this.client?.on(event, cb) + } + + emit(event: string, data: any, cb: (data: any) => void): void { + this.client?.emit(event, data, cb) + } + + close(): void { + this.client?.close() + } +} + +export class SIOClientV3 implements SIOClient { + private client: SocketV3 | undefined + connect(url: string, opts?: Options) { + this.client = ClientV3(url, opts) + } + + on(event: PossibleEvent, cb: (data: any) => void): void { + this.client?.on(event, cb) + } + + emit(event: string, data: any, cb: (data: any) => void): void { + this.client?.emit(event, data, cb) + } + + close(): void { + this.client?.close() + } +} + +export class SIOClientV2 implements SIOClient { + private client: any | undefined + connect(url: string, opts?: Options) { + this.client = new ClientV2(url, opts) + wildcard(ClientV2.Manager)(this.client) + } + + on(event: PossibleEvent, cb: (data: any) => void): void { + this.client?.on(event, cb) + } + + emit(event: string, data: any, cb: (data: any) => void): void { + this.client?.emit(event, data, cb) + } + + close(): void { + this.client?.close() + } +} diff --git a/packages/hoppscotch-app/helpers/realtime/SIOConnection.ts b/packages/hoppscotch-app/helpers/realtime/SIOConnection.ts new file mode 100644 index 000000000..5f215685b --- /dev/null +++ b/packages/hoppscotch-app/helpers/realtime/SIOConnection.ts @@ -0,0 +1,163 @@ +import { BehaviorSubject, Subject } from "rxjs" +import { logHoppRequestRunToAnalytics } from "../fb/analytics" +import { SIOClientV2, SIOClientV3, SIOClientV4, SIOClient } from "./SIOClients" +import { SIOClientVersion } from "~/newstore/SocketIOSession" + +export const SOCKET_CLIENTS = { + v2: SIOClientV2, + v3: SIOClientV3, + v4: SIOClientV4, +} as const + +type SIOAuth = { type: "None" } | { type: "Bearer"; token: string } + +export type ConnectionOption = { + url: string + path: string + clientVersion: SIOClientVersion + auth: SIOAuth | undefined +} + +export type SIOMessage = { + eventName: string + value: unknown +} + +type SIOErrorType = "CONNECTION" | "RECONNECT_ERROR" | "UNKNOWN" +export type SIOError = { + type: SIOErrorType + value: unknown +} + +export type SIOEvent = { time: number } & ( + | { type: "CONNECTING" } + | { type: "CONNECTED" } + | { type: "MESSAGE_SENT"; message: SIOMessage } + | { type: "MESSAGE_RECEIVED"; message: SIOMessage } + | { type: "DISCONNECTED"; manual: boolean } + | { type: "ERROR"; error: SIOError } +) + +export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED" + +export class SIOConnection { + connectionState$: BehaviorSubject + event$: Subject = new Subject() + socket: SIOClient | undefined + constructor() { + this.connectionState$ = new BehaviorSubject("DISCONNECTED") + } + + private addEvent(event: SIOEvent) { + this.event$.next(event) + } + + connect({ url, path, clientVersion, auth }: ConnectionOption) { + this.connectionState$.next("CONNECTING") + this.addEvent({ + time: Date.now(), + type: "CONNECTING", + }) + try { + this.socket = new SOCKET_CLIENTS[clientVersion]() + + if (auth?.type === "Bearer") { + this.socket.connect(url, { + path, + auth: { + token: auth.token, + }, + }) + } else { + this.socket.connect(url) + } + + this.socket.on("connect", () => { + this.connectionState$.next("CONNECTED") + this.addEvent({ + type: "CONNECTED", + time: Date.now(), + }) + }) + + this.socket.on("*", ({ data }: { data: string[] }) => { + const [eventName, message] = data + this.addEvent({ + message: { eventName, value: message }, + type: "MESSAGE_RECEIVED", + time: Date.now(), + }) + }) + + this.socket.on("connect_error", (error: unknown) => { + this.handleError(error, "CONNECTION") + }) + + this.socket.on("reconnect_error", (error: unknown) => { + this.handleError(error, "RECONNECT_ERROR") + }) + + this.socket.on("error", (error: unknown) => { + this.handleError(error, "UNKNOWN") + }) + + this.socket.on("disconnect", () => { + this.connectionState$.next("DISCONNECTED") + this.addEvent({ + type: "DISCONNECTED", + time: Date.now(), + manual: true, + }) + }) + } catch (error) { + this.handleError(error, "CONNECTION") + } + + logHoppRequestRunToAnalytics({ + platform: "socketio", + }) + } + + private handleError(error: unknown, type: SIOErrorType) { + this.disconnect() + this.addEvent({ + time: Date.now(), + type: "ERROR", + error: { + type, + value: error, + }, + }) + } + + sendMessage(event: { message: string; eventName: string }) { + if (this.connectionState$.value === "DISCONNECTED") return + const { message, eventName } = event + + this.socket?.emit(eventName, message, (data) => { + // receive response from server + this.addEvent({ + time: Date.now(), + type: "MESSAGE_RECEIVED", + message: { + eventName, + value: data, + }, + }) + }) + + this.addEvent({ + time: Date.now(), + type: "MESSAGE_SENT", + message: { + eventName, + value: message, + }, + }) + } + + disconnect() { + this.socket?.close() + this.connectionState$.next("DISCONNECTED") + } +} diff --git a/packages/hoppscotch-app/helpers/realtime/SSEConnection.ts b/packages/hoppscotch-app/helpers/realtime/SSEConnection.ts new file mode 100644 index 000000000..54acb2576 --- /dev/null +++ b/packages/hoppscotch-app/helpers/realtime/SSEConnection.ts @@ -0,0 +1,86 @@ +import { BehaviorSubject, Subject } from "rxjs" +import { logHoppRequestRunToAnalytics } from "../fb/analytics" + +export type SSEEvent = { time: number } & ( + | { type: "STARTING" } + | { type: "STARTED" } + | { type: "MESSAGE_RECEIVED"; message: string } + | { type: "STOPPED"; manual: boolean } + | { type: "ERROR"; error: Event | null } +) + +export type ConnectionState = "STARTING" | "STARTED" | "STOPPED" + +export class SSEConnection { + connectionState$: BehaviorSubject + event$: Subject = new Subject() + sse: EventSource | undefined + constructor() { + this.connectionState$ = new BehaviorSubject("STOPPED") + } + + private addEvent(event: SSEEvent) { + this.event$.next(event) + } + + start(url: string, eventType: string) { + this.connectionState$.next("STARTING") + this.addEvent({ + time: Date.now(), + type: "STARTING", + }) + if (typeof EventSource !== "undefined") { + try { + this.sse = new EventSource(url) + this.sse.onopen = () => { + this.connectionState$.next("STARTED") + this.addEvent({ + type: "STARTED", + time: Date.now(), + }) + } + this.sse.onerror = this.handleError + this.sse.addEventListener(eventType, ({ data }) => { + this.addEvent({ + type: "MESSAGE_RECEIVED", + message: data, + time: Date.now(), + }) + }) + } catch (error) { + // A generic event type returned if anything goes wrong or browser doesn't support SSE + // https://developer.mozilla.org/en-US/docs/Web/API/EventSource/error_event#event_type + this.handleError(error as Event) + } + } else { + this.addEvent({ + type: "ERROR", + time: Date.now(), + error: null, + }) + } + + logHoppRequestRunToAnalytics({ + platform: "sse", + }) + } + + private handleError(error: Event) { + this.stop() + this.addEvent({ + time: Date.now(), + type: "ERROR", + error, + }) + } + + stop() { + this.sse?.close() + this.connectionState$.next("STOPPED") + this.addEvent({ + type: "STOPPED", + time: Date.now(), + manual: true, + }) + } +} diff --git a/packages/hoppscotch-app/helpers/realtime/WSConnection.ts b/packages/hoppscotch-app/helpers/realtime/WSConnection.ts new file mode 100644 index 000000000..f0c4a1616 --- /dev/null +++ b/packages/hoppscotch-app/helpers/realtime/WSConnection.ts @@ -0,0 +1,102 @@ +import { BehaviorSubject, Subject } from "rxjs" +import { logHoppRequestRunToAnalytics } from "../fb/analytics" + +export type WSErrorMessage = SyntaxError | Event + +export type WSEvent = { time: number } & ( + | { type: "CONNECTING" } + | { type: "CONNECTED" } + | { type: "MESSAGE_SENT"; message: string } + | { type: "MESSAGE_RECEIVED"; message: string } + | { type: "DISCONNECTED"; manual: boolean } + | { type: "ERROR"; error: WSErrorMessage } +) + +export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED" + +export class WSConnection { + connectionState$: BehaviorSubject + event$: Subject = new Subject() + socket: WebSocket | undefined + + constructor() { + this.connectionState$ = new BehaviorSubject("DISCONNECTED") + } + + private addEvent(event: WSEvent) { + this.event$.next(event) + } + + connect(url: string, protocols: string[]) { + try { + this.connectionState$.next("CONNECTING") + this.socket = new WebSocket(url, protocols) + + this.addEvent({ + time: Date.now(), + type: "CONNECTING", + }) + + this.socket.onopen = () => { + this.connectionState$.next("CONNECTED") + this.addEvent({ + type: "CONNECTED", + time: Date.now(), + }) + } + + this.socket.onerror = (error) => { + this.handleError(error) + } + + this.socket.onclose = () => { + this.connectionState$.next("DISCONNECTED") + this.addEvent({ + type: "DISCONNECTED", + time: Date.now(), + manual: true, + }) + } + + this.socket.onmessage = ({ data }) => { + this.addEvent({ + time: Date.now(), + type: "MESSAGE_RECEIVED", + message: data, + }) + } + } catch (error) { + // We will have SyntaxError if anything goes wrong with WebSocket constructor + // See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#exceptions + this.handleError(error as SyntaxError) + } + + logHoppRequestRunToAnalytics({ + platform: "wss", + }) + } + + private handleError(error: WSErrorMessage) { + this.disconnect() + this.addEvent({ + time: Date.now(), + type: "ERROR", + error, + }) + } + + sendMessage(event: { message: string; eventName: string }) { + if (this.connectionState$.value === "DISCONNECTED") return + const { message } = event + this.socket?.send(message) + this.addEvent({ + time: Date.now(), + type: "MESSAGE_SENT", + message, + }) + } + + disconnect() { + this.socket?.close() + } +} diff --git a/packages/hoppscotch-app/helpers/shortcodes/Shortcode.ts b/packages/hoppscotch-app/helpers/shortcodes/Shortcode.ts new file mode 100644 index 000000000..34039a719 --- /dev/null +++ b/packages/hoppscotch-app/helpers/shortcodes/Shortcode.ts @@ -0,0 +1,8 @@ +/** + * Defines how a Shortcode is represented in the ShortcodeListAdapter + */ +export interface Shortcode { + id: string + request: string + createdOn: Date +} diff --git a/packages/hoppscotch-app/helpers/shortcodes/ShortcodeListAdapter.ts b/packages/hoppscotch-app/helpers/shortcodes/ShortcodeListAdapter.ts new file mode 100644 index 000000000..fbd15e30d --- /dev/null +++ b/packages/hoppscotch-app/helpers/shortcodes/ShortcodeListAdapter.ts @@ -0,0 +1,149 @@ +import * as E from "fp-ts/Either" +import { BehaviorSubject, Subscription } from "rxjs" +import { GQLError, runGQLQuery, runGQLSubscription } from "../backend/GQLClient" +import { + GetUserShortcodesQuery, + GetUserShortcodesDocument, + ShortcodeCreatedDocument, + ShortcodeDeletedDocument, +} from "../backend/graphql" +import { BACKEND_PAGE_SIZE } from "../backend/helpers" +import { Shortcode } from "./Shortcode" + +export default class ShortcodeListAdapter { + error$: BehaviorSubject | null> + loading$: BehaviorSubject + shortcodes$: BehaviorSubject + hasMoreShortcodes$: BehaviorSubject + + private timeoutHandle: ReturnType | null + private isDispose: boolean + + private myShortcodesCreated: Subscription | null + private myShortcodesRevoked: Subscription | null + + constructor(deferInit: boolean = false) { + this.error$ = new BehaviorSubject | null>(null) + this.loading$ = new BehaviorSubject(false) + this.shortcodes$ = new BehaviorSubject< + GetUserShortcodesQuery["myShortcodes"] + >([]) + this.hasMoreShortcodes$ = new BehaviorSubject(true) + this.timeoutHandle = null + this.isDispose = false + this.myShortcodesCreated = null + this.myShortcodesRevoked = null + + if (!deferInit) this.initialize() + } + + unsubscribeSubscriptions() { + this.myShortcodesCreated?.unsubscribe() + this.myShortcodesRevoked?.unsubscribe() + } + + initialize() { + if (this.timeoutHandle) throw new Error(`Adapter already initialized`) + if (this.isDispose) throw new Error(`Adapter has been disposed`) + + this.fetchList() + this.registerSubscriptions() + } + + public dispose() { + if (!this.timeoutHandle) throw new Error(`Adapter has not been initialized`) + if (!this.isDispose) throw new Error(`Adapter has been disposed`) + + this.isDispose = true + clearTimeout(this.timeoutHandle) + this.timeoutHandle = null + this.unsubscribeSubscriptions() + } + + fetchList() { + this.loadMore(true) + } + + async loadMore(forcedAttempt = false) { + if (!this.hasMoreShortcodes$.value && !forcedAttempt) return + + this.loading$.next(true) + + const lastCodeID = + this.shortcodes$.value.length > 0 + ? this.shortcodes$.value[this.shortcodes$.value.length - 1].id + : undefined + + const result = await runGQLQuery({ + query: GetUserShortcodesDocument, + variables: { + cursor: lastCodeID, + }, + }) + if (E.isLeft(result)) { + this.error$.next(result.left) + console.error(result.left) + this.loading$.next(false) + + throw new Error(`Failed fetching short codes list: ${result.left}`) + } + + const fetchedResult = result.right.myShortcodes + + this.pushNewShortcodes(fetchedResult) + + if (fetchedResult.length !== BACKEND_PAGE_SIZE) { + this.hasMoreShortcodes$.next(false) + } + + this.loading$.next(false) + } + + private pushNewShortcodes(results: Shortcode[]) { + const userShortcodes = this.shortcodes$.value + + userShortcodes.push(...results) + + this.shortcodes$.next(userShortcodes) + } + + private createShortcode(shortcode: Shortcode) { + const userShortcodes = this.shortcodes$.value + + userShortcodes.unshift(shortcode) + + this.shortcodes$.next(userShortcodes) + } + + private deleteShortcode(codeId: string) { + const newShortcodes = this.shortcodes$.value.filter( + ({ id }) => id !== codeId + ) + + this.shortcodes$.next(newShortcodes) + } + + private registerSubscriptions() { + this.myShortcodesCreated = runGQLSubscription({ + query: ShortcodeCreatedDocument, + }).subscribe((result) => { + if (E.isLeft(result)) { + console.error(result.left) + throw new Error(`Shortcode Create Error ${result.left}`) + } + + this.createShortcode(result.right.myShortcodesCreated) + }) + + this.myShortcodesRevoked = runGQLSubscription({ + query: ShortcodeDeletedDocument, + }).subscribe((result) => { + if (E.isLeft(result)) { + console.error(result.left) + throw new Error(`Shortcode Delete Error ${result.left}`) + } + + this.deleteShortcode(result.right.myShortcodesRevoked.id) + }) + } +} diff --git a/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts b/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts index 013155f04..254259f12 100644 --- a/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts +++ b/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts @@ -1,4 +1,5 @@ import * as TE from "fp-ts/TaskEither" +import * as O from "fp-ts/Option" import { pipe } from "fp-ts/function" import { AxiosRequestConfig } from "axios" import cloneDeep from "lodash/cloneDeep" @@ -15,12 +16,42 @@ export const hasFirefoxExtensionInstalled = () => hasExtensionInstalled() && browserIsFirefox() export const cancelRunningExtensionRequest = () => { - if ( - hasExtensionInstalled() && - window.__POSTWOMAN_EXTENSION_HOOK__.cancelRunningRequest - ) { - window.__POSTWOMAN_EXTENSION_HOOK__.cancelRunningRequest() + window.__POSTWOMAN_EXTENSION_HOOK__?.cancelRunningRequest() +} + +export const defineSubscribableObject = (obj: T) => { + const proxyObject = { + ...obj, + _subscribers: {} as { + // eslint-disable-next-line no-unused-vars + [key in keyof T]?: ((...args: any[]) => any)[] + }, + subscribe(prop: keyof T, func: (...args: any[]) => any): void { + if (Array.isArray(this._subscribers[prop])) { + this._subscribers[prop]?.push(func) + } else { + this._subscribers[prop] = [func] + } + }, } + + type SubscribableProxyObject = typeof proxyObject + + return new Proxy(proxyObject, { + set(obj, prop, newVal) { + obj[prop as keyof SubscribableProxyObject] = newVal + + const currentSubscribers = obj._subscribers[prop as keyof T] + + if (Array.isArray(currentSubscribers)) { + for (const subscriber of currentSubscribers) { + subscriber(newVal) + } + } + + return true + }, + }) } const preProcessRequest = (req: AxiosRequestConfig): AxiosRequestConfig => { @@ -56,13 +87,20 @@ const extensionStrategy: NetworkStrategy = (req) => // Run the request TE.bind("response", ({ processedReq }) => - TE.tryCatch( - () => - window.__POSTWOMAN_EXTENSION_HOOK__.sendRequest({ - ...processedReq, - wantsBinary: true, - }) as Promise, - (err) => err as any + pipe( + window.__POSTWOMAN_EXTENSION_HOOK__, + O.fromNullable, + TE.fromOption(() => "NO_PW_EXT_HOOK" as const), + TE.chain((extensionHook) => + TE.tryCatch( + () => + extensionHook.sendRequest({ + ...processedReq, + wantsBinary: true, + }), + (err) => err as any + ) + ) ) ), diff --git a/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js b/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js index b44a6f9af..53665d492 100644 --- a/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js +++ b/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js @@ -122,13 +122,6 @@ describe("cancelRunningExtensionRequest", () => { cancelRunningExtensionRequest() expect(cancelFunc).not.toHaveBeenCalled() }) - - test("does not cancel request if extension installed but function not present", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = {} - - cancelRunningExtensionRequest() - expect(cancelFunc).not.toHaveBeenCalled() - }) }) describe("extensionStrategy", () => { diff --git a/packages/hoppscotch-app/helpers/types/HoppRealtimeLog.ts b/packages/hoppscotch-app/helpers/types/HoppRealtimeLog.ts index 4132a487d..9756fce72 100644 --- a/packages/hoppscotch-app/helpers/types/HoppRealtimeLog.ts +++ b/packages/hoppscotch-app/helpers/types/HoppRealtimeLog.ts @@ -1,8 +1,9 @@ export type HoppRealtimeLogLine = { + prefix?: string payload: string source: string color?: string - ts: string + ts: number | undefined } export type HoppRealtimeLog = HoppRealtimeLogLine[] diff --git a/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts b/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts index bcecc176c..fae74280e 100644 --- a/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts +++ b/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts @@ -11,6 +11,8 @@ import { parseBodyEnvVariables, parseRawKeyValueEntries, Environment, + HoppRESTHeader, + HoppRESTParam, } from "@hoppscotch/data" import { arrayFlatMap, arraySort } from "../functional/array" import { toFormData } from "../functional/formData" @@ -29,6 +31,146 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest { effectiveFinalBody: FormData | string | null } +/** + * Get headers that can be generated by authorization config of the request + * @param req Request to check + * @param envVars Currently active environment variables + * @returns The list of headers + */ +const getComputedAuthHeaders = ( + req: HoppRESTRequest, + envVars: Environment["variables"] +) => { + // If Authorization header is also being user-defined, that takes priority + if (req.headers.find((h) => h.key.toLowerCase() === "authorization")) + return [] + + if (!req.auth.authActive) return [] + + const headers: HoppRESTHeader[] = [] + + // TODO: Support a better b64 implementation than btoa ? + if (req.auth.authType === "basic") { + const username = parseTemplateString(req.auth.username, envVars) + const password = parseTemplateString(req.auth.password, envVars) + + headers.push({ + active: true, + key: "Authorization", + value: `Basic ${btoa(`${username}:${password}`)}`, + }) + } else if ( + req.auth.authType === "bearer" || + req.auth.authType === "oauth-2" + ) { + headers.push({ + active: true, + key: "Authorization", + value: `Bearer ${parseTemplateString(req.auth.token, envVars)}`, + }) + } else if (req.auth.authType === "api-key") { + const { key, value, addTo } = req.auth + + if (addTo === "Headers") { + headers.push({ + active: true, + key: parseTemplateString(key, envVars), + value: parseTemplateString(value, envVars), + }) + } + } + + return headers +} + +/** + * Get headers that can be generated by body config of the request + * @param req Request to check + * @returns The list of headers + */ +export const getComputedBodyHeaders = ( + req: HoppRESTRequest +): HoppRESTHeader[] => { + // If a content-type is already defined, that will override this + if ( + req.headers.find( + (req) => req.active && req.key.toLowerCase() === "content-type" + ) + ) + return [] + + // Body should have a non-null content-type + if (req.body.contentType === null) return [] + + return [ + { + active: true, + key: "content-type", + value: req.body.contentType, + }, + ] +} + +export type ComputedHeader = { + source: "auth" | "body" + header: HoppRESTHeader +} + +/** + * Returns a list of headers that will be added during execution of the request + * For e.g, Authorization headers maybe added if an Auth Mode is defined on REST + * @param req The request to check + * @param envVars The environment variables active + * @returns The headers that are generated along with the source of that header + */ +export const getComputedHeaders = ( + req: HoppRESTRequest, + envVars: Environment["variables"] +): ComputedHeader[] => [ + ...getComputedAuthHeaders(req, envVars).map((header) => ({ + source: "auth" as const, + header, + })), + ...getComputedBodyHeaders(req).map((header) => ({ + source: "body" as const, + header, + })), +] + +export type ComputedParam = { + source: "auth" + param: HoppRESTParam +} + +/** + * Returns a list of params that will be added during execution of the request + * For e.g, Authorization params (like API-key) maybe added if an Auth Mode is defined on REST + * @param req The request to check + * @param envVars The environment variables active + * @returns The params that are generated along with the source of that header + */ +export const getComputedParams = ( + req: HoppRESTRequest, + envVars: Environment["variables"] +): ComputedParam[] => { + // When this gets complex, its best to split this function off (like with getComputedHeaders) + // API-key auth can be added to query params + if (!req.auth.authActive) return [] + if (req.auth.authType !== "api-key") return [] + if (req.auth.addTo !== "Query params") return [] + + return [ + { + source: "auth", + param: { + active: true, + key: parseTemplateString(req.auth.key, envVars), + value: parseTemplateString(req.auth.value, envVars), + }, + }, + ] +} + // Resolves environment variables in the body export const resolvesEnvsInBody = ( body: HoppRESTReqBody, @@ -135,83 +277,29 @@ export function getEffectiveRESTRequest( ): EffectiveHoppRESTRequest { const envVariables = [...environment.variables, ...getGlobalVariables()] - const effectiveFinalHeaders = request.headers - .filter( - (x) => - x.key !== "" && // Remove empty keys - x.active // Only active - ) - .map((x) => ({ - // Parse out environment template strings + const effectiveFinalHeaders = pipe( + getComputedHeaders(request, envVariables).map((h) => h.header), + A.concat(request.headers), + A.filter((x) => x.active && x.key !== ""), + A.map((x) => ({ active: true, key: parseTemplateString(x.key, envVariables), value: parseTemplateString(x.value, envVariables), })) + ) - const effectiveFinalParams = request.params - .filter( - (x) => - x.key !== "" && // Remove empty keys - x.active // Only active - ) - .map((x) => ({ + const effectiveFinalParams = pipe( + getComputedParams(request, envVariables).map((p) => p.param), + A.concat(request.params), + A.filter((x) => x.active && x.key !== ""), + A.map((x) => ({ active: true, key: parseTemplateString(x.key, envVariables), value: parseTemplateString(x.value, envVariables), })) - - // Authentication - if (request.auth.authActive) { - // TODO: Support a better b64 implementation than btoa ? - if (request.auth.authType === "basic") { - const username = parseTemplateString(request.auth.username, envVariables) - const password = parseTemplateString(request.auth.password, envVariables) - - effectiveFinalHeaders.push({ - active: true, - key: "Authorization", - value: `Basic ${btoa(`${username}:${password}`)}`, - }) - } else if ( - request.auth.authType === "bearer" || - request.auth.authType === "oauth-2" - ) { - effectiveFinalHeaders.push({ - active: true, - key: "Authorization", - value: `Bearer ${parseTemplateString( - request.auth.token, - envVariables - )}`, - }) - } else if (request.auth.authType === "api-key") { - const { key, value, addTo } = request.auth - if (addTo === "Headers") { - effectiveFinalHeaders.push({ - active: true, - key: parseTemplateString(key, envVariables), - value: parseTemplateString(value, envVariables), - }) - } else if (addTo === "Query params") { - effectiveFinalParams.push({ - active: true, - key: parseTemplateString(key, envVariables), - value: parseTemplateString(value, envVariables), - }) - } - } - } + ) const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables) - const contentTypeInHeader = effectiveFinalHeaders.find( - (x) => x.key.toLowerCase() === "content-type" - ) - if (request.body.contentType && !contentTypeInHeader?.value) - effectiveFinalHeaders.push({ - active: true, - key: "content-type", - value: request.body.contentType, - }) return { ...request, diff --git a/packages/hoppscotch-app/helpers/utils/contenttypes.ts b/packages/hoppscotch-app/helpers/utils/contenttypes.ts index 77656888b..822b07a8a 100644 --- a/packages/hoppscotch-app/helpers/utils/contenttypes.ts +++ b/packages/hoppscotch-app/helpers/utils/contenttypes.ts @@ -14,6 +14,37 @@ export const knownContentTypes: Record = { "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) } diff --git a/packages/hoppscotch-app/helpers/utils/string.ts b/packages/hoppscotch-app/helpers/utils/string.ts deleted file mode 100644 index f23107591..000000000 --- a/packages/hoppscotch-app/helpers/utils/string.ts +++ /dev/null @@ -1,12 +0,0 @@ -const sourceEmojis = { - // Source used for info messages. - info: "\tℹ️ [INFO]:\t", - // Source used for client to server messages. - client: "\t⬅️ [SENT]:\t", - // Source used for server to client messages. - server: "\t➡️ [RECEIVED]:\t", -} - -export function getSourcePrefix(source: keyof typeof sourceEmojis) { - return sourceEmojis[source] -} diff --git a/packages/hoppscotch-app/layouts/default.vue b/packages/hoppscotch-app/layouts/default.vue index cf40259f3..971673e00 100644 --- a/packages/hoppscotch-app/layouts/default.vue +++ b/packages/hoppscotch-app/layouts/default.vue @@ -64,6 +64,8 @@ import { useRouter, watch, ref, + onMounted, + onBeforeUnmount, } from "@nuxtjs/composition-api" import { Splitpanes, Pane } from "splitpanes" import "splitpanes/dist/splitpanes.css" @@ -77,6 +79,12 @@ import { hookKeybindingsListener } from "~/helpers/keybindings" import { defineActionHandler } from "~/helpers/actions" import { useSentry } from "~/helpers/sentry" import { useColorMode } from "~/helpers/utils/composables" +import { + changeExtensionStatus, + ExtensionStatus, +} from "~/newstore/HoppExtension" + +import { defineSubscribableObject } from "~/helpers/strategies/ExtensionStrategy" function appLayout() { const rightSidebar = useSetting("SIDEBAR") @@ -202,6 +210,62 @@ function defineJumpActions() { }) } +function setupExtensionHooks() { + const extensionPollIntervalId = ref>() + + onMounted(() => { + if (window.__HOPP_EXTENSION_STATUS_PROXY__) { + changeExtensionStatus(window.__HOPP_EXTENSION_STATUS_PROXY__.status) + + window.__HOPP_EXTENSION_STATUS_PROXY__.subscribe( + "status", + (status: ExtensionStatus) => changeExtensionStatus(status) + ) + } else { + const statusProxy = defineSubscribableObject({ + status: "waiting" as ExtensionStatus, + }) + + window.__HOPP_EXTENSION_STATUS_PROXY__ = statusProxy + statusProxy.subscribe("status", (status: ExtensionStatus) => + changeExtensionStatus(status) + ) + + /** + * Keeping identifying extension backward compatible + * We are assuming the default version is 0.24 or later. So if the extension exists, its identified immediately, + * then we use a poll to find the version, this will get the version for 0.24 and any other version + * of the extension, but will have a slight lag. + * 0.24 users will get the benefits of 0.24, while the extension won't break for the old users + */ + extensionPollIntervalId.value = setInterval(() => { + if (typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined") { + if (extensionPollIntervalId.value) + clearInterval(extensionPollIntervalId.value) + + const version = window.__POSTWOMAN_EXTENSION_HOOK__.getVersion() + + // When the version is not 0.24 or higher, the extension wont do this. so we have to do it manually + if ( + version.major === 0 && + version.minor <= 23 && + window.__HOPP_EXTENSION_STATUS_PROXY__ + ) { + window.__HOPP_EXTENSION_STATUS_PROXY__.status = "available" + } + } + }, 2000) + } + }) + + // Cleanup timer + onBeforeUnmount(() => { + if (extensionPollIntervalId.value) { + clearInterval(extensionPollIntervalId.value) + } + }) +} + export default defineComponent({ components: { Splitpanes, Pane }, setup() { @@ -229,6 +293,8 @@ export default defineComponent({ showSupport.value = !showSupport.value }) + setupExtensionHooks() + return { mdAndLarger, spacerClass, diff --git a/packages/hoppscotch-app/locales/cn.json b/packages/hoppscotch-app/locales/cn.json index c372b01ab..6be18aa0f 100644 --- a/packages/hoppscotch-app/locales/cn.json +++ b/packages/hoppscotch-app/locales/cn.json @@ -1,7 +1,7 @@ { "action": { "cancel": "取消", - "choose_file": "选择一个文件", + "choose_file": "选择文件", "clear": "清除", "clear_all": "全部清除", "connect": "连接", @@ -9,18 +9,18 @@ "delete": "删除", "disconnect": "断开连接", "dismiss": "忽略", - "dont_save": "Don't save", + "dont_save": "不保存", "download_file": "下载文件", "duplicate": "复制", "edit": "编辑", "go_back": "返回", "label": "标签", "learn_more": "了解更多", - "less": "Less", + "less": "更少", "more": "更多", "new": "新增", "no": "否", - "paste": "Paste", + "paste": "粘贴", "prettify": "美化", "remove": "移除", "restore": "恢复", @@ -45,9 +45,9 @@ "chat_with_us": "与我们交谈", "contact_us": "联系我们", "copy": "复制", - "copy_user_id": "Copy User Auth Token", - "developer_option": "Developer options", - "developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.", + "copy_user_id": "复制认证 Token", + "developer_option": "开发者选项", + "developer_option_description": "开发者工具,有助于开发和维护 Hoppscotch。", "discord": "Discord", "documentation": "帮助文档", "github": "GitHub", @@ -60,7 +60,7 @@ "keyboard_shortcuts": "键盘快捷键", "name": "Hoppscotch", "new_version_found": "已发现新版本。刷新页面以更新。", - "options": "Options", + "options": "选项", "proxy_privacy_policy": "代理隐私政策", "reload": "重新加载", "search": "搜索", @@ -68,7 +68,7 @@ "shortcuts": "快捷方式", "spotlight": "聚光灯", "status": "状态", - "status_description": "Check the status of the website", + "status_description": "检查网站状态", "terms_and_privacy": "隐私条款", "twitter": "Twitter", "type_a_command_search": "输入命令或搜索内容……", @@ -82,7 +82,7 @@ "continue_with_email": "使用电子邮箱登录", "continue_with_github": "使用 GitHub 登录", "continue_with_google": "使用 Google 登录", - "continue_with_microsoft": "Continue with Microsoft", + "continue_with_microsoft": "使用 Microsoft 登录", "email": "电子邮箱地址", "logged_out": "登出", "login": "登录", @@ -106,32 +106,32 @@ "username": "用户名" }, "collection": { - "created": "组合已创建", - "edit": "编辑组合", - "invalid_name": "请提供有效的组合名称", - "my_collections": "我的组合", - "name": "我的新组合", - "name_length_insufficient": "Collection name should be at least 3 characters long", - "new": "新建组合", - "renamed": "组合已更名", - "request_in_use": "Request in use", + "created": "集合已创建", + "edit": "编辑集合", + "invalid_name": "请提供有效的集合名称", + "my_collections": "我的集合", + "name": "我的新集合", + "name_length_insufficient": "集合名字至少需要 3 个字符", + "new": "新建集合", + "renamed": "集合已更名", + "request_in_use": "请求正在使用中", "save_as": "另存为", - "select": "选择一个组合", + "select": "选择一个集合", "select_location": "选择位置", "select_team": "选择一个团队", - "team_collections": "团队组合" + "team_collections": "团队集合" }, "confirm": { "exit_team": "你确定要离开此团队吗?", "logout": "你确定要登出吗?", - "remove_collection": "你确定要永久删除该组合吗?", + "remove_collection": "你确定要永久删除该集合吗?", "remove_environment": "你确定要永久删除该环境吗?", "remove_folder": "你确定要永久删除该文件夹吗?", "remove_history": "你确定要永久删除全部历史记录吗?", "remove_request": "你确定要永久删除该请求吗?", "remove_team": "你确定要删除该团队吗?", "remove_telemetry": "你确定要退出遥测服务吗?", - "request_change": "Are you sure you want to discard current request, unsaved changes will be lost.", + "request_change": "你确定你要放弃当前的请求,未保存的修改将被丢失。", "sync": "您确定要同步该工作区吗?" }, "count": { @@ -144,13 +144,13 @@ }, "documentation": { "generate": "生成文档", - "generate_message": "导入 Hoppscotch 组合以随时随地生成 API 文档。" + "generate_message": "导入 Hoppscotch 集合以随时随地生成 API 文档。" }, "empty": { "authorization": "该请求没有使用任何授权", "body": "该请求没有任何请求体", - "collection": "组合为空", - "collections": "组合为空", + "collection": "集合为空", + "collections": "集合为空", "documentation": "连接至 GraphQL 端点以查看文档", "endpoint": "端点不能为空", "environments": "环境为空", @@ -169,20 +169,20 @@ "tests": "没有针对该请求的测试" }, "environment": { - "add_to_global": "Add to Global", - "added": "Environment addition", - "create_new": "已创建新环境", - "created": "Environment created", - "deleted": "Environment deletion", + "add_to_global": "添加到全局环境", + "added": "环境已添加", + "create_new": "创建新环境", + "created": "环境已创建", + "deleted": "环境已删除", "edit": "编辑环境", "invalid_name": "请提供有效的环境名称", - "nested_overflow": "nested environment variables are limited to 10 levels", + "nested_overflow": "环境嵌套深度超过限制(10层)", "new": "新建环境", "no_environment": "无环境", - "no_environment_description": "No environments were selected. Choose what to do with the following variables.", + "no_environment_description": "没有选择环境。选择如何处理以下变量。", "select": "选择环境", "title": "环境", - "updated": "Environment updation", + "updated": "环境已更新", "variable_list": "变量列表" }, "error": { @@ -190,9 +190,9 @@ "check_console_details": "检查控制台日志以获悉详情", "curl_invalid_format": "cURL 格式不正确", "empty_req_name": "空请求名称", - "f12_details": "(F12 详情)", + "f12_details": "(F12 详情)", "gql_prettify_invalid_query": "无法美化无效的查询,处理查询语法错误并重试", - "incomplete_config_urls": "Incomplete configuration URLs", + "incomplete_config_urls": "配置文件中的 URL 无效", "incorrect_email": "电子邮箱错误", "invalid_link": "无效链接", "invalid_link_description": "你点击的链接无效或已过期。", @@ -202,7 +202,7 @@ "no_duration": "无持续时间", "script_fail": "无法执行预请求脚本", "something_went_wrong": "发生了一些错误", - "test_script_fail": "Could not execute post-request script" + "test_script_fail": "无法执行请求脚本" }, "export": { "as_json": "导出为 JSON", @@ -215,7 +215,7 @@ "created": "已创建文件夹", "edit": "编辑文件夹", "invalid_name": "请提供文件夹的名称", - "name_length_insufficient": "Folder name should be at least 3 characters long", + "name_length_insufficient": "文件夹名称应至少为 3 个字符", "new": "新文件夹", "renamed": "文件夹已更名" }, @@ -238,46 +238,46 @@ "post_request_tests": "测试脚本使用 JavaScript 编写,并在收到响应后执行。", "pre_request_script": "预请求脚本使用 JavaScript 编写,并在请求发送前执行。", "script_fail": "预请求脚本中似乎存在故障。 检查下面的错误并相应地修复脚本。", - "test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again", + "test_script_fail": "测试脚本似乎有一个错误。请修复错误并再次运行测试", "tests": "编写测试脚本以自动调试。" }, "hide": { - "collection": "Collapse Collection Panel", + "collection": "隐藏集合", "more": "隐藏更多", "preview": "隐藏预览", "sidebar": "隐藏侧边栏" }, "import": { - "collections": "导入组合", + "collections": "导入集合", "curl": "导入 cURL", "failed": "导入失败", "from_gist": "从 Gist 导入", - "from_gist_description": "Import from Gist URL", - "from_insomnia": "Import from Insomnia", - "from_insomnia_description": "Import from Insomnia collection", - "from_json": "Import from Hoppscotch", - "from_json_description": "Import from Hoppscotch collection file", - "from_my_collections": "从我的组合导入", - "from_my_collections_description": "Import from My Collections file", - "from_openapi": "Import from OpenAPI", - "from_openapi_description": "Import from OpenAPI specification file (YML/JSON)", - "from_postman": "Import from Postman", - "from_postman_description": "Import from Postman collection", - "from_url": "Import from URL", + "from_gist_description": "从 Gist URL 导入", + "from_insomnia": "从 Insomnia 导入", + "from_insomnia_description": "从 Insomnia 集合中导入", + "from_json": "从 Hoppscotch 导入", + "from_json_description": "从 Hoppscotch 集合中导入", + "from_my_collections": "从我的集合导入", + "from_my_collections_description": "从我的集合文件导入", + "from_openapi": "从 OpenAPI 导入", + "from_openapi_description": "从 OpenAPI 文件导入(YML/JSON)", + "from_postman": "从 Postman 导入", + "from_postman_description": "从 Postman 集合中导入", + "from_url": "从 URL 导入", "gist_url": "输入 Gist URL", - "json_description": "Import collections from a Hoppscotch Collections JSON file", + "json_description": "从 Hoppscotch 的集合文件导入(JSON)", "title": "导入" }, "layout": { - "collapse_collection": "Collapse or Expand Collections", - "collapse_sidebar": "Collapse or Expand the sidebar", + "collapse_collection": "折叠/展开集合", + "collapse_sidebar": "折叠/展开边栏", "column": "垂直布局", - "name": "Layout", + "name": "布局", "row": "水平布局", - "zen_mode": "禅意模式" + "zen_mode": "ZEN 模式" }, "modal": { - "collections": "组合", + "collections": "集合", "confirm": "确认", "edit_request": "编辑请求", "import_export": "导入/导出" @@ -315,12 +315,12 @@ "email_verification_mail": "确认邮件已发送至你的邮箱,请点击链接以验证你的电子邮箱。", "no_permission": "你无权执行此操作。", "owner": "所有者", - "owner_description": "所有者可以添加、编辑和删除请求、组合及团队成员。", + "owner_description": "所有者可以添加、编辑和删除请求、集合及团队成员。", "roles": "角色", - "roles_description": "角色用以控制共享组合的访问权限。", + "roles_description": "角色用以控制共享集合的访问权限。", "updated": "档案已更新", - "viewer": "阅览者", - "viewer_description": "阅览者只可查看与使用请求。" + "viewer": "查看者", + "viewer_description": "查看者只可查看与使用请求。" }, "remove": { "star": "移除星标" @@ -340,10 +340,10 @@ "invalid_name": "请提供请求名称", "method": "方法", "name": "请求名称", - "new": "New Request", - "override": "Override", - "override_help": "Set Content-Type in Headers", - "overriden": "Overridden", + "new": "新请求", + "override": "覆盖", + "override_help": "设置 Content-Type 头", + "overriden": "覆盖", "parameter_list": "查询参数", "parameters": "参数", "path": "路径", @@ -356,7 +356,7 @@ "save_as": "另存为", "saved": "请求已保存", "share": "分享", - "share_description": "Share Hoppscotch with your friends", + "share_description": "分享 Hoppscotch 给你的朋友", "title": "请求", "type": "请求类型", "url": "URL", @@ -396,7 +396,7 @@ "extension_version": "扩展版本", "extensions": "扩展", "extensions_use_toggle": "使用浏览器扩展发送请求(如果存在)", - "follow": "Follow Us", + "follow": "关注我们", "font_size": "字体大小", "font_size_large": "大", "font_size_medium": "中", @@ -417,7 +417,7 @@ "reset_default": "重置为默认", "sidebar_on_left": "侧边栏移至左侧", "sync": "同步", - "sync_collections": "组合", + "sync_collections": "集合", "sync_description": "这些设置会同步到云。", "sync_environments": "环境", "sync_history": "历史", @@ -464,21 +464,21 @@ "previous_method": "选择上一个方法", "put_method": "选择 PUT 方法", "reset_request": "重置请求", - "save_to_collections": "保存到组合", + "save_to_collections": "保存到集合", "send_request": "发送请求", "title": "请求" }, "theme": { - "black": "Switch theme to black mode", - "dark": "Switch theme to dark mode", - "light": "Switch theme to light mode", - "system": "Switch theme to system mode", - "title": "Theme" + "black": "切换为黑色主题", + "dark": "切换为深色主题", + "light": "切换为浅色主题", + "system": "切换为系统主题", + "title": "主题" } }, "show": { "code": "显示代码", - "collection": "Expand Collection Panel", + "collection": "展开集合", "more": "显示更多", "sidebar": "显示侧边栏" }, @@ -525,7 +525,7 @@ "community": "提问与互助", "documentation": "阅读更多有关 Hoppscotch 的内容", "forum": "答疑解惑", - "github": "Follow us on Github", + "github": "在 Github 关注我们", "shortcuts": "更快浏览应用", "team": "与团队保持联系", "title": "支持", @@ -534,7 +534,7 @@ "tab": { "authorization": "授权", "body": "请求体", - "collections": "组合", + "collections": "集合", "documentation": "帮助文档", "headers": "请求头", "history": "历史记录", @@ -552,18 +552,18 @@ "websocket": "WebSocket" }, "team": { - "already_member": "你已经是此团队的成员。请联系你的团队所有人。", + "already_member": "你已经是此团队的成员。请联系你的团队者。", "create_new": "创建新团队", "deleted": "团队已删除", "edit": "编辑团队", "email": "电子邮箱", - "email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队所有人。", + "email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队者。", "exit": "退出团队", "exit_disabled": "团队所有者无法退出团队", "invalid_email_format": "电子邮箱格式无效", - "invalid_id": "无效的团队 ID,请联系你的团队所有人。", + "invalid_id": "无效的团队 ID,请联系你的团队者。", "invalid_invite_link": "无效的邀请链接", - "invalid_invite_link_description": "你点击的链接无效。请联系你的团队所有人。", + "invalid_invite_link_description": "你点击的链接无效。请联系你的团队者。", "invalid_member_permission": "请为团队成员提供有效的权限", "invite": "邀请", "invite_more": "邀请更多成员", @@ -578,8 +578,8 @@ "login_to_continue": "登录以继续", "login_to_continue_description": "你需要登录以加入团队", "logout_and_try_again": "登出并以其他帐户登录", - "member_has_invite": "此邮箱 ID 已有邀请。请联系你的团队所有人。", - "member_not_found": "未找到成员。请联系你的团队所有人。", + "member_has_invite": "此邮箱 ID 已有邀请。请联系你的团队者。", + "member_not_found": "未找到成员。请联系你的团队者。", "member_removed": "用户已移除", "member_role_updated": "用户角色已更新", "members": "成员", @@ -588,10 +588,10 @@ "new": "新团队", "new_created": "已创建新团队", "new_name": "我的新团队", - "no_access": "你没有编辑组合的权限", - "no_invite_found": "未找到邀请。请联系你的团队所有人。", - "not_found": "Team not found. Contact your team owner.", - "not_valid_viewer": "你不是有效的阅览者。请联系你的团队所有人。", + "no_access": "你没有编辑集合的权限", + "no_invite_found": "未找到邀请。请联系你的团队者。", + "not_found": "没有找到团队,请联系您的团队所有者。", + "not_valid_viewer": "你不是有效的查看者。请联系你的团队者。", "pending_invites": "待办邀请", "permissions": "权限", "saved": "团队已保存", diff --git a/packages/hoppscotch-app/locales/en.json b/packages/hoppscotch-app/locales/en.json index b7d058558..df0e137b3 100644 --- a/packages/hoppscotch-app/locales/en.json +++ b/packages/hoppscotch-app/locales/en.json @@ -1,5 +1,6 @@ { "action": { + "autoscroll": "Autoscroll", "cancel": "Cancel", "choose_file": "Choose a file", "clear": "Clear", @@ -13,6 +14,7 @@ "download_file": "Download file", "duplicate": "Duplicate", "edit": "Edit", + "filter_response": "Filter response", "go_back": "Go back", "label": "Label", "learn_more": "Learn more", @@ -20,11 +22,14 @@ "more": "More", "new": "New", "no": "No", + "open_workspace": "Open workspace", "paste": "Paste", "prettify": "Prettify", "remove": "Remove", "restore": "Restore", "save": "Save", + "scroll_to_bottom": "Scroll to bottom", + "scroll_to_top": "Scroll to top", "search": "Search", "send": "Send", "start": "Start", @@ -164,6 +169,7 @@ "profile": "Login in to view your profile", "protocols": "Protocols are empty", "schema": "Connect to a GraphQL endpoint to view schema", + "shortcodes": "Shortcodes are empty", "team_name": "Team name empty", "teams": "You don't belong to any teams", "tests": "There are no tests for this request" @@ -197,9 +203,11 @@ "invalid_link": "Invalid link", "invalid_link_description": "The link you clicked is invalid or expired.", "json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again", + "json_parsing_failed": "Invalid JSON", "network_error": "There seems to be a network error. Please try again.", "network_fail": "Could not send request", "no_duration": "No duration", + "no_results_found": "No matches found", "script_fail": "Could not execute pre-request script", "something_went_wrong": "Something went wrong", "test_script_fail": "Could not execute post-request script" @@ -335,6 +343,11 @@ "body": "Request Body", "choose_language": "Choose language", "content_type": "Content Type", + "content_type_titles": { + "others": "Others", + "structured": "Structured", + "text": "Text" + }, "copy_link": "Copy link", "duration": "Duration", "enter_curl": "Enter cURL", @@ -364,10 +377,12 @@ "title": "Request", "type": "Request type", "url": "URL", - "variables": "Variables" + "variables": "Variables", + "view_my_links": "View my links" }, "response": { "body": "Response Body", + "filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "headers": "Headers", "html": "HTML", "image": "Image", @@ -419,6 +434,8 @@ "proxy_use_toggle": "Use the proxy middleware to send requests", "read_the": "Read the", "reset_default": "Reset to default", + "short_codes": "Short codes", + "short_codes_description": "Short codes which were created by you.", "sidebar_on_left": "Sidebar on left", "sync": "Synchronise", "sync_collections": "Collections", @@ -480,6 +497,15 @@ "title": "Theme" } }, + "shortcodes":{ + "actions":"Actions", + "created_on": "Created on", + "deleted" : "Shortcode deleted", + "method": "Method", + "not_found":"Shortcode not found", + "short_code":"Short code", + "url": "URL" + }, "show": { "code": "Show code", "collection": "Expand Collection Panel", @@ -491,7 +517,8 @@ "event_name": "Event Name", "events": "Events", "log": "Log", - "url": "URL" + "url": "URL", + "connection_not_authorized": "This SocketIO connection does not use any authentication." }, "sse": { "event_type": "Event type", @@ -521,7 +548,19 @@ "loading": "Loading...", "none": "None", "nothing_found": "Nothing found for", - "waiting_send_request": "Waiting to send request" + "waiting_send_request": "Waiting to send request", + "subscribed_success": "Successfully subscribed to topic: {topic}", + "unsubscribed_success": "Successfully unsubscribed from topic: {topic}", + "subscribed_failed": "Failed to subscribe to topic: {topic}", + "unsubscribed_failed": "Failed to unsubscribe from topic: {topic}", + "published_message": "Published message: {message} to topic: {topic}", + "published_error": "Something went wrong while publishing msg: {topic} to topic: {message}", + "message_received": "Message: {message} arrived on topic: {topic}", + "mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}", + "connection_lost": "Connection lost", + "connection_failed": "Connection failed", + "connection_error": "Failed to connect", + "reconnection_error": "Failed to reconnect" }, "support": { "changelog": "Read more about latest releases", diff --git a/packages/hoppscotch-app/locales/hu.json b/packages/hoppscotch-app/locales/hu.json index 442cb5d2d..ca90a9736 100644 --- a/packages/hoppscotch-app/locales/hu.json +++ b/packages/hoppscotch-app/locales/hu.json @@ -1,5 +1,6 @@ { "action": { + "autoscroll": "Automatikus görgetés", "cancel": "Mégse", "choose_file": "Válasszon egy fájlt", "clear": "Törlés", @@ -9,7 +10,7 @@ "delete": "Törlés", "disconnect": "Leválasztás", "dismiss": "Eltüntetés", - "dont_save": "Don't save", + "dont_save": "Ne mentse", "download_file": "Fájl letöltése", "duplicate": "Kettőzés", "edit": "Szerkesztés", @@ -20,11 +21,13 @@ "more": "Több", "new": "Új", "no": "Nem", - "paste": "Paste", + "paste": "Beillesztés", "prettify": "Csinosítás", "remove": "Eltávolítás", "restore": "Visszaállítás", "save": "Mentés", + "scroll_to_bottom": "Görgetés az aljára", + "scroll_to_top": "Görgetés a tetejére", "search": "Keresés", "send": "Küldés", "start": "Indítás", @@ -45,9 +48,9 @@ "chat_with_us": "Csevegjen velünk", "contact_us": "Lépjen kapcsolatba velünk", "copy": "Másolás", - "copy_user_id": "Copy User Auth Token", - "developer_option": "Developer options", - "developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.", + "copy_user_id": "Felhasználó-hitelesítési token másolása", + "developer_option": "Fejlesztői beállítások", + "developer_option_description": "Fejlesztői eszközök, amelyek segítenek a Hoppscotch fejlesztésében és karbantartásában.", "discord": "Discord", "documentation": "Dokumentáció", "github": "GitHub", @@ -60,7 +63,7 @@ "keyboard_shortcuts": "Gyorsbillentyűk", "name": "Hoppscotch", "new_version_found": "Új verzió található. Töltse újra az oldalt a frissítéshez.", - "options": "Options", + "options": "Beállítások", "proxy_privacy_policy": "Proxy adatvédelmi irányelvei", "reload": "Újratöltés", "search": "Keresés", @@ -68,7 +71,7 @@ "shortcuts": "Gyorsbillentyűk", "spotlight": "Reflektorfény", "status": "Állapot", - "status_description": "Check the status of the website", + "status_description": "A weboldal állapotának ellenőrzése", "terms_and_privacy": "Feltételek és adatvédelem", "twitter": "Twitter", "type_a_command_search": "Írjon be parancsot vagy keresést…", @@ -82,7 +85,7 @@ "continue_with_email": "Folytatás e-mail-címmel", "continue_with_github": "Folytatás GitHub használatával", "continue_with_google": "Folytatás Google használatával", - "continue_with_microsoft": "Continue with Microsoft", + "continue_with_microsoft": "Folytatás Microsoft használatával", "email": "E-mail", "logged_out": "Kijelentkezett", "login": "Bejelentkezés", @@ -111,7 +114,7 @@ "invalid_name": "Adjon nevet a gyűjteménynek", "my_collections": "Saját gyűjtemények", "name": "Saját új gyűjtemény", - "name_length_insufficient": "Collection name should be at least 3 characters long", + "name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie", "new": "Új gyűjtemény", "renamed": "Gyűjtemény átnevezve", "request_in_use": "A kérés használatban", @@ -131,7 +134,7 @@ "remove_request": "Biztosan véglegesen törölni szeretné ezt a kérést?", "remove_team": "Biztosan törölni szeretné ezt a csapatot?", "remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?", - "request_change": "Are you sure you want to discard current request, unsaved changes will be lost.", + "request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.", "sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát." }, "count": { @@ -169,20 +172,20 @@ "tests": "Nincsenek tesztek ehhez a kéréshez" }, "environment": { - "add_to_global": "Add to Global", - "added": "Environment addition", + "add_to_global": "Hozzáadás a globálishoz", + "added": "Környezet hozzáadása", "create_new": "Új környezet létrehozása", - "created": "Environment created", - "deleted": "Environment deletion", + "created": "Környezet létrehozva", + "deleted": "Környezet törlése", "edit": "Környezet szerkesztése", "invalid_name": "Adjon nevet a környezetnek", - "nested_overflow": "nested environment variables are limited to 10 levels", + "nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva", "new": "Új környezet", "no_environment": "Nincs környezet", - "no_environment_description": "No environments were selected. Choose what to do with the following variables.", + "no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.", "select": "Környezet kiválasztása", "title": "Környezetek", - "updated": "Environment updation", + "updated": "Környezet frissítve", "variable_list": "Változólista" }, "error": { @@ -192,7 +195,7 @@ "empty_req_name": "Üres kérésnév", "f12_details": "(F12 a részletekért)", "gql_prettify_invalid_query": "Nem sikerült csinosítani egy érvénytelen lekérdezést, oldja meg a lekérdezés szintaktikai hibáit, és próbálja újra", - "incomplete_config_urls": "Incomplete configuration URLs", + "incomplete_config_urls": "Befejezetlen beállítási URL-ek", "incorrect_email": "Hibás e-mail", "invalid_link": "Érvénytelen hivatkozás", "invalid_link_description": "A kattintott hivatkozás érvénytelen vagy lejárt.", @@ -202,20 +205,20 @@ "no_duration": "Nincs időtartam", "script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt", "something_went_wrong": "Valami elromlott", - "test_script_fail": "Could not execute post-request script" + "test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt" }, "export": { "as_json": "Exportálás JSON formátumban", "create_secret_gist": "Titkos Gist létrehozása", "gist_created": "Gist létrehozva", "require_github": "Jelentkezzen be GitHub használatával a titkos Gist létrehozásához", - "title": "Export" + "title": "Exportálás" }, "folder": { "created": "Mappa létrehozva", "edit": "Mappa szerkesztése", "invalid_name": "Adjon nevet a mappának", - "name_length_insufficient": "Folder name should be at least 3 characters long", + "name_length_insufficient": "A mappa nevének legalább 3 karakter hosszúságúnak kell lennie", "new": "Új mappa", "renamed": "Mappa átnevezve" }, @@ -238,11 +241,11 @@ "post_request_tests": "A tesztparancsfájlokat JavaScriptben írták, és a válasz megérkezése után lesznek futtatva.", "pre_request_script": "A kérés előtti parancsfájlokat JavaScriptben írták, és a kérés elküldése előtt lesznek futtatva.", "script_fail": "Úgy tűnik, hogy működési hiba van a kérés előtti parancsfájlban. Nézze meg az alábbi hibát, és annak megfelelően javítsa a parancsfájlt.", - "test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again", + "test_script_fail": "Úgy tűnik, hogy hiba van a tesztparancsfájlokkal. Javítsa ki a hibákat, és futtassa újra a teszteket.", "tests": "Írjon tesztparancsfájlt a hibakeresés automatizálására." }, "hide": { - "collection": "Collapse Collection Panel", + "collection": "Gyűjteménypanel összecsukása", "more": "Több elrejtése", "preview": "Előnézet elrejtése", "sidebar": "Oldalsáv összecsukása" @@ -266,13 +269,17 @@ "from_url": "Importálás URL-ből", "gist_url": "Gist URL megadása", "json_description": "Gyűjtemények importálása Hoppscotch-gyűjtemények JSON-fájlból", - "title": "Importálás" + "title": "Importálás", + "import_from_url_success": "Gyűjtemények importálva", + "import_from_url_invalid_file_format": "Hiba a gyűjtemények importálása során", + "import_from_url_invalid_type": "Nem támogatott típus. Az elfogadott értékek: „hoppscotch”, „openapi”, „postman” vagy „insomnia”.", + "import_from_url_invalid_fetch": "Nem sikerült lekérni az adatokat az URL-ről" }, "layout": { - "collapse_collection": "Collapse or Expand Collections", - "collapse_sidebar": "Collapse or Expand the sidebar", + "collapse_collection": "Gyűjtemények összecsukása vagy kinyitása", + "collapse_sidebar": "Az oldalsáv összecsukása vagy kinyitása", "column": "Függőleges elrendezés", - "name": "Layout", + "name": "Elrendezés", "row": "Vízszintes elrendezés", "zen_mode": "Zen mód" }, @@ -340,10 +347,10 @@ "invalid_name": "Adjon nevet a kérésnek", "method": "Módszer", "name": "Kérés neve", - "new": "New Request", - "override": "Override", - "override_help": "Set Content-Type in Headers", - "overriden": "Overridden", + "new": "Új kérés", + "override": "Felülbírálás", + "override_help": "A Content-Type beállítása a fejlécekben", + "overriden": "Felülbírálva", "parameter_list": "Lekérdezési paraméterek", "parameters": "Paraméterek", "path": "Útvonal", @@ -356,7 +363,7 @@ "save_as": "Mentés másként", "saved": "Kérés elmentve", "share": "Megosztás", - "share_description": "Share Hoppscotch with your friends", + "share_description": "A Hoppscotch megosztása az ismerőseivel", "title": "Kérés", "type": "Kérés típusa", "url": "URL", @@ -396,7 +403,7 @@ "extension_version": "Kiterjesztés verziója", "extensions": "Böngészőkiterjesztés", "extensions_use_toggle": "A böngészőkiterjesztés használata a kérések küldéséhez (ha jelen van)", - "follow": "Follow Us", + "follow": "Kövessen minket", "font_size": "Betűméret", "font_size_large": "Nagy", "font_size_medium": "Közepes", @@ -461,7 +468,7 @@ "method": "Módszer", "next_method": "Következő módszer kiválasztása", "post_method": "POST módszer kiválasztása", - "previous_method": "Elősző módszer kiválasztása", + "previous_method": "Előző módszer kiválasztása", "put_method": "PUT módszer kiválasztása", "reset_request": "Kérés visszaállítása", "save_to_collections": "Mentés a gyűjteményekbe", @@ -478,7 +485,7 @@ }, "show": { "code": "Kód megjelenítése", - "collection": "Expand Collection Panel", + "collection": "Gyűjteménypanel kinyitása", "more": "Több megjelenítése", "sidebar": "Oldalsáv kinyitása" }, @@ -525,11 +532,11 @@ "community": "Tegyen fel kérdéseket és segítsen másoknak", "documentation": "Tudjon meg többet a Hoppscotchról", "forum": "Tegyen fel kérdéseket és kapjon válaszokat", - "github": "Follow us on Github", + "github": "Kövessen minket GitHubon", "shortcuts": "Az alkalmazás gyorsabb böngészése", "team": "Vegye fel a kapcsolatot a csapattal", "title": "Támogatás", - "twitter": "Kövess minket Twitteren" + "twitter": "Kövessen minket Twitteren" }, "tab": { "authorization": "Felhatalmazás", @@ -590,7 +597,7 @@ "new_name": "Saját új csapat", "no_access": "Nincs szerkesztési jogosultsága ezekhez a gyűjteményekhez", "no_invite_found": "A meghívás nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.", - "not_found": "Team not found. Contact your team owner.", + "not_found": "A csapat nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.", "not_valid_viewer": "Ön nem érvényes megtekintő. Vegye fel a kapcsolatot a csapat tulajdonosával.", "pending_invites": "Függőben lévő meghívások", "permissions": "Jogosultságok", diff --git a/packages/hoppscotch-app/locales/pt-br.json b/packages/hoppscotch-app/locales/pt-br.json index a775b3ae0..fada4f71d 100644 --- a/packages/hoppscotch-app/locales/pt-br.json +++ b/packages/hoppscotch-app/locales/pt-br.json @@ -2,16 +2,16 @@ "action": { "cancel": "Cancelar", "choose_file": "Escolha um arquivo", - "clear": "Claro", + "clear": "Limpar", "clear_all": "Limpar tudo", "connect": "Conectar", "copy": "Copiar", "delete": "Excluir", - "disconnect": "desconectar", + "disconnect": "Desconectar", "dismiss": "Dispensar", - "dont_save": "Don't save", + "dont_save": "Não Salvar", "download_file": "⇬ Fazer download do arquivo", - "duplicate": "Duplicate", + "duplicate": "Duplicar", "edit": "Editar", "go_back": "Voltar", "label": "Etiqueta", @@ -35,7 +35,7 @@ "turn_off": "Desligar", "turn_on": "Ligar", "undo": "Desfazer", - "yes": "sim" + "yes": "Sim" }, "add": { "new": "Adicionar novo", @@ -45,9 +45,9 @@ "chat_with_us": "Converse conosco", "contact_us": "Contate-Nos", "copy": "Copiar", - "copy_user_id": "Copy User Auth Token", - "developer_option": "Developer options", - "developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.", + "copy_user_id": "Copiar token de autenticação do usuário", + "developer_option": "Opções de desenvolvedor", + "developer_option_description": "Opções de desenvolvedor que ajudam no desenvolvimento e manutenção do Hoppscotch.", "discord": "Discord", "documentation": "Documentação", "github": "GitHub", @@ -60,18 +60,18 @@ "keyboard_shortcuts": "Atalhos do teclado", "name": "Hoppscotch", "new_version_found": "Nova versão encontrada. Atualize para atualizar.", - "options": "Options", + "options": "Opções", "proxy_privacy_policy": "Política de privacidade do proxy", - "reload": "recarregar", + "reload": "Recarregar", "search": "Procurar", "share": "Compartilhado", "shortcuts": "Atalhos", "spotlight": "Holofote", - "status": "Status", - "status_description": "Check the status of the website", + "status": "Estado", + "status_description": "Cheque o estado do website.", "terms_and_privacy": "Termos e privacidade", "twitter": "Twitter", - "type_a_command_search": "Digite um comando ou pesquise ...", + "type_a_command_search": "Digite um comando ou pesquise...", "we_use_cookies": "Usamos cookies", "whats_new": "O que há de novo?", "wiki": "Wiki" @@ -114,7 +114,7 @@ "name_length_insufficient": "O nome da coleção deve ter pelo menos 3 caracteres", "new": "Nova coleção", "renamed": "Coleção renomeada", - "request_in_use": "Request in use", + "request_in_use": "Requisição em uso", "save_as": "Salvar como", "select": "Selecione uma coleção", "select_location": "Selecione a localização", @@ -131,7 +131,7 @@ "remove_request": "Tem certeza de que deseja excluir permanentemente esta solicitação?", "remove_team": "Tem certeza que deseja excluir esta equipe?", "remove_telemetry": "Tem certeza de que deseja cancelar a telemetria?", - "request_change": "Are you sure you want to discard current request, unsaved changes will be lost.", + "request_change": "Tem certeza que deseja descartar a requisição atual? Alterações não salvas serão perdidas.", "sync": "Tem certeza de que deseja sincronizar este espaço de trabalho?" }, "count": { @@ -151,8 +151,8 @@ "body": "Este pedido não tem corpo", "collection": "Coleção está vazia", "collections": "Coleções estão vazias", - "documentation": "Connect to a GraphQL endpoint to view documentation", - "endpoint": "Endpoint cannot be empty", + "documentation": "Se conecte à um endpoint GraphQL para ver a documentação", + "endpoint": "O endpoint não pode ser vazio", "environments": "Ambientes estão vazios", "folder": "Pasta está vazia", "headers": "Esta solicitação não possui cabeçalhos", @@ -172,11 +172,11 @@ "add_to_global": "Adicionar ao Global", "added": "Adição de ambiente", "create_new": "Crie um novo ambiente", - "created": "Environment created", + "created": "Ambiente criado", "deleted": "Deleção de ambiente", "edit": "Editar Ambiente", "invalid_name": "Forneça um nome válido para o ambiente", - "nested_overflow": "variáveis de ambiente aninhadas são limitadas a 10 níveis", + "nested_overflow": "Variáveis de ambiente aninhadas são limitadas a 10 níveis", "new": "Novo ambiente", "no_environment": "Sem ambiente", "no_environment_description": "Nenhum ambiente foi selecionado. Escolha o que fazer com as seguintes variáveis.", @@ -195,9 +195,9 @@ "incomplete_config_urls": "URLs de configuração incompletas", "incorrect_email": "Email incorreto", "invalid_link": "Link inválido", - "invalid_link_description": "The link you clicked is invalid or expired.", + "invalid_link_description": "O link que você clicou é inválido ou já expirou.", "json_prettify_invalid_body": "Não foi possível embelezar um corpo inválido, resolver erros de sintaxe json e tentar novamente", - "network_error": "There seems to be a network error. Please try again.", + "network_error": "Parece que houve um problema de rede. Por favor, tente novamente.", "network_fail": "Não foi possível enviar requisição", "no_duration": "Sem duração", "script_fail": "Não foi possível executar o script pré-requisição", @@ -252,25 +252,25 @@ "curl": "Importar cURL", "failed": "A importação falhou", "from_gist": "Importar do Gist", - "from_gist_description": "Import from Gist URL", - "from_insomnia": "Import from Insomnia", - "from_insomnia_description": "Import from Insomnia collection", - "from_json": "Import from Hoppscotch", - "from_json_description": "Import from Hoppscotch collection file", + "from_gist_description": "Importar de URL Gist", + "from_insomnia": "Importar de Insomnia", + "from_insomnia_description": "Importa de coleção Insomnia", + "from_json": "Importar de Hoppscotch", + "from_json_description": "Importa de arquivo de coleção Hoppscotch", "from_my_collections": "Importar de minhas coleções", - "from_my_collections_description": "Import from My Collections file", - "from_openapi": "Import from OpenAPI", - "from_openapi_description": "Import from OpenAPI specification file (YML/JSON)", - "from_postman": "Import from Postman", - "from_postman_description": "Import from Postman collection", - "from_url": "Import from URL", - "gist_url": "Insira o URL da essência", - "json_description": "Import collections from a Hoppscotch Collections JSON file", + "from_my_collections_description": "Importa de arquivo Minhas Coleções", + "from_openapi": "Importar de OpenAPI", + "from_openapi_description": "Importa de arquivo de especificação OpenAPI (YML/JSON)", + "from_postman": "Importar de Postman", + "from_postman_description": "Importa de coleção Postman", + "from_url": "Importar de URL", + "gist_url": "Insira o URL do Gist", + "json_description": "Importa coleções de um arquivo JSON de Coleções Hoppscotch", "title": "Importar" }, "layout": { - "collapse_collection": "Collapse or Expand Collections", - "collapse_sidebar": "Collapse or Expand the sidebar", + "collapse_collection": "Encolher ou expandir coleções", + "collapse_sidebar": "Encolher ou Expandir a barra lateral", "column": "Layout vertical", "name": "Layout", "row": "Layout horizontal", @@ -311,16 +311,16 @@ "profile": { "app_settings": "App Settings", "editor": "Editor", - "editor_description": "Editors can add, edit, and delete requests.", - "email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.", - "no_permission": "You do not have permission to perform this action.", - "owner": "Owner", - "owner_description": "Owners can add, edit, and delete requests, collections and team members.", - "roles": "Roles", - "roles_description": "Roles are used to control access to the shared collections.", - "updated": "Profile updated", - "viewer": "Viewer", - "viewer_description": "Viewers can only view and use requests." + "editor_description": "Editores podem adicionar, editar e deletar requisições.", + "email_verification_mail": "Um e-mail de verificação foi enviado ao seu endereço de e-mail. Por favor, clique no link para verificar seu endereço e-mail.", + "no_permission": "Você não tem permissão para realizar esta ação.", + "owner": "Dono", + "owner_description": "Donos podem adicionar, editar e deletar requisições, coleções e membros de equipe.", + "roles": "Funções", + "roles_description": "Funções são utilizadas para gerenciar acesso às coleções compartilhadas.", + "updated": "Perfil atualizado", + "viewer": "Espectador", + "viewer_description": "Espectadores só podem ver e usar requisições." }, "remove": { "star": "Remover estrela" @@ -340,10 +340,10 @@ "invalid_name": "Forneça um nome para a requisição", "method": "Método", "name": "Nome da requisição", - "new": "New Request", - "override": "Override", - "override_help": "Set Content-Type in Headers", - "overriden": "Overridden", + "new": "Nova requisição", + "override": "Substituir", + "override_help": "Substituir Content-Type em Headers", + "overriden": "Substituído", "parameter_list": "Parâmetros da requisição", "parameters": "Parâmetros", "path": "Caminho", @@ -356,7 +356,7 @@ "save_as": "Salvar como", "saved": "Requisição salva", "share": "Compartilhadar", - "share_description": "Share Hoppscotch with your friends", + "share_description": "Compartilhe o Hoppscotch com seus amigos", "title": "Solicitar", "type": "Tipo de requisição", "url": "URL", @@ -396,7 +396,7 @@ "extension_version": "Versão da extensão", "extensions": "Extensões", "extensions_use_toggle": "Use a extensão do navegador para enviar solicitações (se houver)", - "follow": "Follow Us", + "follow": "Nos siga", "font_size": "Tamanho da fonte", "font_size_large": "Grande", "font_size_medium": "Médio", @@ -407,7 +407,7 @@ "light_mode": "Luz", "official_proxy_hosting": "Official Proxy é hospedado por Hoppscotch.", "profile": "Perfil", - "profile_description": "Update your profile details", + "profile_description": "Atualize os detalhes de seu perfil", "profile_email": "Endereço de email", "profile_name": "Nome do perfil", "proxy": "Proxy", diff --git a/packages/hoppscotch-app/newstore/HoppExtension.ts b/packages/hoppscotch-app/newstore/HoppExtension.ts new file mode 100644 index 000000000..ae725333f --- /dev/null +++ b/packages/hoppscotch-app/newstore/HoppExtension.ts @@ -0,0 +1,40 @@ +import { distinctUntilChanged, pluck } from "rxjs" +import DispatchingStore, { defineDispatchers } from "./DispatchingStore" + +export type ExtensionStatus = "available" | "unknown-origin" | "waiting" + +type InitialState = { + extensionStatus: ExtensionStatus +} + +const initialState: InitialState = { + extensionStatus: "waiting", +} + +const dispatchers = defineDispatchers({ + changeExtensionStatus( + _, + { extensionStatus }: { extensionStatus: ExtensionStatus } + ) { + return { + extensionStatus, + } + }, +}) + +export const hoppExtensionStore = new DispatchingStore( + initialState, + dispatchers +) + +export const extensionStatus$ = hoppExtensionStore.subject$.pipe( + pluck("extensionStatus"), + distinctUntilChanged() +) + +export function changeExtensionStatus(extensionStatus: ExtensionStatus) { + hoppExtensionStore.dispatch({ + dispatcher: "changeExtensionStatus", + payload: { extensionStatus }, + }) +} diff --git a/packages/hoppscotch-app/newstore/MQTTSession.ts b/packages/hoppscotch-app/newstore/MQTTSession.ts index e67382709..cad1e8d68 100644 --- a/packages/hoppscotch-app/newstore/MQTTSession.ts +++ b/packages/hoppscotch-app/newstore/MQTTSession.ts @@ -1,6 +1,6 @@ -import { pluck, distinctUntilChanged } from "rxjs/operators" -import { Client as MQTTClient } from "paho-mqtt" +import { distinctUntilChanged, pluck } from "rxjs/operators" import DispatchingStore, { defineDispatchers } from "./DispatchingStore" +import { MQTTConnection } from "~/helpers/realtime/MQTTConnection" import { HoppRealtimeLog, HoppRealtimeLogLine, @@ -12,11 +12,9 @@ type HoppMQTTRequest = { type HoppMQTTSession = { request: HoppMQTTRequest - connectingState: boolean - connectionState: boolean subscriptionState: boolean log: HoppRealtimeLog - socket: MQTTClient | null + socket: MQTTConnection } const defaultMQTTRequest: HoppMQTTRequest = { @@ -25,10 +23,8 @@ const defaultMQTTRequest: HoppMQTTRequest = { const defaultMQTTSession: HoppMQTTSession = { request: defaultMQTTRequest, - connectionState: false, - connectingState: false, subscriptionState: false, - socket: null, + socket: new MQTTConnection(), log: [], } @@ -48,21 +44,11 @@ const dispatchers = defineDispatchers({ }, } }, - setSocket(_: HoppMQTTSession, { socket }: { socket: MQTTClient }) { + setConn(_: HoppMQTTSession, { socket }: { socket: MQTTConnection }) { return { socket, } }, - setConnectionState(_: HoppMQTTSession, { state }: { state: boolean }) { - return { - connectionState: state, - } - }, - setConnectingState(_: HoppMQTTSession, { state }: { state: boolean }) { - return { - connectingState: state, - } - }, setSubscriptionState(_: HoppMQTTSession, { state }: { state: boolean }) { return { subscriptionState: state, @@ -100,33 +86,15 @@ export function setMQTTEndpoint(newEndpoint: string) { }) } -export function setMQTTSocket(socket: MQTTClient) { +export function setMQTTConn(socket: MQTTConnection) { MQTTSessionStore.dispatch({ - dispatcher: "setSocket", + dispatcher: "setConn", payload: { socket, }, }) } -export function setMQTTConnectionState(state: boolean) { - MQTTSessionStore.dispatch({ - dispatcher: "setConnectionState", - payload: { - state, - }, - }) -} - -export function setMQTTConnectingState(state: boolean) { - MQTTSessionStore.dispatch({ - dispatcher: "setConnectingState", - payload: { - state, - }, - }) -} - export function setMQTTSubscriptionState(state: boolean) { MQTTSessionStore.dispatch({ dispatcher: "setSubscriptionState", @@ -179,7 +147,7 @@ export const MQTTSubscriptionState$ = MQTTSessionStore.subject$.pipe( distinctUntilChanged() ) -export const MQTTSocket$ = MQTTSessionStore.subject$.pipe( +export const MQTTConn$ = MQTTSessionStore.subject$.pipe( pluck("socket"), distinctUntilChanged() ) diff --git a/packages/hoppscotch-app/newstore/SSESession.ts b/packages/hoppscotch-app/newstore/SSESession.ts index fec8770d7..3577483ae 100644 --- a/packages/hoppscotch-app/newstore/SSESession.ts +++ b/packages/hoppscotch-app/newstore/SSESession.ts @@ -4,6 +4,7 @@ import { HoppRealtimeLog, HoppRealtimeLogLine, } from "~/helpers/types/HoppRealtimeLog" +import { SSEConnection } from "~/helpers/realtime/SSEConnection" type HoppSSERequest = { endpoint: string @@ -12,10 +13,8 @@ type HoppSSERequest = { type HoppSSESession = { request: HoppSSERequest - connectingState: boolean - connectionState: boolean log: HoppRealtimeLog - socket: EventSource | null + socket: SSEConnection } const defaultSSERequest: HoppSSERequest = { @@ -25,9 +24,7 @@ const defaultSSERequest: HoppSSERequest = { const defaultSSESession: HoppSSESession = { request: defaultSSERequest, - connectionState: false, - connectingState: false, - socket: null, + socket: new SSEConnection(), log: [], } @@ -56,21 +53,11 @@ const dispatchers = defineDispatchers({ }, } }, - setSocket(_: HoppSSESession, { socket }: { socket: EventSource }) { + setSocket(_: HoppSSESession, { socket }: { socket: SSEConnection }) { return { socket, } }, - setConnectionState(_: HoppSSESession, { state }: { state: boolean }) { - return { - connectionState: state, - } - }, - setConnectingState(_: HoppSSESession, { state }: { state: boolean }) { - return { - connectingState: state, - } - }, setLog(_: HoppSSESession, { log }: { log: HoppRealtimeLog }) { return { log, @@ -112,7 +99,7 @@ export function setSSEEventType(newType: string) { }) } -export function setSSESocket(socket: EventSource) { +export function setSSESocket(socket: SSEConnection) { SSESessionStore.dispatch({ dispatcher: "setSocket", payload: { @@ -121,23 +108,6 @@ export function setSSESocket(socket: EventSource) { }) } -export function setSSEConnectionState(state: boolean) { - SSESessionStore.dispatch({ - dispatcher: "setConnectionState", - payload: { - state, - }, - }) -} -export function setSSEConnectingState(state: boolean) { - SSESessionStore.dispatch({ - dispatcher: "setConnectingState", - payload: { - state, - }, - }) -} - export function setSSELog(log: HoppRealtimeLog) { SSESessionStore.dispatch({ dispatcher: "setLog", @@ -176,11 +146,6 @@ export const SSEConnectingState$ = SSESessionStore.subject$.pipe( distinctUntilChanged() ) -export const SSEConnectionState$ = SSESessionStore.subject$.pipe( - pluck("connectionState"), - distinctUntilChanged() -) - export const SSESocket$ = SSESessionStore.subject$.pipe( pluck("socket"), distinctUntilChanged() diff --git a/packages/hoppscotch-app/newstore/SocketIOSession.ts b/packages/hoppscotch-app/newstore/SocketIOSession.ts index 4b3aa57a4..f8561bdef 100644 --- a/packages/hoppscotch-app/newstore/SocketIOSession.ts +++ b/packages/hoppscotch-app/newstore/SocketIOSession.ts @@ -10,16 +10,16 @@ import { type SocketIO = SocketV2 | SocketV3 | SocketV4 +export type SIOClientVersion = "v4" | "v3" | "v2" + type HoppSIORequest = { endpoint: string path: string - version: string + version: SIOClientVersion } type HoppSIOSession = { request: HoppSIORequest - connectingState: boolean - connectionState: boolean log: HoppRealtimeLog socket: SocketIO | null } @@ -32,8 +32,6 @@ const defaultSIORequest: HoppSIORequest = { const defaultSIOSession: HoppSIOSession = { request: defaultSIORequest, - connectionState: false, - connectingState: false, socket: null, log: [], } @@ -63,7 +61,10 @@ const dispatchers = defineDispatchers({ }, } }, - setVersion(curr: HoppSIOSession, { newVersion }: { newVersion: string }) { + setVersion( + curr: HoppSIOSession, + { newVersion }: { newVersion: SIOClientVersion } + ) { return { request: { ...curr.request, @@ -76,16 +77,6 @@ const dispatchers = defineDispatchers({ socket, } }, - setConnectionState(_: HoppSIOSession, { state }: { state: boolean }) { - return { - connectionState: state, - } - }, - setConnectingState(_: HoppSIOSession, { state }: { state: boolean }) { - return { - connectingState: state, - } - }, setLog(_: HoppSIOSession, { log }: { log: HoppRealtimeLog }) { return { log, @@ -145,23 +136,6 @@ export function setSIOSocket(socket: SocketIO) { }) } -export function setSIOConnectionState(state: boolean) { - SIOSessionStore.dispatch({ - dispatcher: "setConnectionState", - payload: { - state, - }, - }) -} -export function setSIOConnectingState(state: boolean) { - SIOSessionStore.dispatch({ - dispatcher: "setConnectingState", - payload: { - state, - }, - }) -} - export function setSIOLog(log: HoppRealtimeLog) { SIOSessionStore.dispatch({ dispatcher: "setLog", @@ -200,11 +174,6 @@ export const SIOPath$ = SIOSessionStore.subject$.pipe( distinctUntilChanged() ) -export const SIOConnectingState$ = SIOSessionStore.subject$.pipe( - pluck("connectingState"), - distinctUntilChanged() -) - export const SIOConnectionState$ = SIOSessionStore.subject$.pipe( pluck("connectionState"), distinctUntilChanged() diff --git a/packages/hoppscotch-app/newstore/WebSocketSession.ts b/packages/hoppscotch-app/newstore/WebSocketSession.ts index 9d2d76873..6f5759e0a 100644 --- a/packages/hoppscotch-app/newstore/WebSocketSession.ts +++ b/packages/hoppscotch-app/newstore/WebSocketSession.ts @@ -4,8 +4,9 @@ import { HoppRealtimeLog, HoppRealtimeLogLine, } from "~/helpers/types/HoppRealtimeLog" +import { WSConnection } from "~/helpers/realtime/WSConnection" -type HoppWSProtocol = { +export type HoppWSProtocol = { value: string active: boolean } @@ -17,10 +18,8 @@ type HoppWSRequest = { export type HoppWSSession = { request: HoppWSRequest - connectingState: boolean - connectionState: boolean log: HoppRealtimeLog - socket: WebSocket | null + socket: WSConnection } const defaultWSRequest: HoppWSRequest = { @@ -30,9 +29,7 @@ const defaultWSRequest: HoppWSRequest = { const defaultWSSession: HoppWSSession = { request: defaultWSRequest, - connectionState: false, - connectingState: false, - socket: null, + socket: new WSConnection(), log: [], } @@ -101,21 +98,11 @@ const dispatchers = defineDispatchers({ }, } }, - setSocket(_: HoppWSSession, { socket }: { socket: WebSocket }) { + setSocket(_: HoppWSSession, { socket }: { socket: WSConnection }) { return { socket, } }, - setConnectionState(_: HoppWSSession, { state }: { state: boolean }) { - return { - connectionState: state, - } - }, - setConnectingState(_: HoppWSSession, { state }: { state: boolean }) { - return { - connectingState: state, - } - }, setLog(_: HoppWSSession, { log }: { log: HoppRealtimeLog }) { return { log, @@ -195,7 +182,7 @@ export function updateWSProtocol( }) } -export function setWSSocket(socket: WebSocket) { +export function setWSSocket(socket: WSConnection) { WSSessionStore.dispatch({ dispatcher: "setSocket", payload: { @@ -204,23 +191,6 @@ export function setWSSocket(socket: WebSocket) { }) } -export function setWSConnectionState(state: boolean) { - WSSessionStore.dispatch({ - dispatcher: "setConnectionState", - payload: { - state, - }, - }) -} -export function setWSConnectingState(state: boolean) { - WSSessionStore.dispatch({ - dispatcher: "setConnectingState", - payload: { - state, - }, - }) -} - export function setWSLog(log: HoppRealtimeLog) { WSSessionStore.dispatch({ dispatcher: "setLog", diff --git a/packages/hoppscotch-app/newstore/environments.ts b/packages/hoppscotch-app/newstore/environments.ts index 75fa93c44..c7d282481 100644 --- a/packages/hoppscotch-app/newstore/environments.ts +++ b/packages/hoppscotch-app/newstore/environments.ts @@ -540,6 +540,6 @@ export function updateEnvironmentVariable( }) } -export function getEnviroment(index: number) { +export function getEnvironment(index: number) { return environmentsStore.value.environments[index] } diff --git a/packages/hoppscotch-app/nuxt.config.js b/packages/hoppscotch-app/nuxt.config.js index d6afbfb29..4a32366ff 100644 --- a/packages/hoppscotch-app/nuxt.config.js +++ b/packages/hoppscotch-app/nuxt.config.js @@ -132,7 +132,7 @@ export default { // https://github.com/nuxt/typescript ["@nuxt/typescript-build", { typeCheck: false }], // https://github.com/nuxt-community/dotenv-module - "@nuxtjs/dotenv", + ["@nuxtjs/dotenv", { systemvars: true }], // https://github.com/nuxt-community/composition-api "@nuxtjs/composition-api/module", "~/modules/emit-volar-types.ts", @@ -339,6 +339,8 @@ export default { APP_ID: process.env.APP_ID, MEASUREMENT_ID: process.env.MEASUREMENT_ID, BASE_URL: process.env.BASE_URL, + BACKEND_GQL_URL: process.env.BACKEND_GQL_URL, + BACKEND_WS_URL: process.env.BACKEND_WS_URL, }, publicRuntimeConfig: { diff --git a/packages/hoppscotch-app/package.json b/packages/hoppscotch-app/package.json index 1a9547dba..f0ad8b55a 100644 --- a/packages/hoppscotch-app/package.json +++ b/packages/hoppscotch-app/package.json @@ -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.1.0", + "@hoppscotch/codemirror-lang-graphql": "workspace:^0.2.0", "@hoppscotch/data": "workspace:^0.4.2", "@hoppscotch/js-sandbox": "workspace:^2.0.0", "@nuxtjs/axios": "^5.13.6", @@ -88,6 +88,7 @@ "io-ts": "^2.2.16", "js-yaml": "^4.1.0", "json-loader": "^0.5.7", + "jsonpath-plus": "^6.0.1", "lodash": "^4.17.21", "lossless-json": "^1.0.5", "mustache": "^4.2.0", @@ -153,6 +154,7 @@ "@types/paho-mqtt": "^1.0.6", "@types/postman-collection": "^3.5.7", "@types/qs": "^6.9.7", + "@types/socketio-wildcard": "^2.0.4", "@types/splitpanes": "^2.2.1", "@types/uuid": "^8.3.4", "@types/yargs-parser": "^21.0.0", diff --git a/packages/hoppscotch-app/pages/profile.vue b/packages/hoppscotch-app/pages/profile.vue index 9d5f64f85..21ec30056 100644 --- a/packages/hoppscotch-app/pages/profile.vue +++ b/packages/hoppscotch-app/pages/profile.vue @@ -3,7 +3,13 @@
+ +
+
- + -
-

- {{ t("settings.profile") }} -

-
- {{ t("settings.profile_description") }} -
-
- -
- - - -
-
- -
- - - -
-
-
-

- {{ t("settings.sync") }} -

-
- {{ t("settings.sync_description") }} -
-
-
- - {{ t("settings.sync_collections") }} - +
+
+

+ {{ t("settings.profile") }} +

+
+ {{ t("settings.profile_description") }}
-
- + +
- {{ t("settings.sync_environments") }} - + + +
-
- + +
- {{ t("settings.sync_history") }} - + + +
-
-
+ +
+

+ {{ t("settings.sync") }} +

+
+ {{ t("settings.sync_description") }} +
+
+
+ + {{ t("settings.sync_collections") }} + +
+
+ + {{ t("settings.sync_environments") }} + +
+
+ + {{ t("settings.sync_history") }} + +
+
+
+
+

+ {{ t("settings.short_codes") }} +

+
+ {{ t("settings.short_codes_description") }} +
+
+
+ + {{ + t("state.loading") + }} +
+
+ + + {{ t("empty.shortcodes") }} + +
+
+ +
+
+ + +
+ +
+
+
+
+
+
+ help_outline + {{ getErrorMessage(adapterError) }} +
+
+
+
@@ -193,12 +293,18 @@ import { useMeta, defineComponent, watchEffect, + computed, } from "@nuxtjs/composition-api" +import { pipe } from "fp-ts/function" +import * as TE from "fp-ts/TaskEither" +import { GQLError } from "~/helpers/backend/GQLClient" import { currentUser$, + probableUser$, setDisplayName, setEmailAddress, verifyEmailAddress, + onLoggedIn, } from "~/helpers/fb/auth" import { useReadonlyStream, @@ -206,6 +312,8 @@ import { useToast, } from "~/helpers/utils/composables" import { toggleSetting, useSetting } from "~/newstore/settings" +import ShortcodeListAdapter from "~/helpers/shortcodes/ShortcodeListAdapter" +import { deleteShortcode as backendDeleteShortcode } from "~/helpers/backend/mutations/Shortcode" type ProfileTabs = "sync" | "teams" @@ -220,6 +328,13 @@ const SYNC_COLLECTIONS = useSetting("syncCollections") const SYNC_ENVIRONMENTS = useSetting("syncEnvironments") const SYNC_HISTORY = useSetting("syncHistory") const currentUser = useReadonlyStream(currentUser$, null) +const probableUser = useReadonlyStream(probableUser$, null) + +const loadingCurrentUser = computed(() => { + if (!probableUser.value) return false + else if (!currentUser.value) return true + else return false +}) const displayName = ref(currentUser.value?.displayName) const updatingDisplayName = ref(false) @@ -273,6 +388,51 @@ const sendEmailVerification = () => { }) } +const adapter = new ShortcodeListAdapter(true) +const adapterLoading = useReadonlyStream(adapter.loading$, false) +const adapterError = useReadonlyStream(adapter.error$, null) +const myShortcodes = useReadonlyStream(adapter.shortcodes$, []) +const hasMoreShortcodes = useReadonlyStream(adapter.hasMoreShortcodes$, true) + +const loading = computed( + () => adapterLoading.value && myShortcodes.value.length === 0 +) + +onLoggedIn(() => { + adapter.initialize() +}) + +const deleteShortcode = (codeID: string) => { + pipe( + backendDeleteShortcode(codeID), + TE.match( + (err: GQLError) => { + toast.error(`${getErrorMessage(err)}`) + }, + () => { + toast.success(`${t("shortcodes.deleted")}`) + } + ) + )() +} + +const loadMoreShortcodes = () => { + adapter.loadMore() +} + +const getErrorMessage = (err: GQLError) => { + if (err.type === "network_error") { + return t("error.network_error") + } else { + switch (err.error) { + case "shortcode/not_found": + return t("shortcodes.not_found") + default: + return t("error.something_went_wrong") + } + } +} + useMeta({ title: `${t("navigation.profile")} • Hoppscotch`, }) diff --git a/packages/hoppscotch-app/pages/realtime.vue b/packages/hoppscotch-app/pages/realtime.vue index c4852a0ca..5288ba75d 100644 --- a/packages/hoppscotch-app/pages/realtime.vue +++ b/packages/hoppscotch-app/pages/realtime.vue @@ -1,53 +1,67 @@ - diff --git a/packages/hoppscotch-app/pages/realtime/Mqtt.vue b/packages/hoppscotch-app/pages/realtime/Mqtt.vue new file mode 100644 index 000000000..dd4d63882 --- /dev/null +++ b/packages/hoppscotch-app/pages/realtime/Mqtt.vue @@ -0,0 +1,350 @@ + + + diff --git a/packages/hoppscotch-app/pages/realtime/Socketio.vue b/packages/hoppscotch-app/pages/realtime/Socketio.vue new file mode 100644 index 000000000..cb4a05e1f --- /dev/null +++ b/packages/hoppscotch-app/pages/realtime/Socketio.vue @@ -0,0 +1,437 @@ + + + diff --git a/packages/hoppscotch-app/pages/realtime/Sse.vue b/packages/hoppscotch-app/pages/realtime/Sse.vue new file mode 100644 index 000000000..d9b4a7136 --- /dev/null +++ b/packages/hoppscotch-app/pages/realtime/Sse.vue @@ -0,0 +1,199 @@ + + + diff --git a/packages/hoppscotch-app/pages/realtime/Websocket.vue b/packages/hoppscotch-app/pages/realtime/Websocket.vue new file mode 100644 index 000000000..750c2b7cc --- /dev/null +++ b/packages/hoppscotch-app/pages/realtime/Websocket.vue @@ -0,0 +1,370 @@ + + diff --git a/packages/hoppscotch-app/pages/settings.vue b/packages/hoppscotch-app/pages/settings.vue index da460ad2b..7e482e4c6 100644 --- a/packages/hoppscotch-app/pages/settings.vue +++ b/packages/hoppscotch-app/pages/settings.vue @@ -236,19 +236,17 @@