Compare commits

..

2 Commits

Author SHA1 Message Date
Andrew Bastin
7047289d95 refactor: implement updated equality heuristics for hopprestrequest struct 2022-04-14 20:23:29 +05:30
Nivedin
efbd165c5c feat: unsaved change popup (#2239)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: liyasthomas <liyascthomas@gmail.com>
2022-04-13 20:52:44 +05:30
264 changed files with 5837 additions and 13960 deletions

View File

@@ -1,34 +0,0 @@
name: Deploy to Netlify
on:
push:
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: Setup Environment
run: mv packages/hoppscotch-app/.env.example packages/hoppscotch-app/.env
- name: Build Site
run: pnpm run generate
# Deploy the production site with netlify-cli
- name: Deploy to Netlify (production)
uses: netlify/actions/cli@master
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_PRODUCTION_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
with:
args: deploy --dir=packages/hoppscotch-app/dist --prod

View File

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

View File

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

View File

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

View File

@@ -22,11 +22,11 @@
],
"dependencies": {
"husky": "^7.0.4",
"lint-staged": "^12.3.8"
"lint-staged": "^12.3.7"
},
"devDependencies": {
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@types/node": "^17.0.24"
"@types/node": "^17.0.23"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/codemirror-lang-graphql",
"version": "0.2.0",
"version": "0.1.0",
"description": "GraphQL language support for CodeMirror",
"author": "Hoppscotch (support@hoppscotch.io)",
"license": "MIT",
@@ -24,9 +24,9 @@
"devDependencies": {
"@lezer/generator": "^0.15.4",
"mocha": "^9.2.2",
"rollup": "^2.70.2",
"rollup": "^2.70.1",
"rollup-plugin-dts": "^4.2.1",
"rollup-plugin-ts": "^2.0.7",
"rollup-plugin-ts": "^2.0.5",
"typescript": "^4.6.3"
}
}

View File

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="17" y1="7" x2="7" y2="17"></line>
<polyline points="17 17 7 17 7 7"></polyline>
</svg>

Before

Width:  |  Height:  |  Size: 283 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<polyline points="19 12 12 19 5 12"></polyline>
</svg>

Before

Width:  |  Height:  |  Size: 285 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="7" y1="17" x2="17" y2="7"></line>
<polyline points="7 7 17 7 17 17"></polyline>
</svg>

Before

Width:  |  Height:  |  Size: 283 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>

Before

Width:  |  Height:  |  Size: 284 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="7 13 12 18 17 13"></polyline>
<polyline points="7 6 12 11 17 6"></polyline>
</svg>

Before

Width:  |  Height:  |  Size: 286 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 11 12 6 7 11"></polyline>
<polyline points="17 18 12 13 7 18"></polyline>
</svg>

Before

Width:  |  Height:  |  Size: 287 B

View File

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

Before

Width:  |  Height:  |  Size: 283 B

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>

Before

Width:  |  Height:  |  Size: 329 B

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>

Before

Width:  |  Height:  |  Size: 329 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>

Before

Width:  |  Height:  |  Size: 292 B

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,11 @@
{{ t("settings.interceptor_description") }}
</p>
</div>
<SmartRadioGroup v-model="interceptorSelection" :radios="interceptors" />
<SmartRadioGroup
:radios="interceptors"
:selected="interceptorSelection"
@change="toggleSettingKey"
/>
<div
v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion"
class="flex space-x-2"
@@ -34,29 +38,58 @@
</template>
<script setup lang="ts">
import { computed } from "@nuxtjs/composition-api"
import { applySetting, toggleSetting, useSetting } from "~/newstore/settings"
import { useI18n, useReadonlyStream } from "~/helpers/utils/composables"
import { extensionStatus$ } from "~/newstore/HoppExtension"
import { computed, ref, watchEffect } from "@nuxtjs/composition-api"
import { KeysMatching } from "~/types/ts-utils"
import {
applySetting,
SettingsType,
toggleSetting,
useSetting,
} from "~/newstore/settings"
import { hasExtensionInstalled } from "~/helpers/strategies/ExtensionStrategy"
import { useI18n, usePolled } from "~/helpers/utils/composables"
const t = useI18n()
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
const currentExtensionStatus = useReadonlyStream(extensionStatus$, null)
const toggleSettingKey = <
K extends KeysMatching<SettingsType | "BROWSER_ENABLED", boolean>
>(
key: K
) => {
interceptorSelection.value = key
if (key === "EXTENSIONS_ENABLED") {
applySetting("EXTENSIONS_ENABLED", true)
if (PROXY_ENABLED.value) toggleSetting("PROXY_ENABLED")
}
if (key === "PROXY_ENABLED") {
applySetting("PROXY_ENABLED", true)
if (EXTENSIONS_ENABLED.value) toggleSetting("EXTENSIONS_ENABLED")
}
if (key === "BROWSER_ENABLED") {
applySetting("PROXY_ENABLED", false)
applySetting("EXTENSIONS_ENABLED", false)
}
}
const extensionVersion = computed(() => {
return currentExtensionStatus.value === "available"
? window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() ?? null
const extensionVersion = usePolled(5000, (stopPolling) => {
const result = hasExtensionInstalled()
? window.__POSTWOMAN_EXTENSION_HOOK__.getVersion()
: null
// We don't need to poll anymore after we get value
if (result) stopPolling()
return result
})
const interceptors = computed(() => [
{ value: "BROWSER_ENABLED" as const, label: t("state.none") },
{ value: "PROXY_ENABLED" as const, label: t("settings.proxy") },
{ value: "BROWSER_ENABLED", label: t("state.none") },
{ value: "PROXY_ENABLED", label: t("settings.proxy") },
{
value: "EXTENSIONS_ENABLED" as const,
value: "EXTENSIONS_ENABLED",
label:
`${t("settings.extensions")}: ` +
(extensionVersion.value !== null
@@ -65,27 +98,15 @@ const interceptors = computed(() => [
},
])
type InterceptorMode = typeof interceptors["value"][number]["value"]
const interceptorSelection = ref("")
const interceptorSelection = computed<InterceptorMode>({
get() {
if (PROXY_ENABLED.value) return "PROXY_ENABLED"
if (EXTENSIONS_ENABLED.value) return "EXTENSIONS_ENABLED"
return "BROWSER_ENABLED"
},
set(val) {
if (val === "EXTENSIONS_ENABLED") {
applySetting("EXTENSIONS_ENABLED", true)
if (PROXY_ENABLED.value) toggleSetting("PROXY_ENABLED")
}
if (val === "PROXY_ENABLED") {
applySetting("PROXY_ENABLED", true)
if (EXTENSIONS_ENABLED.value) toggleSetting("EXTENSIONS_ENABLED")
}
if (val === "BROWSER_ENABLED") {
applySetting("PROXY_ENABLED", false)
applySetting("EXTENSIONS_ENABLED", false)
}
},
watchEffect(() => {
if (PROXY_ENABLED.value) {
interceptorSelection.value = "PROXY_ENABLED"
} else if (EXTENSIONS_ENABLED.value) {
interceptorSelection.value = "EXTENSIONS_ENABLED"
} else {
interceptorSelection.value = "BROWSER_ENABLED"
}
})
</script>

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,7 @@
]"
:disabled="disabled"
:tabindex="loading ? '-1' : '0'"
:type="type"
role="button"
>
<span
@@ -66,41 +67,79 @@
</SmartLink>
</template>
<script setup lang="ts">
interface Props {
to: string
exact: boolean
blank: boolean
label: string
icon: string
svg: string
color: string
disabled: boolean
loading: boolean
large: boolean
shadow: boolean
reverse: boolean
rounded: boolean
gradient: boolean
outline: boolean
shortcut: string[]
}
withDefaults(defineProps<Props>(), {
to: "",
exact: true,
blank: false,
label: "",
icon: "",
svg: "",
color: "",
disabled: false,
loading: false,
large: false,
shadow: false,
reverse: false,
rounded: false,
gradient: false,
outline: false,
shortcut: () => [],
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
export default defineComponent({
props: {
to: {
type: String,
default: "",
},
exact: {
type: Boolean,
default: true,
},
blank: {
type: Boolean,
default: false,
},
label: {
type: String,
default: "",
},
icon: {
type: String,
default: "",
},
svg: {
type: String,
default: "",
},
color: {
type: String,
default: "",
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
large: {
type: Boolean,
default: false,
},
shadow: {
type: Boolean,
default: false,
},
reverse: {
type: Boolean,
default: false,
},
rounded: {
type: Boolean,
default: false,
},
gradient: {
type: Boolean,
default: false,
},
outline: {
type: Boolean,
default: false,
},
shortcut: {
type: Array,
default: () => [],
},
type: {
type: String,
default: "button",
},
},
})
</script>

View File

@@ -66,39 +66,71 @@
</SmartLink>
</template>
<script setup lang="ts">
interface Props {
to: string
exact: boolean
blank: boolean
label: string
icon: string
svg: string
color: string
disabled: boolean
loading: boolean
reverse: boolean
rounded: boolean
large: boolean
outline: boolean
shortcut: string[]
filled: boolean
}
withDefaults(defineProps<Props>(), {
to: "",
exact: true,
blank: false,
label: "",
icon: "",
svg: "",
color: "",
disabled: false,
loading: false,
reverse: false,
rounded: false,
large: false,
outline: false,
shortcut: () => [],
filled: false,
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
export default defineComponent({
props: {
to: {
type: String,
default: "",
},
exact: {
type: Boolean,
default: true,
},
blank: {
type: Boolean,
default: false,
},
label: {
type: String,
default: "",
},
icon: {
type: String,
default: "",
},
svg: {
type: String,
default: "",
},
color: {
type: String,
default: "",
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
reverse: {
type: Boolean,
default: false,
},
rounded: {
type: Boolean,
default: false,
},
large: {
type: Boolean,
default: false,
},
outline: {
type: Boolean,
default: false,
},
shortcut: {
type: Array,
default: () => [],
},
filled: {
type: Boolean,
default: false,
},
},
})
</script>

View File

@@ -26,7 +26,6 @@
<span>
<ButtonPrimary
:label="$t('action.save')"
:loading="loadingState"
@click.native="addNewCollection"
/>
<ButtonSecondary
@@ -44,7 +43,6 @@ import { defineComponent } from "@nuxtjs/composition-api"
export default defineComponent({
props: {
show: Boolean,
loadingState: Boolean,
},
data() {
return {
@@ -58,6 +56,7 @@ export default defineComponent({
return
}
this.$emit("submit", this.name)
this.hideModal()
},
hideModal() {
this.name = null

View File

@@ -24,11 +24,7 @@
</template>
<template #footer>
<span>
<ButtonPrimary
:label="$t('action.save')"
:loading="loadingState"
@click.native="addFolder"
/>
<ButtonPrimary :label="$t('action.save')" @click.native="addFolder" />
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
@@ -47,7 +43,6 @@ export default defineComponent({
folder: { type: Object, default: () => {} },
folderPath: { type: String, default: null },
collectionIndex: { type: Number, default: null },
loadingState: Boolean,
},
data() {
return {
@@ -65,6 +60,7 @@ export default defineComponent({
folder: this.folder,
path: this.folderPath || `${this.collectionIndex}`,
})
this.hideModal()
},
hideModal() {
this.name = null

View File

@@ -1,92 +0,0 @@
<template>
<SmartModal
v-if="show"
dialog
:title="$t('request.new')"
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col px-2">
<input
id="selectLabelAddRequest"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addRequest"
/>
<label for="selectLabelAddRequest">{{ $t("action.label") }}</label>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary
:label="$t('action.save')"
:loading="loadingState"
@click.native="addRequest"
/>
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script setup lang="ts">
import { ref, watch } from "@nuxtjs/composition-api"
import { useI18n, useToast } from "~/helpers/utils/composables"
import { getRESTRequest } from "~/newstore/RESTSession"
const toast = useToast()
const t = useI18n()
const props = defineProps<{
show: boolean
loadingState: boolean
folder?: object
folderPath?: string
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
(
e: "add-request",
v: {
name: string
folder: object | undefined
path: string | undefined
}
): void
}>()
const name = ref("")
watch(
() => props.show,
(show) => {
if (show) {
name.value = getRESTRequest().name
}
}
)
const addRequest = () => {
if (!name.value) {
toast.error(`${t("error.empty_req_name")}`)
return
}
emit("add-request", {
name: name.value,
folder: props.folder,
path: props.folderPath,
})
}
const hideModal = () => {
emit("hide-modal")
}
</script>

View File

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

View File

@@ -26,7 +26,6 @@
<span>
<ButtonPrimary
:label="$t('action.save')"
:loading="loadingState"
@click.native="saveCollection"
/>
<ButtonSecondary
@@ -45,7 +44,6 @@ export default defineComponent({
props: {
show: Boolean,
editingCollectionName: { type: String, default: null },
loadingState: Boolean,
},
data() {
return {
@@ -64,6 +62,7 @@ export default defineComponent({
return
}
this.$emit("submit", this.name)
this.hideModal()
},
hideModal() {
this.name = null

View File

@@ -24,11 +24,7 @@
</template>
<template #footer>
<span>
<ButtonPrimary
:label="$t('action.save')"
:loading="loadingState"
@click.native="editFolder"
/>
<ButtonPrimary :label="$t('action.save')" @click.native="editFolder" />
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
@@ -45,7 +41,6 @@ export default defineComponent({
props: {
show: Boolean,
editingFolderName: { type: String, default: null },
loadingState: Boolean,
},
data() {
return {
@@ -64,6 +59,7 @@ export default defineComponent({
return
}
this.$emit("submit", this.name)
this.hideModal()
},
hideModal() {
this.name = null

View File

@@ -24,11 +24,7 @@
</template>
<template #footer>
<span>
<ButtonPrimary
:label="$t('action.save')"
:loading="loadingState"
@click.native="saveRequest"
/>
<ButtonPrimary :label="$t('action.save')" @click.native="saveRequest" />
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
@@ -45,7 +41,6 @@ export default defineComponent({
props: {
show: Boolean,
editingRequestName: { type: String, default: null },
loadingState: Boolean,
},
data() {
return {
@@ -66,6 +61,7 @@ export default defineComponent({
return
}
this.$emit("submit", this.requestUpdateData)
this.hideModal()
},
hideModal() {
this.requestUpdateData = { name: null }

View File

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

View File

@@ -233,7 +233,6 @@ const saveRequestAs = async () => {
originLocation: "user-collection",
folderPath: picked.value.folderPath,
requestIndex: picked.value.requestIndex,
req: cloneDeep(requestUpdated),
})
requestSaved()
@@ -250,7 +249,6 @@ const saveRequestAs = async () => {
originLocation: "user-collection",
folderPath: picked.value.folderPath,
requestIndex: insertionIndex,
req: cloneDeep(requestUpdated),
})
requestSaved()
@@ -267,7 +265,6 @@ const saveRequestAs = async () => {
originLocation: "user-collection",
folderPath: `${picked.value.collectionIndex}`,
requestIndex: insertionIndex,
req: cloneDeep(requestUpdated),
})
requestSaved()
@@ -296,7 +293,6 @@ const saveRequestAs = async () => {
setRESTSaveContext({
originLocation: "team-collection",
requestID: picked.value.requestID,
req: cloneDeep(requestUpdated),
})
} else if (picked.value.pickedType === "teams-folder") {
if (!isHoppRESTRequest(requestUpdated))
@@ -323,7 +319,6 @@ const saveRequestAs = async () => {
requestID: result.right.createRequestInCollection.id,
teamID: collectionsType.value.selectedTeam.id,
collectionID: picked.value.folderID,
req: cloneDeep(requestUpdated),
})
requestSaved()
@@ -353,7 +348,6 @@ const saveRequestAs = async () => {
requestID: result.right.createRequestInCollection.id,
teamID: collectionsType.value.selectedTeam.id,
collectionID: picked.value.collectionID,
req: cloneDeep(requestUpdated),
})
requestSaved()

View File

@@ -50,16 +50,12 @@ export default defineComponent({
},
methods: {
addFolder() {
if (!this.name) {
this.$toast.error(`${this.$t("folder.name_length_insufficient")}`)
return
}
// TODO: Blocking when name is null ?
this.$emit("add-folder", {
name: this.name,
path: this.folderPath || `${this.collectionIndex}`,
})
this.hideModal()
},
hideModal() {

View File

@@ -1,87 +0,0 @@
<template>
<SmartModal
v-if="show"
dialog
:title="$t('request.new')"
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col px-2">
<input
id="selectLabelGqlAddRequest"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addRequest"
/>
<label for="selectLabelGqlAddRequest">
{{ $t("action.label") }}
</label>
</div>
</template>
<template #footer>
<span>
<ButtonPrimary :label="$t('action.save')" @click.native="addRequest" />
<ButtonSecondary
:label="$t('action.cancel')"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
<script setup lang="ts">
import { ref, watch } from "@nuxtjs/composition-api"
import { useI18n, useToast } from "~/helpers/utils/composables"
import { getGQLSession } from "~/newstore/GQLSession"
const toast = useToast()
const t = useI18n()
const props = defineProps<{
show: boolean
folderPath?: string
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
(
e: "add-request",
v: {
name: string
path: string | undefined
}
): void
}>()
const name = ref("")
watch(
() => props.show,
(show) => {
if (show) {
name.value = getGQLSession().request.name
}
}
)
const addRequest = () => {
if (!name.value) {
toast.error(`${t("error.empty_req_name")}`)
return
}
emit("add-request", {
name: name.value,
path: props.folderPath,
})
hideModal()
}
const hideModal = () => {
emit("hide-modal")
}
</script>

View File

@@ -29,17 +29,6 @@
</span>
</span>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="file-plus"
:title="$t('request.new')"
class="hidden group-hover:inline-flex"
@click.native="
$emit('add-request', {
path: `${collectionIndex}`,
})
"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="folder-plus"
@@ -72,26 +61,11 @@
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="requestAction"
svg="file-plus"
:label="`${$t('request.new')}`"
:shortcut="['R']"
@click.native="
() => {
$emit('add-request', {
path: `${collectionIndex}`,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="folderAction"
svg="folder-plus"
@@ -152,7 +126,6 @@
:collection-index="collectionIndex"
:doc="doc"
:is-filtered="isFiltered"
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@@ -223,7 +196,6 @@ export default defineComponent({
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
requestAction: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),

View File

@@ -29,13 +29,6 @@
</span>
</span>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="file-plus"
:title="$t('request.new')"
class="hidden group-hover:inline-flex"
@click.native="$emit('add-request', { path: folderPath })"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="folder-plus"
@@ -64,24 +57,11 @@
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="requestAction"
svg="file-plus"
:label="`${$t('request.new')}`"
:shortcut="['R']"
@click.native="
() => {
$emit('add-request', { path: folderPath })
options.tippy().hide()
}
"
/>
<SmartItem
ref="folderAction"
svg="folder-plus"
@@ -140,7 +120,6 @@
:collection-index="collectionIndex"
:doc="doc"
:is-filtered="isFiltered"
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@@ -214,7 +193,6 @@ export default defineComponent({
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
requestAction: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),

View File

@@ -49,7 +49,6 @@
:is-filtered="filterText.length > 0"
:saving-mode="savingMode"
@edit-collection="editCollection(collection, index)"
@add-request="addRequest($event)"
@add-folder="addFolder($event)"
@edit-folder="editFolder($event)"
@edit-request="editRequest($event)"
@@ -97,12 +96,6 @@
:editing-collection-name="editingCollection ? editingCollection.name : ''"
@hide-modal="displayModalEdit(false)"
/>
<CollectionsGraphqlAddRequest
:show="showModalAddRequest"
:folder-path="editingFolderPath"
@add-request="onAddRequest($event)"
@hide-modal="displayModalAddRequest(false)"
/>
<CollectionsGraphqlAddFolder
:show="showModalAddFolder"
:folder-path="editingFolderPath"
@@ -143,7 +136,6 @@ import {
addGraphqlFolder,
saveGraphqlRequestAs,
} from "~/newstore/collections"
import { getGQLSession, setGQLSession } from "~/newstore/GQLSession"
export default defineComponent({
props: {
@@ -164,7 +156,6 @@ export default defineComponent({
showModalAdd: false,
showModalEdit: false,
showModalImportExport: false,
showModalAddRequest: false,
showModalAddFolder: false,
showModalEditFolder: false,
showModalEditRequest: false,
@@ -231,11 +222,6 @@ export default defineComponent({
displayModalImportExport(shouldDisplay) {
this.showModalImportExport = shouldDisplay
},
displayModalAddRequest(shouldDisplay) {
this.showModalAddRequest = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalAddFolder(shouldDisplay) {
this.showModalAddFolder = shouldDisplay
@@ -256,26 +242,6 @@ export default defineComponent({
this.$data.editingCollectionIndex = collectionIndex
this.displayModalEdit(true)
},
onAddRequest({ name, path }) {
const newRequest = {
...getGQLSession().request,
name,
}
saveGraphqlRequestAs(path, newRequest)
setGQLSession({
request: newRequest,
schema: "",
response: "",
})
this.displayModalAddRequest(false)
},
addRequest(payload) {
const { path } = payload
this.$data.editingFolderPath = path
this.displayModalAddRequest(true)
},
onAddFolder({ name, path }) {
addGraphqlFolder(name, path)
this.displayModalAddFolder(false)

View File

@@ -11,7 +11,6 @@
autocomplete="off"
:placeholder="$t('action.search')"
class="py-2 pl-4 pr-2 bg-transparent"
:disabled="collectionsType.type == 'team-collections'"
/>
</div>
<CollectionsChooseType
@@ -83,7 +82,6 @@
:picked="picked"
:loading-collection-i-ds="loadingCollectionIDs"
@edit-collection="editCollection(collection, index)"
@add-request="addRequest($event)"
@add-folder="addFolder($event)"
@edit-folder="editFolder($event)"
@edit-request="editRequest($event)"
@@ -95,7 +93,6 @@
@expand-collection="expandCollection"
@remove-collection="removeCollection"
@remove-request="removeRequest"
@remove-folder="removeFolder"
/>
</div>
<div
@@ -149,7 +146,6 @@
</div>
<CollectionsAdd
:show="showModalAdd"
:loading-state="modalLoadingState"
@submit="addNewRootCollection"
@hide-modal="displayModalAdd(false)"
/>
@@ -160,23 +156,13 @@
? editingCollection.name || editingCollection.title
: ''
"
:loading-state="modalLoadingState"
@hide-modal="displayModalEdit(false)"
@submit="updateEditingCollection"
/>
<CollectionsAddRequest
:show="showModalAddRequest"
:folder="editingFolder"
:folder-path="editingFolderPath"
:loading-state="modalLoadingState"
@add-request="onAddRequest($event)"
@hide-modal="displayModalAddRequest(false)"
/>
<CollectionsAddFolder
:show="showModalAddFolder"
:folder="editingFolder"
:folder-path="editingFolderPath"
:loading-state="modalLoadingState"
@add-folder="onAddFolder($event)"
@hide-modal="displayModalAddFolder(false)"
/>
@@ -185,14 +171,12 @@
:editing-folder-name="
editingFolder ? editingFolder.name || editingFolder.title : ''
"
:loading-state="modalLoadingState"
@submit="updateEditingFolder"
@hide-modal="displayModalEditFolder(false)"
/>
<CollectionsEditRequest
:show="showModalEditRequest"
:editing-request-name="editingRequest ? editingRequest.name : ''"
:loading-state="modalLoadingState"
@submit="updateEditingRequest"
@hide-modal="displayModalEditRequest(false)"
/>
@@ -202,13 +186,6 @@
@hide-modal="displayModalImportExport(false)"
@update-team-collections="updateTeamCollections"
/>
<SmartConfirmModal
:show="showConfirmModal"
:title="confirmModalTitle"
:loading-state="modalLoadingState"
@hide-modal="showConfirmModal = false"
@resolve="resolveConfirmModal"
/>
</div>
</template>
@@ -227,17 +204,11 @@ import {
editRESTCollection,
addRESTFolder,
removeRESTCollection,
removeRESTFolder,
editRESTFolder,
removeRESTRequest,
editRESTRequest,
saveRESTRequestAs,
} from "~/newstore/collections"
import {
setRESTRequest,
getRESTRequest,
getRESTSaveContext,
} from "~/newstore/RESTSession"
import {
useReadonlyStream,
useStreamSubscriber,
@@ -279,22 +250,17 @@ export default defineComponent({
showModalAdd: false,
showModalEdit: false,
showModalImportExport: false,
showModalAddRequest: false,
showModalAddFolder: false,
showModalEditFolder: false,
showModalEditRequest: false,
showConfirmModal: false,
modalLoadingState: false,
editingCollection: undefined,
editingCollectionIndex: undefined,
editingCollectionID: undefined,
editingFolder: undefined,
editingFolderName: undefined,
editingFolderIndex: undefined,
editingFolderPath: undefined,
editingRequest: undefined,
editingRequestIndex: undefined,
confirmModalTitle: undefined,
filterText: "",
collectionsType: {
type: "my-collections",
@@ -409,18 +375,14 @@ export default defineComponent({
requests: [],
})
)
this.displayModalAdd(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
runMutation(CreateNewRootCollectionDocument, {
title: name,
teamID: this.collectionsType.selectedTeam.id,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
if (result.left.error === "team_coll/short_title")
this.$toast.error(this.$t("collection.name_length_insufficient"))
@@ -428,10 +390,10 @@ export default defineComponent({
console.error(result.left.error)
} else {
this.$toast.success(this.$t("collection.created"))
this.displayModalAdd(false)
}
})
}
this.displayModalAdd(false)
},
// Intented to be called by CollectionEdit modal submit event
updateEditingCollection(newName) {
@@ -446,107 +408,66 @@ export default defineComponent({
}
editRESTCollection(this.editingCollectionIndex, collectionUpdated)
this.displayModalEdit(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
runMutation(RenameCollectionDocument, {
collectionID: this.editingCollection.id,
newTitle: newName,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.$toast.error(this.$t("error.something_went_wrong"))
console.error(result.left.error)
console.error(e)
} else {
this.$toast.success(this.$t("collection.renamed"))
this.displayModalEdit(false)
}
})
}
this.displayModalEdit(false)
},
// Intended to be called by CollectionEditFolder modal submit event
updateEditingFolder(name) {
if (this.collectionsType.type === "my-collections") {
editRESTFolder(this.editingFolderPath, { ...this.editingFolder, name })
this.displayModalEditFolder(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
runMutation(RenameCollectionDocument, {
collectionID: this.editingFolder.id,
newTitle: name,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
if (result.left.error === "team_coll/short_title")
this.$toast.error(this.$t("folder.name_length_insufficient"))
else this.$toast.error(this.$t("error.something_went_wrong"))
console.error(result.left.error)
this.$toast.error(this.$t("error.something_went_wrong"))
console.error(e)
} else {
this.$toast.success(this.$t("folder.renamed"))
this.displayModalEditFolder(false)
}
})
}
this.displayModalEditFolder(false)
},
// Intented to by called by CollectionsEditRequest modal submit event
updateEditingRequest(requestUpdateData) {
const saveCtx = getRESTSaveContext()
const requestUpdated = {
...this.editingRequest,
name: requestUpdateData.name || this.editingRequest.name,
}
if (this.collectionsType.type === "my-collections") {
// Update REST Session with the updated state
if (
saveCtx &&
saveCtx.originLocation === "user-collection" &&
saveCtx.requestIndex === this.editingRequestIndex &&
saveCtx.folderPath === this.editingFolderPath
) {
setRESTRequest({
...getRESTRequest(),
name: requestUpdateData.name,
})
}
editRESTRequest(
this.editingFolderPath,
this.editingRequestIndex,
requestUpdated
)
this.displayModalEditRequest(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
const requestName = requestUpdateData.name || this.editingRequest.name
// Update REST Session with the updated state
if (
saveCtx &&
saveCtx.originLocation === "team-collection" &&
saveCtx.requestID === this.editingRequestIndex
) {
setRESTRequest({
...getRESTRequest(),
name: requestUpdateData.name,
})
}
runMutation(UpdateRequestDocument, {
data: {
request: JSON.stringify(requestUpdated),
@@ -554,18 +475,17 @@ export default defineComponent({
},
requestID: this.editingRequestIndex,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.$toast.error(this.$t("error.something_went_wrong"))
console.error(result.left.error)
console.error(e)
} else {
this.$toast.success(this.$t("request.renamed"))
this.$emit("update-team-collections")
this.displayModalEditRequest(false)
}
})
}
this.displayModalEditRequest(false)
},
displayModalAdd(shouldDisplay) {
this.showModalAdd = shouldDisplay
@@ -578,11 +498,6 @@ export default defineComponent({
displayModalImportExport(shouldDisplay) {
this.showModalImportExport = shouldDisplay
},
displayModalAddRequest(shouldDisplay) {
this.showModalAddRequest = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalAddFolder(shouldDisplay) {
this.showModalAddFolder = shouldDisplay
@@ -598,11 +513,6 @@ export default defineComponent({
if (!shouldDisplay) this.resetSelectedData()
},
displayConfirmModal(shouldDisplay) {
this.showConfirmModal = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
editCollection(collection, collectionIndex) {
this.$data.editingCollection = collection
this.$data.editingCollectionIndex = collectionIndex
@@ -611,29 +521,26 @@ export default defineComponent({
onAddFolder({ name, folder, path }) {
if (this.collectionsType.type === "my-collections") {
addRESTFolder(name, path)
this.displayModalAddFolder(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
runMutation(CreateChildCollectionDocument, {
childTitle: name,
collectionID: folder.id,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
if (result.left.error === "team_coll/short_title")
this.$toast.error(this.$t("folder.name_length_insufficient"))
else this.$toast.error(this.$t("error.something_went_wrong"))
console.error(result.left.error)
} else {
this.$toast.success(this.$t("folder.created"))
this.displayModalAddFolder(false)
this.$emit("update-team-collections")
}
})
} else if (this.collectionsType.type === "team-collections") {
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
runMutation(CreateChildCollectionDocument, {
childTitle: name,
collectionID: folder.id,
})().then((result) => {
if (E.isLeft(result)) {
if (result.left.error === "team_coll/short_title")
this.$toast.error(this.$t("folder.name_length_insufficient"))
else this.$toast.error(this.$t("error.something_went_wrong"))
console.error(result.left.error)
} else {
this.$toast.success(this.$t("folder.created"))
this.$emit("update-team-collections")
}
})
}
}
this.displayModalAddFolder(false)
},
addFolder(payload) {
const { folder, path } = payload
@@ -671,30 +578,16 @@ export default defineComponent({
resetSelectedData() {
this.$data.editingCollection = undefined
this.$data.editingCollectionIndex = undefined
this.$data.editingCollectionID = undefined
this.$data.editingFolder = undefined
this.$data.editingFolderPath = undefined
this.$data.editingFolderIndex = undefined
this.$data.editingRequest = undefined
this.$data.editingRequestIndex = undefined
this.$data.confirmModalTitle = undefined
},
expandCollection(collectionID) {
this.teamCollectionAdapter.expandCollection(collectionID)
},
removeCollection({ collectionIndex, collectionID }) {
this.$data.editingCollectionIndex = collectionIndex
this.$data.editingCollectionID = collectionID
this.confirmModalTitle = `${this.$t("confirm.remove_collection")}`
this.displayConfirmModal(true)
},
onRemoveCollection() {
const collectionIndex = this.$data.editingCollectionIndex
const collectionID = this.$data.editingCollectionID
if (this.collectionsType.type === "my-collections") {
removeCollection({ collectionsType, collectionIndex, collectionID }) {
if (collectionsType.type === "my-collections") {
// Cancel pick if picked collection is deleted
if (
this.picked &&
@@ -705,12 +598,8 @@ export default defineComponent({
}
removeRESTCollection(collectionIndex)
this.$toast.success(this.$t("state.deleted"))
this.displayConfirmModal(false)
} else if (this.collectionsType.type === "team-collections") {
this.modalLoadingState = true
} else if (collectionsType.type === "team-collections") {
// Cancel pick if picked collection is deleted
if (
this.picked &&
@@ -720,89 +609,21 @@ export default defineComponent({
this.$emit("select", { picked: null })
}
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
if (collectionsType.selectedTeam.myRole !== "VIEWER") {
runMutation(DeleteCollectionDocument, {
collectionID,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.$toast.error(this.$t("error.something_went_wrong"))
console.error(result.left.error)
console.error(e)
} else {
this.$toast.success(this.$t("state.deleted"))
this.displayConfirmModal(false)
}
})
}
}
},
removeFolder({ collectionID, folder, folderPath }) {
this.$data.editingCollectionID = collectionID
this.$data.editingFolder = folder
this.$data.editingFolderPath = folderPath
this.confirmModalTitle = `${this.$t("confirm.remove_folder")}`
this.displayConfirmModal(true)
},
onRemoveFolder() {
const folder = this.$data.editingFolder
const folderPath = this.$data.editingFolderPath
if (this.collectionsType.type === "my-collections") {
// Cancel pick if picked folder was deleted
if (
this.picked &&
this.picked.pickedType === "my-folder" &&
this.picked.folderPath === folderPath
) {
this.$emit("select", { picked: null })
}
removeRESTFolder(folderPath)
this.$toast.success(this.$t("state.deleted"))
this.displayConfirmModal(false)
} else if (this.collectionsType.type === "team-collections") {
this.modalLoadingState = true
// Cancel pick if picked collection folder was deleted
if (
this.picked &&
this.picked.pickedType === "teams-folder" &&
this.picked.folderID === folder.id
) {
this.$emit("select", { picked: null })
}
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
runMutation(DeleteCollectionDocument, {
collectionID: folder.id,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
console.error(result.left.error)
} else {
this.$toast.success(`${this.$t("state.deleted")}`)
this.displayConfirmModal(false)
this.updateTeamCollections()
}
})
}
}
},
removeRequest({ requestIndex, folderPath }) {
this.$data.editingRequestIndex = requestIndex
this.$data.editingFolderPath = folderPath
this.confirmModalTitle = `${this.$t("confirm.remove_request")}`
this.displayConfirmModal(true)
},
onRemoveRequest() {
const requestIndex = this.$data.editingRequestIndex
const folderPath = this.$data.editingFolderPath
if (this.collectionsType.type === "my-collections") {
// Cancel pick if the picked item is being deleted
if (
@@ -814,11 +635,8 @@ export default defineComponent({
this.$emit("select", { picked: null })
}
removeRESTRequest(folderPath, requestIndex)
this.$toast.success(this.$t("state.deleted"))
this.displayConfirmModal(false)
} else if (this.collectionsType.type === "team-collections") {
this.modalLoadingState = true
// Cancel pick if the picked item is being deleted
if (
this.picked &&
@@ -831,68 +649,11 @@ export default defineComponent({
runMutation(DeleteRequestDocument, {
requestID: requestIndex,
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.$toast.error(this.$t("error.something_went_wrong"))
console.error(result.left.error)
console.error(e)
} else {
this.$toast.success(this.$t("state.deleted"))
this.displayConfirmModal(false)
}
})
}
},
addRequest(payload) {
// TODO: check if the request being worked on
// is being overwritten (selected or not)
const { folder, path } = payload
this.$data.editingFolder = folder
this.$data.editingFolderPath = path
this.displayModalAddRequest(true)
},
onAddRequest({ name, folder, path }) {
const newRequest = {
...cloneDeep(getRESTRequest()),
name,
}
if (this.collectionsType.type === "my-collections") {
const insertionIndex = saveRESTRequestAs(path, newRequest)
// point to it
setRESTRequest(newRequest, {
originLocation: "user-collection",
folderPath: path,
requestIndex: insertionIndex,
})
this.displayModalAddRequest(false)
} else if (
this.collectionsType.type === "team-collections" &&
this.collectionsType.selectedTeam.myRole !== "VIEWER"
) {
this.modalLoadingState = true
runMutation(CreateRequestInCollectionDocument, {
collectionID: folder.id,
data: {
request: JSON.stringify(newRequest),
teamID: this.collectionsType.selectedTeam.id,
title: name,
},
})().then((result) => {
this.modalLoadingState = false
if (E.isLeft(result)) {
this.$toast.error(this.$t("error.something_went_wrong"))
console.error(result.left.error)
} else {
const { createRequestInCollection } = result.right
// point to it
setRESTRequest(newRequest, {
originLocation: "team-collection",
requestID: createRequestInCollection.id,
collectionID: createRequestInCollection.collection.id,
teamID: createRequestInCollection.collection.team.id,
})
this.displayModalAddRequest(false)
}
})
}
@@ -920,22 +681,6 @@ export default defineComponent({
})
}
},
resolveConfirmModal(title) {
if (title === `${this.$t("confirm.remove_collection")}`)
this.onRemoveCollection()
else if (title === `${this.$t("confirm.remove_request")}`)
this.onRemoveRequest()
else if (title === `${this.$t("confirm.remove_folder")}`)
this.onRemoveFolder()
else {
console.error(
`Confirm modal title ${title} is not handled by the component`
)
this.$toast.error(this.$t("error.something_went_wrong"))
this.displayConfirmModal(false)
}
},
},
})
// request inside folder is not being deleted, you dumb fuck
</script>

View File

@@ -45,18 +45,6 @@
color="green"
@click.native="$emit('unselect-collection')"
/>
<ButtonSecondary
v-if="!doc"
v-tippy="{ theme: 'tooltip' }"
svg="file-plus"
:title="$t('request.new')"
class="hidden group-hover:inline-flex"
@click.native="
$emit('add-request', {
path: `${collectionIndex}`,
})
"
/>
<ButtonSecondary
v-if="!doc"
v-tippy="{ theme: 'tooltip' }"
@@ -91,27 +79,12 @@
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.x="exportAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="requestAction"
svg="file-plus"
:label="$t('request.new')"
:shortcut="['R']"
@click.native="
() => {
$emit('add-request', {
path: `${collectionIndex}`,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="folderAction"
svg="folder-plus"
@@ -158,7 +131,7 @@
:shortcut="['⌫']"
@click.native="
() => {
removeCollection()
confirmRemove = true
options.tippy().hide()
}
"
@@ -186,14 +159,12 @@
:collections-type="collectionsType"
:is-filtered="isFiltered"
:picked="picked"
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
@remove-folder="$emit('remove-folder', $event)"
/>
<CollectionsMyRequest
v-for="(request, index) in collection.requests"
@@ -234,6 +205,12 @@
</div>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('confirm.remove_collection')"
@hide-modal="confirmRemove = false"
@resolve="removeCollection"
/>
</div>
</template>
@@ -256,7 +233,6 @@ export default defineComponent({
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
requestAction: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
@@ -268,6 +244,7 @@ export default defineComponent({
showChildren: false,
dragging: false,
selectedFolder: {},
confirmRemove: false,
prevCursor: "",
cursor: "",
pageNo: 0,
@@ -320,6 +297,7 @@ export default defineComponent({
},
removeCollection() {
this.$emit("remove-collection", {
collectionsType: this.collectionsType,
collectionIndex: this.collectionIndex,
collectionID: this.collection.id,
})

View File

@@ -29,13 +29,6 @@
</span>
</span>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="file-plus"
:title="t('request.new')"
class="hidden group-hover:inline-flex"
@click.native="$emit('add-request', { path: folderPath })"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="folder-plus"
@@ -64,25 +57,12 @@
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.x="exportAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="requestAction"
svg="file-plus"
:label="$t('request.new')"
:shortcut="['R']"
@click.native="
() => {
$emit('add-request', { path: folderPath })
options.tippy().hide()
}
"
/>
<SmartItem
ref="folderAction"
svg="folder-plus"
@@ -131,7 +111,7 @@
:shortcut="['⌫']"
@click.native="
() => {
removeFolder()
confirmRemove = true
options.tippy().hide()
}
"
@@ -158,15 +138,13 @@
:collections-type="collectionsType"
:folder-path="`${folderPath}/${subFolderIndex}`"
:picked="picked"
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@update-team-collections="$emit('update-team-collections')"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
@remove-folder="$emit('remove-folder', $event)"
@remove-request="removeRequest"
/>
<CollectionsMyRequest
v-for="(request, index) in folder.requests"
@@ -184,7 +162,7 @@
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
@remove-request="removeRequest"
/>
<div
v-if="
@@ -207,13 +185,23 @@
</div>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_folder')"
@hide-modal="confirmRemove = false"
@resolve="removeFolder"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api"
import { useI18n } from "~/helpers/utils/composables"
import { moveRESTRequest } from "~/newstore/collections"
import {
removeRESTFolder,
removeRESTRequest,
moveRESTRequest,
} from "~/newstore/collections"
export default defineComponent({
name: "Folder",
@@ -234,7 +222,6 @@ export default defineComponent({
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
requestAction: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
@@ -246,6 +233,7 @@ export default defineComponent({
return {
showChildren: false,
dragging: false,
confirmRemove: false,
prevCursor: "",
cursor: "",
}
@@ -296,10 +284,17 @@ export default defineComponent({
this.showChildren = !this.showChildren
},
removeFolder() {
this.$emit("remove-folder", {
folder: this.folder,
folderPath: this.folderPath,
})
// TODO: Bubble it up ?
// Cancel pick if picked folder was deleted
if (
this.picked &&
this.picked.pickedType === "my-folder" &&
this.picked.folderPath === this.folderPath
) {
this.$emit("select", { picked: null })
}
removeRESTFolder(this.folderPath)
this.$toast.success(`${this.$t("state.deleted")}`)
},
dropEvent({ dataTransfer }) {
this.dragging = !this.dragging
@@ -307,6 +302,19 @@ export default defineComponent({
const requestIndex = dataTransfer.getData("requestIndex")
moveRESTRequest(folderPath, requestIndex, this.folderPath)
},
removeRequest({ requestIndex }) {
// TODO: Bubble it up to root ?
// Cancel pick if the picked item is being deleted
if (
this.picked &&
this.picked.pickedType === "my-request" &&
this.picked.folderPath === this.folderPath &&
this.picked.requestIndex === requestIndex
) {
this.$emit("select", { picked: null })
}
removeRESTRequest(this.folderPath, requestIndex)
},
},
})
</script>

View File

@@ -126,7 +126,7 @@
:shortcut="['⌫']"
@click.native="
() => {
removeRequest()
confirmRemove = true
options.tippy().hide()
}
"
@@ -136,6 +136,12 @@
</span>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_request')"
@hide-modal="confirmRemove = false"
@resolve="removeRequest"
/>
<HttpReqChangeConfirmModal
:show="confirmChange"
@hide-modal="confirmChange = false"
@@ -159,7 +165,6 @@ import {
isEqualHoppRESTRequest,
} from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import cloneDeep from "lodash/cloneDeep"
import {
useI18n,
useToast,
@@ -216,6 +221,8 @@ const emit = defineEmits<{
(
e: "remove-request",
data: {
collectionIndex: number
folderName: string
folderPath: string
requestIndex: number
}
@@ -257,6 +264,7 @@ const requestMethodLabels = {
delete: "text-red-500",
default: "text-gray-500",
}
const confirmRemove = ref(false)
const confirmChange = ref(false)
const showSaveRequestModal = ref(false)
@@ -295,6 +303,8 @@ const dragStart = ({ dataTransfer }: DragEvent) => {
const removeRequest = () => {
emit("remove-request", {
collectionIndex: props.collectionIndex,
folderName: props.folderName,
folderPath: props.folderPath,
requestIndex: props.requestIndex,
})
@@ -307,46 +317,33 @@ const getRequestLabelColor = (method: string) =>
const setRestReq = (request: any) => {
setRESTRequest(
cloneDeep(
safelyExtractRESTRequest(
translateToNewRequest(request),
getDefaultRESTRequest()
)
safelyExtractRESTRequest(
translateToNewRequest(request),
getDefaultRESTRequest()
),
{
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
req: cloneDeep(request),
req: request,
}
)
}
/** Loads request from the save once, checks for unsaved changes, but ignores default values */
const selectRequest = () => {
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
if (props.saveRequest) {
emit("select", {
picked: {
pickedType: "my-request",
collectionIndex: props.collectionIndex,
folderPath: props.folderPath,
folderName: props.folderName,
requestIndex: props.requestIndex,
},
})
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
confirmChange.value = false
setRestReq(props.request)
} else if (!active.value) {
// If the current request is the same as the request to be loaded in, there is no data loss
const currentReq = getRESTRequest()
if (!active.value) {
confirmChange.value = true
if (isEqualHoppRESTRequest(currentReq, props.request)) {
setRestReq(props.request)
} else {
confirmChange.value = true
}
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "my-request",
collectionIndex: props.collectionIndex,
folderPath: props.folderPath,
folderName: props.folderName,
requestIndex: props.requestIndex,
},
})
} else {
const currentReqWithNoChange = active.value.req
const currentFullReq = getRESTRequest()
@@ -356,6 +353,16 @@ const selectRequest = () => {
// Check if there is any changes done on the current request
if (isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)) {
setRestReq(props.request)
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "my-request",
collectionIndex: props.collectionIndex,
folderPath: props.folderPath,
folderName: props.folderName,
requestIndex: props.requestIndex,
},
})
} else {
confirmChange.value = true
}
@@ -375,12 +382,22 @@ const saveRequestChange = () => {
/** Discard changes and change the current request and context */
const discardRequestChange = () => {
setRestReq(props.request)
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "my-request",
collectionIndex: props.collectionIndex,
folderPath: props.folderPath,
folderName: props.folderName,
requestIndex: props.requestIndex,
},
})
if (!isActive.value) {
setRESTSaveContext({
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
req: cloneDeep(props.request),
req: props.request,
})
}

View File

@@ -45,19 +45,6 @@
color="green"
@click.native="$emit('unselect-collection')"
/>
<ButtonSecondary
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
svg="file-plus"
:title="$t('request.new')"
class="hidden group-hover:inline-flex"
@click.native="
$emit('add-request', {
folder: collection,
path: `${collectionIndex}`,
})
"
/>
<ButtonSecondary
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
@@ -93,28 +80,12 @@
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.x="exportAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="requestAction"
svg="file-plus"
:label="t('request.new')"
:shortcut="['R']"
@click.native="
() => {
$emit('add-request', {
folder: collection,
path: `${collectionIndex}`,
})
options.tippy().hide()
}
"
/>
<SmartItem
ref="folderAction"
svg="folder-plus"
@@ -157,7 +128,7 @@
:shortcut="['⌫']"
@click.native="
() => {
removeCollection()
confirmRemove = true
options.tippy().hide()
}
"
@@ -186,14 +157,12 @@
:is-filtered="isFiltered"
:picked="picked"
:loading-collection-i-ds="loadingCollectionIDs"
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
@expand-collection="expandCollection"
@remove-request="$emit('remove-request', $event)"
@remove-folder="$emit('remove-folder', $event)"
@remove-request="removeRequest"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<CollectionsTeamsRequest
@@ -211,7 +180,7 @@
:picked="picked"
@edit-request="editRequest($event)"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
@remove-request="removeRequest"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<div
@@ -242,6 +211,12 @@
</div>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_collection')"
@hide-modal="confirmRemove = false"
@resolve="removeCollection"
/>
</div>
</template>
@@ -273,7 +248,6 @@ export default defineComponent({
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
requestAction: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
@@ -287,6 +261,7 @@ export default defineComponent({
showChildren: false,
dragging: false,
selectedFolder: {},
confirmRemove: false,
prevCursor: "",
cursor: "",
pageNo: 0,
@@ -369,6 +344,7 @@ export default defineComponent({
},
removeCollection() {
this.$emit("remove-collection", {
collectionsType: this.collectionsType,
collectionIndex: this.collectionIndex,
collectionID: this.collection.id,
})
@@ -386,6 +362,13 @@ export default defineComponent({
if (E.isLeft(moveRequestResult))
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
},
removeRequest({ collectionIndex, folderName, requestIndex }: any) {
this.$emit("remove-request", {
collectionIndex,
folderName,
requestIndex,
})
},
},
})
</script>

View File

@@ -29,14 +29,6 @@
</span>
</span>
<div class="flex">
<ButtonSecondary
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
svg="file-plus"
:title="$t('request.new')"
class="hidden group-hover:inline-flex"
@click.native="$emit('add-request', { folder, path: folderPath })"
/>
<ButtonSecondary
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
@@ -67,25 +59,12 @@
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.delete="deleteAction.$el.click()"
@keyup.x="exportAction.$el.click()"
@keyup.escape="options.tippy().hide()"
>
<SmartItem
ref="requestAction"
svg="file-plus"
:label="$t('request.new')"
:shortcut="['R']"
@click.native="
() => {
$emit('add-request', { folder, path: folderPath })
options.tippy().hide()
}
"
/>
<SmartItem
ref="folderAction"
svg="folder-plus"
@@ -130,7 +109,7 @@
:shortcut="['⌫']"
@click.native="
() => {
removeFolder()
confirmRemove = true
options.tippy().hide()
}
"
@@ -158,15 +137,13 @@
:folder-path="`${folderPath}/${subFolderIndex}`"
:picked="picked"
:loading-collection-i-ds="loadingCollectionIDs"
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@update-team-collections="$emit('update-team-collections')"
@select="$emit('select', $event)"
@expand-collection="expandCollection"
@remove-request="$emit('remove-request', $event)"
@remove-folder="$emit('remove-folder', $event)"
@remove-request="removeRequest"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<CollectionsTeamsRequest
@@ -184,7 +161,7 @@
:collection-i-d="folder.id"
@edit-request="$emit('edit-request', $event)"
@select="$emit('select', $event)"
@remove-request="$emit('remove-request', $event)"
@remove-request="removeRequest"
@duplicate-request="$emit('duplicate-request', $event)"
/>
<div
@@ -213,12 +190,20 @@
</div>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('confirm.remove_folder')"
@hide-modal="confirmRemove = false"
@resolve="removeFolder"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api"
import * as E from "fp-ts/Either"
import { runMutation } from "~/helpers/backend/GQLClient"
import { DeleteCollectionDocument } from "~/helpers/backend/graphql"
import {
getCompleteCollectionTree,
teamCollToHoppRESTColl,
@@ -243,7 +228,6 @@ export default defineComponent({
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
requestAction: ref<any | null>(null),
folderAction: ref<any | null>(null),
edit: ref<any | null>(null),
deleteAction: ref<any | null>(null),
@@ -255,6 +239,7 @@ export default defineComponent({
return {
showChildren: false,
dragging: false,
confirmRemove: false,
prevCursor: "",
cursor: "",
}
@@ -325,10 +310,30 @@ export default defineComponent({
this.showChildren = !this.showChildren
},
removeFolder() {
this.$emit("remove-folder", {
collectionsType: this.collectionsType,
folder: this.folder,
})
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
// Cancel pick if picked collection folder was deleted
if (
this.picked &&
this.picked.pickedType === "teams-folder" &&
this.picked.folderID === this.folder.id
) {
this.$emit("select", { picked: null })
}
runMutation(DeleteCollectionDocument, {
collectionID: this.folder.id,
})().then((result) => {
if (E.isLeft(result)) {
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
console.error(result.left)
} else {
this.$toast.success(`${this.$t("state.deleted")}`)
this.$emit("update-team-collections")
}
})
this.$emit("update-team-collections")
}
},
expandCollection(collectionID: number) {
this.$emit("expand-collection", collectionID)
@@ -343,6 +348,13 @@ export default defineComponent({
if (E.isLeft(moveRequestResult))
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
},
removeRequest({ collectionIndex, folderName, requestIndex }: any) {
this.$emit("remove-request", {
collectionIndex,
folderName,
requestIndex,
})
},
},
})
</script>

View File

@@ -123,7 +123,7 @@
:shortcut="['⌫']"
@click.native="
() => {
removeRequest()
confirmRemove = true
options.tippy().hide()
}
"
@@ -133,6 +133,12 @@
</span>
</div>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="$t('confirm.remove_request')"
@hide-modal="confirmRemove = false"
@resolve="removeRequest"
/>
<HttpReqChangeConfirmModal
:show="confirmChange"
@hide-modal="confirmChange = false"
@@ -209,7 +215,8 @@ const emit = defineEmits<{
(
e: "remove-request",
data: {
folderPath: string | undefined
collectionIndex: number
folderName: string | undefined
requestIndex: string
}
): void
@@ -246,6 +253,7 @@ const requestMethodLabels = {
delete: "text-red-500",
default: "text-gray-500",
}
const confirmRemove = ref(false)
const confirmChange = ref(false)
const showSaveRequestModal = ref(false)
@@ -261,7 +269,7 @@ const active = useReadonlyStream(restSaveContext$, null)
const isSelected = computed(
() =>
props.picked &&
props.picked.pickedType === "teams-request" &&
props.picked.pickedType === "team-collection" &&
props.picked.requestID === props.requestIndex
)
@@ -281,7 +289,8 @@ const dragStart = ({ dataTransfer }: DragEvent) => {
const removeRequest = () => {
emit("remove-request", {
folderPath: props.folderName,
collectionIndex: props.collectionIndex,
folderName: props.folderName,
requestIndex: props.requestIndex,
})
}
@@ -308,19 +317,16 @@ const setRestReq = (request: HoppRESTRequest) => {
}
const selectRequest = () => {
// Check if this is a save as request popup, if so we don't need to prompt the confirm change popup.
if (props.saveRequest) {
emit("select", {
picked: {
pickedType: "teams-request",
requestID: props.requestIndex,
},
})
} else if (isEqualHoppRESTRequest(props.request, getDefaultRESTRequest())) {
confirmChange.value = false
setRestReq(props.request)
} else if (!active.value) {
if (!active.value) {
confirmChange.value = true
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "team-collection",
requestID: props.requestIndex,
},
})
} else {
const currentReqWithNoChange = active.value.req
const currentFullReq = getRESTRequest()
@@ -330,6 +336,13 @@ const selectRequest = () => {
// Check if there is any changes done on the current request
if (isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)) {
setRestReq(props.request)
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "team-collection",
requestID: props.requestIndex,
},
})
} else {
confirmChange.value = true
}
@@ -349,6 +362,13 @@ const saveRequestChange = () => {
/** Discard changes and change the current request and context */
const discardRequestChange = () => {
setRestReq(props.request)
if (props.saveRequest)
emit("select", {
picked: {
pickedType: "team-collection",
requestID: props.requestIndex,
},
})
if (!isActive.value) {
setRESTSaveContext({
originLocation: "team-collection",
@@ -356,6 +376,7 @@ const discardRequestChange = () => {
req: props.request,
})
}
confirmChange.value = false
}

View File

@@ -2,7 +2,7 @@
<SmartModal
v-if="show"
dialog
:title="t(`environment.${action}`)"
:title="$t(`environment.${action}`)"
@close="hideModal"
>
<template #body>
@@ -20,24 +20,24 @@
@keyup.enter="saveEnvironment"
/>
<label for="selectLabelEnvEdit">
{{ t("action.label") }}
{{ $t("action.label") }}
</label>
</div>
<div class="flex items-center justify-between flex-1">
<label for="variableList" class="p-4">
{{ t("environment.variable_list") }}
{{ $t("environment.variable_list") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:title="$t('action.clear_all')"
:svg="clearIcon"
@click.native="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="plus"
:title="t('add.new')"
:title="$t('add.new')"
@click.native="addEnvironmentVariable"
/>
</div>
@@ -46,23 +46,23 @@
v-if="evnExpandError"
class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
>
{{ t("environment.nested_overflow") }}
{{ $t("environment.nested_overflow") }}
</div>
<div class="border rounded divide-y divide-dividerLight border-divider">
<div
v-for="({ id, env }, index) in vars"
:key="`variable-${id}-${index}`"
v-for="(variable, index) in vars"
:key="`variable-${index}`"
class="flex divide-x divide-dividerLight"
>
<input
v-model="env.key"
v-model="variable.key"
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
:placeholder="`${$t('count.variable', { count: index + 1 })}`"
:name="'param' + index"
/>
<SmartEnvInput
v-model="env.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
v-model="variable.value"
:placeholder="`${$t('count.value', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'value' + index"
/>
@@ -70,7 +70,7 @@
<ButtonSecondary
id="variable"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:title="$t('action.remove')"
svg="trash"
color="red"
@click.native="removeEnvironmentVariable(index)"
@@ -85,13 +85,13 @@
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.environments')}`"
:alt="`${$t('empty.environments')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.environments") }}
{{ $t("empty.environments") }}
</span>
<ButtonSecondary
:label="`${t('add.new')}`"
:label="`${$t('add.new')}`"
filled
class="mb-4"
@click.native="addEnvironmentVariable"
@@ -103,11 +103,11 @@
<template #footer>
<span>
<ButtonPrimary
:label="`${t('action.save')}`"
:label="`${$t('action.save')}`"
@click.native="saveEnvironment"
/>
<ButtonSecondary
:label="`${t('action.cancel')}`"
:label="`${$t('action.cancel')}`"
@click.native="hideModal"
/>
</span>
@@ -115,205 +115,151 @@
</SmartModal>
</template>
<script setup lang="ts">
<script lang="ts">
import clone from "lodash/clone"
import { computed, ref, watch } from "@nuxtjs/composition-api"
import { computed, defineComponent, PropType } from "@nuxtjs/composition-api"
import * as E from "fp-ts/Either"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import { pipe, flow } from "fp-ts/function"
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import {
createEnvironment,
environments$,
getEnvironment,
getEnviroment,
getGlobalVariables,
globalEnv$,
setCurrentEnvironment,
setGlobalEnvVariables,
updateEnvironment,
} from "~/newstore/environments"
import {
useReadonlyStream,
useI18n,
useToast,
} from "~/helpers/utils/composables"
import { useReadonlyStream } from "~/helpers/utils/composables"
type EnvironmentVariable = {
id: number
env: {
key: string
value: string
}
}
export default defineComponent({
props: {
show: Boolean,
action: {
type: String as PropType<"new" | "edit">,
default: "edit",
},
editingEnvironmentIndex: {
type: [Number, String] as PropType<number | "Global" | null>,
default: null,
},
envVars: {
type: Function as PropType<() => Environment["variables"]>,
default: () => [],
},
},
setup(props) {
const globalVars = useReadonlyStream(globalEnv$, [])
const t = useI18n()
const toast = useToast()
const workingEnv = computed(() => {
if (props.editingEnvironmentIndex === "Global") {
return {
name: "Global",
variables: getGlobalVariables(),
} as Environment
} else if (props.action === "new") {
return {
name: "",
variables: props.envVars(),
}
} else if (props.editingEnvironmentIndex !== null) {
return getEnviroment(props.editingEnvironmentIndex)
} else {
return null
}
})
const props = withDefaults(
defineProps<{
show: boolean
action: "edit" | "new"
editingEnvironmentIndex: number | "Global" | null
envVars: () => Environment["variables"]
}>(),
{
show: false,
action: "edit",
editingEnvironmentIndex: null,
envVars: () => [],
}
)
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const idTicker = ref(0)
const name = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } },
])
const clearIcon = refAutoReset<"trash-2" | "check">("trash-2", 1000)
const globalVars = useReadonlyStream(globalEnv$, [])
const workingEnv = computed(() => {
if (props.editingEnvironmentIndex === "Global") {
return {
name: "Global",
variables: getGlobalVariables(),
} as Environment
} else if (props.action === "new") {
globalVars,
workingEnv,
envList: useReadonlyStream(environments$, []) || props.envVars(),
}
},
data() {
return {
name: "",
variables: props.envVars(),
name: null as string | null,
vars: [] as { key: string; value: string }[],
clearIcon: "trash-2",
}
} else if (props.editingEnvironmentIndex !== null) {
return getEnvironment(props.editingEnvironmentIndex)
} else {
return null
}
})
},
computed: {
evnExpandError(): boolean {
for (const variable of this.vars) {
const result = parseTemplateStringE(variable.value, this.vars)
const envList = useReadonlyStream(environments$, []) || props.envVars()
if (E.isLeft(result)) {
console.error("error", result.left)
return true
}
}
return false
},
liveEnvs(): Array<{ key: string; value: string; source: string }> {
if (this.evnExpandError) {
return []
}
const evnExpandError = computed(() => {
const variables = pipe(
vars.value,
A.map((e) => e.env)
)
return pipe(
variables,
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
)
})
const liveEnvs = computed(() => {
if (evnExpandError) {
return []
}
if (props.editingEnvironmentIndex === "Global") {
return [...vars.value.map((x) => ({ ...x, source: name.value! }))]
} else {
return [
...vars.value.map((x) => ({ ...x, source: name.value! })),
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
]
}
})
watch(
() => props.show,
(show) => {
if (show) {
name.value = workingEnv.value?.name ?? null
vars.value = pipe(
workingEnv.value?.variables ?? [],
A.map((e) => ({
id: idTicker.value++,
env: clone(e),
}))
)
}
}
)
const clearContent = () => {
vars.value = [
{
id: idTicker.value++,
env: {
if (this.$props.editingEnvironmentIndex === "Global") {
return [...this.vars.map((x) => ({ ...x, source: this.name! }))]
} else {
return [
...this.vars.map((x) => ({ ...x, source: this.name! })),
...this.globalVars.map((x) => ({ ...x, source: "Global" })),
]
}
},
},
watch: {
show() {
this.name = this.workingEnv?.name ?? null
this.vars = clone(this.workingEnv?.variables ?? [])
},
},
methods: {
clearContent() {
this.vars = []
this.clearIcon = "check"
this.$toast.success(`${this.$t("state.cleared")}`)
setTimeout(() => (this.clearIcon = "trash-2"), 1000)
},
addEnvironmentVariable() {
this.vars.push({
key: "",
value: "",
},
})
},
]
clearIcon.value = "check"
toast.success(`${t("state.cleared")}`)
}
const addEnvironmentVariable = () => {
vars.value.push({
id: idTicker.value++,
env: {
key: "",
value: "",
removeEnvironmentVariable(index: number) {
this.vars.splice(index, 1)
},
})
}
saveEnvironment() {
if (!this.name) {
this.$toast.error(`${this.$t("environment.invalid_name")}`)
return
}
const removeEnvironmentVariable = (index: number) => {
vars.value.splice(index, 1)
}
if (this.action === "new") {
createEnvironment(this.name)
setCurrentEnvironment(this.envList.length - 1)
}
const saveEnvironment = () => {
if (!name.value) {
toast.error(`${t("environment.invalid_name")}`)
return
}
const environmentUpdated: Environment = {
name: this.name,
variables: this.vars,
}
const filterdVariables = pipe(
vars.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.env.key !== ""),
O.map((e) => e.env)
)
)
)
const environmentUpdated: Environment = {
name: name.value,
variables: filterdVariables,
}
if (props.action === "new") {
// Creating a new environment
createEnvironment(name.value)
updateEnvironment(envList.value.length - 1, environmentUpdated)
setCurrentEnvironment(envList.value.length - 1)
toast.success(`${t("environment.created")}`)
} else if (props.editingEnvironmentIndex === "Global") {
// Editing the Global environment
setGlobalEnvVariables(environmentUpdated.variables)
toast.success(`${t("environment.updated")}`)
} else if (props.editingEnvironmentIndex !== null) {
// Editing an environment
updateEnvironment(props.editingEnvironmentIndex, environmentUpdated)
toast.success(`${t("environment.updated")}`)
}
hideModal()
}
const hideModal = () => {
name.value = null
emit("hide-modal")
}
if (this.editingEnvironmentIndex === "Global")
setGlobalEnvVariables(environmentUpdated.variables)
else if (this.action === "new") {
updateEnvironment(this.envList.length - 1, environmentUpdated)
} else {
updateEnvironment(this.editingEnvironmentIndex!, environmentUpdated)
}
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -5,13 +5,13 @@
>
<span
class="flex items-center justify-center px-4 cursor-pointer"
@click="emit('edit-environment')"
@click="$emit('edit-environment')"
>
<SmartIcon class="svg-icons" name="layers" />
</span>
<span
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="emit('edit-environment')"
@click="$emit('edit-environment')"
>
<span class="truncate">
{{ environment.name }}
@@ -29,7 +29,7 @@
<template #trigger>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:title="$t('action.more')"
svg="more-vertical"
/>
</template>
@@ -48,11 +48,11 @@
<SmartItem
ref="edit"
svg="edit"
:label="`${t('action.edit')}`"
:label="`${$t('action.edit')}`"
:shortcut="['E']"
@click.native="
() => {
emit('edit-environment')
$emit('edit-environment')
options.tippy().hide()
}
"
@@ -60,11 +60,11 @@
<SmartItem
ref="duplicate"
svg="copy"
:label="`${t('action.duplicate')}`"
:label="`${$t('action.duplicate')}`"
:shortcut="['D']"
@click.native="
() => {
duplicateEnvironments()
duplicateEnvironment()
options.tippy().hide()
}
"
@@ -73,7 +73,7 @@
v-if="!(environmentIndex === 'Global')"
ref="deleteAction"
svg="trash-2"
:label="`${t('action.delete')}`"
:label="`${$t('action.delete')}`"
:shortcut="['⌫']"
@click.native="
() => {
@@ -87,17 +87,15 @@
</span>
<SmartConfirmModal
:show="confirmRemove"
:title="`${t('confirm.remove_environment')}`"
:title="`${$t('confirm.remove_environment')}`"
@hide-modal="confirmRemove = false"
@resolve="removeEnvironment"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from "@nuxtjs/composition-api"
import { Environment } from "@hoppscotch/data"
import cloneDeep from "lodash/cloneDeep"
<script lang="ts">
import { defineComponent, PropType, ref } from "@nuxtjs/composition-api"
import {
deleteEnvironment,
duplicateEnvironment,
@@ -106,43 +104,47 @@ import {
getGlobalVariables,
environmentsStore,
} from "~/newstore/environments"
import { useI18n, useToast } from "~/helpers/utils/composables"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
environment: Environment
environmentIndex: number | "Global" | null
}>()
const emit = defineEmits<{
(e: "edit-environment"): void
}>()
const confirmRemove = ref(false)
const tippyActions = ref<any | null>(null)
const options = ref<any | null>(null)
const edit = ref<any | null>(null)
const duplicate = ref<any | null>(null)
const deleteAction = ref<any | null>(null)
const removeEnvironment = () => {
if (props.environmentIndex === null) return
if (props.environmentIndex !== "Global")
deleteEnvironment(props.environmentIndex)
toast.success(`${t("state.deleted")}`)
}
const duplicateEnvironments = () => {
if (props.environmentIndex === null) return
if (props.environmentIndex === "Global") {
createEnvironment(`Global - ${t("action.duplicate")}`)
setEnvironmentVariables(
environmentsStore.value.environments.length - 1,
cloneDeep(getGlobalVariables())
)
} else duplicateEnvironment(props.environmentIndex)
}
export default defineComponent({
props: {
environment: { type: Object, default: () => {} },
environmentIndex: {
type: [Number, String] as PropType<number | "Global">,
default: null,
},
},
setup() {
return {
tippyActions: ref<any | null>(null),
options: ref<any | null>(null),
edit: ref<any | null>(null),
duplicate: ref<any | null>(null),
deleteAction: ref<any | null>(null),
}
},
data() {
return {
confirmRemove: false,
}
},
methods: {
removeEnvironment() {
if (this.environmentIndex !== "Global")
deleteEnvironment(this.environmentIndex)
this.$toast.success(`${this.$t("state.deleted")}`)
},
duplicateEnvironment() {
if (this.environmentIndex === "Global") {
createEnvironment(`Global - ${this.$t("action.duplicate")}`)
setEnvironmentVariables(
environmentsStore.value.environments.length - 1,
getGlobalVariables().reduce((gVars, gVar) => {
gVars.push({ key: gVar.key, value: gVar.value })
return gVars
}, [])
)
} else duplicateEnvironment(this.environmentIndex)
},
},
})
</script>

View File

@@ -5,7 +5,7 @@
<template #trigger>
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`"
:title="`${$t('environment.select')}`"
class="flex-1 bg-transparent border-b border-dividerLight select-wrapper"
>
<ButtonSecondary
@@ -15,20 +15,20 @@
/>
<ButtonSecondary
v-else
:label="`${t('environment.select')}`"
:label="`${$t('environment.select')}`"
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
</template>
<div class="flex flex-col" role="menu">
<SmartItem
:label="`${t('environment.no_environment')}`"
:label="`${$t('environment.no_environment')}`"
:info-icon="selectedEnvironmentIndex === -1 ? 'done' : ''"
:active-info-icon="selectedEnvironmentIndex === -1"
@click.native="
() => {
selectedEnvironmentIndex = -1
options.tippy().hide()
$refs.options.tippy().hide()
}
"
/>
@@ -42,7 +42,7 @@
@click.native="
() => {
selectedEnvironmentIndex = index
options.tippy().hide()
$refs.options.tippy().hide()
}
"
/>
@@ -51,7 +51,7 @@
<div class="flex justify-between flex-1 border-b border-dividerLight">
<ButtonSecondary
svg="plus"
:label="`${t('action.new')}`"
:label="`${$t('action.new')}`"
class="!rounded-none"
@click.native="displayModalAdd(true)"
/>
@@ -60,13 +60,13 @@
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/environments"
blank
:title="t('app.wiki')"
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
svg="archive"
:title="t('modal.import_export')"
:title="$t('modal.import_export')"
@click.native="displayModalImportExport(true)"
/>
</div>
@@ -95,13 +95,13 @@
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.environments')}`"
:alt="`${$t('empty.environments')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.environments") }}
{{ $t("empty.environments") }}
</span>
<ButtonSecondary
:label="`${t('add.new')}`"
:label="`${$t('add.new')}`"
filled
class="mb-4"
@click.native="displayModalAdd(true)"
@@ -120,13 +120,9 @@
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "@nuxtjs/composition-api"
import {
useReadonlyStream,
useStream,
useI18n,
} from "~/helpers/utils/composables"
<script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api"
import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
import {
environments$,
setCurrentEnvironment,
@@ -134,49 +130,55 @@ import {
globalEnv$,
} from "~/newstore/environments"
const t = useI18n()
export default defineComponent({
setup() {
const globalEnv = useReadonlyStream(globalEnv$, [])
const options = ref<any | null>(null)
const globalEnvironment = computed(() => ({
name: "Global",
variables: globalEnv.value,
}))
const globalEnv = useReadonlyStream(globalEnv$, [])
return {
environments: useReadonlyStream(environments$, []),
globalEnvironment,
selectedEnvironmentIndex: useStream(
selectedEnvIndex$,
-1,
setCurrentEnvironment
),
}
},
data() {
return {
showModalImportExport: false,
showModalDetails: false,
action: "edit" as "new" | "edit",
editingEnvironmentIndex: undefined as number | "Global" | undefined,
}
},
methods: {
displayModalAdd(shouldDisplay: boolean) {
this.action = "new"
this.showModalDetails = shouldDisplay
},
displayModalEdit(shouldDisplay: boolean) {
this.action = "edit"
this.showModalDetails = shouldDisplay
const globalEnvironment = computed(() => ({
name: "Global",
variables: globalEnv.value,
}))
const environments = useReadonlyStream(environments$, [])
const selectedEnvironmentIndex = useStream(
selectedEnvIndex$,
-1,
setCurrentEnvironment
)
const showModalImportExport = ref(false)
const showModalDetails = ref(false)
const action = ref<"new" | "edit">("edit")
const editingEnvironmentIndex = ref<number | "Global" | null>(null)
const displayModalAdd = (shouldDisplay: boolean) => {
action.value = "new"
showModalDetails.value = shouldDisplay
}
const displayModalEdit = (shouldDisplay: boolean) => {
action.value = "edit"
showModalDetails.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalImportExport = (shouldDisplay: boolean) => {
showModalImportExport.value = shouldDisplay
}
const editEnvironment = (environmentIndex: number | "Global") => {
editingEnvironmentIndex.value = environmentIndex
action.value = "edit"
displayModalEdit(true)
}
const resetSelectedData = () => {
editingEnvironmentIndex.value = null
}
if (!shouldDisplay) this.resetSelectedData()
},
displayModalImportExport(shouldDisplay: boolean) {
this.showModalImportExport = shouldDisplay
},
editEnvironment(environmentIndex: number | "Global") {
this.$data.editingEnvironmentIndex = environmentIndex
this.action = "edit"
this.displayModalEdit(true)
},
resetSelectedData() {
this.$data.editingEnvironmentIndex = undefined
},
},
})
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -137,47 +137,6 @@
/>
</span>
</div>
<div
v-for="(header, index) in computedHeaders"
:key="`header-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<ButtonSecondary
svg="lock"
class="opacity-25 cursor-auto text-secondaryLight bg-divider"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.header.key"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<SmartEnvInput
:value="mask(header)"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<span>
<ButtonSecondary
v-if="header.source === 'auth'"
:svg="masking ? 'eye' : 'eye-off'"
@click.native="toggleMask()"
/>
<ButtonSecondary
v-else
svg="arrow-up-right"
class="cursor-auto text-primary hover:text-primary"
/>
</span>
<span>
<ButtonSecondary
svg="arrow-up-right"
@click.native="changeTab(header.source)"
/>
</span>
</div>
</draggable>
<div
v-if="workingHeaders.length === 0"
@@ -203,7 +162,7 @@
</template>
<script setup lang="ts">
import { computed, Ref, ref, watch } from "@nuxtjs/composition-api"
import { Ref, ref, watch } from "@nuxtjs/composition-api"
import isEqual from "lodash/isEqual"
import {
HoppRESTHeader,
@@ -218,29 +177,13 @@ import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import cloneDeep from "lodash/cloneDeep"
import draggable from "vuedraggable"
import { RequestOptionTabs } from "./RequestOptions.vue"
import { useCodemirror } from "~/helpers/editor/codemirror"
import {
getRESTRequest,
restHeaders$,
restRequest$,
setRESTHeaders,
} from "~/newstore/RESTSession"
import { restHeaders$, setRESTHeaders } from "~/newstore/RESTSession"
import { commonHeaders } from "~/helpers/headers"
import {
useI18n,
useReadonlyStream,
useStream,
useToast,
} from "~/helpers/utils/composables"
import { useI18n, useStream, useToast } from "~/helpers/utils/composables"
import linter from "~/helpers/editor/linting/rawKeyValue"
import { throwError } from "~/helpers/functional/error"
import { objRemoveKey } from "~/helpers/functional/object"
import {
ComputedHeader,
getComputedHeaders,
} from "~/helpers/utils/EffectiveURL"
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
const t = useI18n()
const toast = useToast()
@@ -253,10 +196,6 @@ const bulkEditor = ref<any | null>(null)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const emit = defineEmits<{
(e: "change-tab", value: RequestOptionTabs): void
}>()
useCodemirror(bulkEditor, bulkHeaders, {
extendedEditorConfig: {
mode: "text/x-yaml",
@@ -440,28 +379,4 @@ const clearContent = () => {
bulkHeaders.value = ""
}
const restRequest = useReadonlyStream(restRequest$, getRESTRequest())
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
const computedHeaders = computed(() =>
getComputedHeaders(restRequest.value, aggregateEnvs.value)
)
const masking = ref(true)
const toggleMask = () => {
masking.value = !masking.value
}
const mask = (header: ComputedHeader) => {
if (header.source === "auth" && masking.value)
return header.header.value.replace(/\S/gi, "*")
return header.header.value
}
const changeTab = (tab: ComputedHeader["source"]) => {
if (tab === "auth") emit("change-tab", "authorization")
else emit("change-tab", "bodyParams")
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -171,12 +171,6 @@
}
"
/>
<SmartItem
svg="link-2"
:label="`${t('request.view_my_links')}`"
to="/profile"
/>
<hr />
<SmartItem
ref="saveRequestAction"
:label="`${t('request.save_as')}`"
@@ -214,8 +208,6 @@
import { computed, ref, watch } from "@nuxtjs/composition-api"
import { isLeft, isRight } from "fp-ts/lib/Either"
import * as E from "fp-ts/Either"
import cloneDeep from "lodash/cloneDeep"
import { refAutoReset } from "@vueuse/core"
import {
updateRESTResponse,
restEndpoint$,
@@ -348,8 +340,7 @@ const newSendRequest = async () => {
const ensureMethodInEndpoint = () => {
if (
!/^http[s]?:\/\//.test(newEndpoint.value) &&
!newEndpoint.value.startsWith("<<") &&
!newEndpoint.value.startsWith("{{")
!newEndpoint.value.startsWith("<<")
) {
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
@@ -395,11 +386,7 @@ const clearContent = () => {
resetRESTRequest()
}
const copyLinkIcon = refAutoReset<"share-2" | "copy" | "check">(
hasNavigatorShare ? "share-2" : "copy",
1000
)
const copyLinkIcon = hasNavigatorShare ? ref("share-2") : ref("copy")
const shareLink = ref<string | null>("")
const fetchingShareLink = ref(false)
@@ -454,6 +441,7 @@ const copyShareLink = (shareLink: string) => {
copyLinkIcon.value = "check"
copyToClipboard(`https://hopp.sh/r${shareLink}`)
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyLinkIcon.value = "copy"), 2000)
}
}
@@ -489,21 +477,14 @@ const saveRequest = () => {
showSaveRequestModal.value = true
return
}
if (saveCtx.originLocation === "user-collection") {
const req = getRESTRequest()
if (saveCtx.originLocation === "user-collection") {
try {
editRESTRequest(
saveCtx.folderPath,
saveCtx.requestIndex,
getRESTRequest()
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: saveCtx.folderPath,
requestIndex: saveCtx.requestIndex,
req: cloneDeep(req),
})
toast.success(`${t("request.saved")}`)
} catch (e) {
setRESTSaveContext(null)
@@ -524,11 +505,6 @@ const saveRequest = () => {
if (E.isLeft(result)) {
toast.error(`${t("profile.no_permission")}`)
} else {
setRESTSaveContext({
originLocation: "team-collection",
requestID: saveCtx.requestID,
req: cloneDeep(req),
})
toast.success(`${t("request.saved")}`)
}
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,17 +17,32 @@
/>
</div>
</div>
<LensesHeadersRendererEntry
<div
v-for="(header, index) in headers"
:key="index"
:header="header"
/>
:key="`header-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight group"
>
<span
class="flex flex-1 min-w-0 px-4 py-2 transition group-hover:text-secondaryDark"
>
<span class="truncate rounded-sm select-all">
{{ header.key }}
</span>
</span>
<span
class="flex flex-1 min-w-0 px-4 py-2 transition group-hover:text-secondaryDark"
>
<span class="truncate rounded-sm select-all">
{{ header.value }}
</span>
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "@nuxtjs/composition-api"
import { HoppRESTHeader } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n, useToast } from "~/helpers/utils/composables"
@@ -39,11 +54,12 @@ const props = defineProps<{
headers: Array<HoppRESTHeader>
}>()
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
const copyIcon = ref("copy")
const copyHeaders = () => {
copyToClipboard(JSON.stringify(props.headers))
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
</script>

View File

@@ -1,51 +0,0 @@
<template>
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight group"
>
<span
class="flex flex-1 min-w-0 px-4 py-2 transition group-hover:text-secondaryDark"
>
<span class="truncate rounded-sm select-all">
{{ header.key }}
</span>
</span>
<span
class="flex flex-1 min-w-0 pl-4 py-2 transition group-hover:text-secondaryDark justify-between"
>
<span class="truncate rounded-sm select-all">
{{ header.value }}
</span>
<ButtonSecondary
ref="copyHeader"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:svg="copyIcon"
class="hidden group-hover:inline-flex !py-0"
@click.native="copyHeader(header.value)"
/>
</span>
</div>
</template>
<script setup lang="ts">
import { refAutoReset } from "@vueuse/core"
import { HoppRESTHeader } from "~/../hoppscotch-data/dist"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n, useToast } from "~/helpers/utils/composables"
const t = useI18n()
const toast = useToast()
defineProps<{
header: HoppRESTHeader
}>()
const copyIcon = refAutoReset<"copy" | "check">("copy", 1000)
const copyHeader = (headerValue: string) => {
copyToClipboard(headerValue)
copyIcon.value = "check"
toast.success(`${t("state.copied_to_clipboard")}`)
}
</script>

View File

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

View File

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

View File

@@ -1,136 +0,0 @@
<template>
<div
class="table-row-groups lg:flex block my-6 lg:my-0 w-full border lg:border-0 divide-y lg:divide-y-0 lg:divide-x divide-dividerLight border-dividerLight"
>
<div
class="table-column font-mono text-tiny"
:data-label="t('shortcodes.short_code')"
>
{{ shortcode.id }}
</div>
<div
class="table-column"
:class="requestLabelColor"
:data-label="t('shortcodes.method')"
>
{{ parseShortcodeRequest.method }}
</div>
<div class="table-column" :data-label="t('shortcodes.url')">
{{ parseShortcodeRequest.endpoint }}
</div>
<div
ref="timeStampRef"
class="table-column"
:data-label="t('shortcodes.created_on')"
>
<span v-tippy="{ theme: 'tooltip' }" :title="timeStamp">
{{ dateStamp }}
</span>
</div>
<div
class="flex flex-1 items-center justify-center px-3"
:data-label="t('shortcodes.actions')"
>
<SmartAnchor
v-tippy="{ theme: 'tooltip' }"
:title="t('action.open_workspace')"
:to="`https://hopp.sh/r/${shortcode.id}`"
blank
svg="external-link"
class="px-3 text-accent hover:text-accent"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
color="green"
:svg="copyIconRefs"
class="px-3"
@click.native="copyShortcode(shortcode.id)"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.delete')"
svg="trash"
color="red"
class="px-3"
@click.native="deleteShortcode(shortcode.id)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "@nuxtjs/composition-api"
import { pipe } from "fp-ts/function"
import * as RR from "fp-ts/ReadonlyRecord"
import * as O from "fp-ts/Option"
import { translateToNewRequest } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { useI18n, useToast } from "~/helpers/utils/composables"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { Shortcode } from "~/helpers/shortcodes/Shortcode"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
shortcode: Shortcode
}>()
const emit = defineEmits<{
(e: "delete-shortcode", codeID: string): void
}>()
const deleteShortcode = (codeID: string) => {
emit("delete-shortcode", codeID)
}
const requestMethodLabels = {
get: "text-green-500",
post: "text-yellow-500",
put: "text-blue-500",
delete: "text-red-500",
default: "text-gray-500",
} as const
const timeStampRef = ref()
const copyIconRefs = refAutoReset<"copy" | "check">("copy", 1000)
const parseShortcodeRequest = computed(() =>
pipe(props.shortcode.request, JSON.parse, translateToNewRequest)
)
const requestLabelColor = computed(() =>
pipe(
requestMethodLabels,
RR.lookup(parseShortcodeRequest.value.method.toLowerCase()),
O.getOrElseW(() => requestMethodLabels.default)
)
)
const dateStamp = computed(() =>
new Date(props.shortcode.createdOn).toLocaleDateString()
)
const timeStamp = computed(() =>
new Date(props.shortcode.createdOn).toLocaleTimeString()
)
const copyShortcode = (codeID: string) => {
copyToClipboard(`https://hopp.sh/r/${codeID}`)
toast.success(`${t("state.copied_to_clipboard")}`)
copyIconRefs.value = "check"
}
</script>
<style lang="scss">
.table-column {
@apply flex flex-1 items-center px-3 py-3 truncate;
}
.table-row-groups {
.table-column {
@apply before:text-secondary before:font-bold before:content-[attr(data-label)] lg:before:hidden;
}
}
</style>

View File

@@ -1,222 +0,0 @@
<template>
<div class="flex flex-col flex-1">
<div v-if="showEventField" class="flex items-center justify-between p-4">
<input
id="event_name"
v-model="eventName"
class="input"
name="event_name"
:placeholder="`${t('socketio.event_name')}`"
type="text"
autocomplete="off"
/>
</div>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold text-secondaryLight">
{{ $t("websocket.message") }}
</label>
<tippy
ref="contentTypeOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<span class="select-wrapper">
<ButtonSecondary
:label="contentType || $t('state.none').toLowerCase()"
class="pr-8 ml-2 rounded-none"
/>
</span>
</template>
<div class="flex flex-col" role="menu">
<SmartItem
v-for="(contentTypeItem, index) in validContentTypes"
:key="`contentTypeItem-${index}`"
:label="contentTypeItem"
:info-icon="contentTypeItem === contentType ? 'done' : ''"
:active-info-icon="contentTypeItem === contentType"
@click.native="
() => {
contentType = contentTypeItem
$refs.contentTypeOptions.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t('action.send')}`"
:label="`${t('action.send')}`"
:disabled="!communicationBody || !isConnected"
svg="send"
class="rounded-none !text-accent !hover:text-accentDark"
@click.native="sendMessage()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/body"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="wrap-text"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear')"
svg="trash-2"
@click.native="clearContent"
/>
<ButtonSecondary
v-if="contentType && contentType == 'JSON'"
ref="prettifyRequest"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:svg="prettifyIcon"
@click.native="prettifyRequestBody"
/>
<label for="payload">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('import.title')"
svg="file-plus"
@click.native="$refs.payload.click()"
/>
</label>
<input
ref="payload"
class="input"
name="payload"
type="file"
@change="uploadPayload"
/>
</div>
</div>
<div ref="wsCommunicationBody" class="flex flex-col flex-1"></div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from "@nuxtjs/composition-api"
import { pipe } from "fp-ts/function"
import * as TO from "fp-ts/TaskOption"
import * as O from "fp-ts/Option"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "~/helpers/editor/codemirror"
import jsonLinter from "~/helpers/editor/linting/json"
import { readFileAsText } from "~/helpers/functional/files"
import { useI18n, useToast } from "~/helpers/utils/composables"
import { isJSONContentType } from "~/helpers/utils/contenttypes"
defineProps({
showEventField: {
type: Boolean,
default: false,
},
isConnected: {
type: Boolean,
default: false,
},
})
const emit = defineEmits<{
(
e: "send-message",
body: {
eventName: string
message: string
}
): void
}>()
const t = useI18n()
const toast = useToast()
const linewrapEnabled = ref(true)
const wsCommunicationBody = ref<HTMLElement>()
const prettifyIcon = refAutoReset<"wand" | "check" | "info">("wand", 1000)
const knownContentTypes = {
JSON: "application/ld+json",
Raw: "text/plain",
} as const
const validContentTypes = Object.keys(knownContentTypes)
const contentType = ref<keyof typeof knownContentTypes>("JSON")
const eventName = ref("")
const communicationBody = ref("")
const rawInputEditorLang = computed(() => knownContentTypes[contentType.value])
const langLinter = computed(() =>
isJSONContentType(contentType.value) ? jsonLinter : null
)
useCodemirror(
wsCommunicationBody,
communicationBody,
reactive({
extendedEditorConfig: {
lineWrapping: linewrapEnabled,
mode: rawInputEditorLang,
placeholder: t("websocket.message").toString(),
},
linter: langLinter,
completer: null,
environmentHighlights: true,
})
)
const clearContent = () => {
communicationBody.value = ""
}
const sendMessage = () => {
if (!communicationBody.value) return
emit("send-message", {
eventName: eventName.value,
message: communicationBody.value,
})
communicationBody.value = ""
}
const uploadPayload = async (e: InputEvent) => {
const result = await pipe(
(e.target as HTMLInputElement).files?.[0],
TO.fromNullable,
TO.chain(readFileAsText)
)()
if (O.isSome(result)) {
communicationBody.value = result.value
toast.success(`${t("state.file_imported")}`)
} else {
toast.error(`${t("action.choose_file")}`)
}
}
const prettifyRequestBody = () => {
try {
const jsonObj = JSON.parse(communicationBody.value)
communicationBody.value = JSON.stringify(jsonObj, null, 2)
prettifyIcon.value = "check"
} catch (e) {
console.error(e)
prettifyIcon.value = "info"
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
}
</script>

View File

@@ -1,129 +1,77 @@
<template>
<div ref="container" class="flex flex-col flex-1 overflow-y-auto">
<div class="flex flex-col flex-1">
<div
class="sticky top-0 z-10 flex items-center justify-between flex-none pl-4 border-b bg-primary border-dividerLight"
class="sticky top-0 z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight"
>
<label for="log" class="py-2 font-semibold text-secondaryLight">
{{ title }}
</label>
<div>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.delete')"
svg="trash"
@click.native="emit('delete')"
/>
<ButtonSecondary
id="bottompage"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.scroll_to_top')"
svg="arrow-up"
@click.native="scrollTo('top')"
/>
<ButtonSecondary
id="bottompage"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.scroll_to_bottom')"
svg="arrow-down"
@click.native="scrollTo('bottom')"
/>
<ButtonSecondary
id="bottompage"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.autoscroll')"
svg="chevrons-down"
:class="toggleAutoscrollColor"
@click.native="toggleAutoscroll()"
/>
</div>
</div>
<div
v-if="log.length !== 0"
ref="logs"
class="overflow-y-auto border-b border-dividerLight"
>
<div
class="flex flex-col h-auto h-full border-r divide-y divide-dividerLight border-dividerLight"
>
<RealtimeLogEntry
<div ref="logsRef" name="log" class="realtime-log">
<span v-if="log" class="space-y-2">
<span
v-for="(entry, index) in log"
:key="`entry-${index}`"
:entry="entry"
/>
</div>
:style="{ color: entry.color }"
class="font-mono"
>{{ entry.ts }}{{ source(entry.source) }}{{ entry.payload }}</span
>
</span>
<span v-else>{{ t("response.waiting_for_connection") }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, PropType, computed, watch } from "@nuxtjs/composition-api"
import { useThrottleFn, useScroll } from "@vueuse/core"
import { nextTick, ref, watch } from "@nuxtjs/composition-api"
import { getSourcePrefix as source } from "~/helpers/utils/string"
import { useI18n } from "~/helpers/utils/composables"
export type LogEntryData = {
prefix?: string
ts: number | undefined
source: "info" | "client" | "server" | "disconnected"
payload: string
event: "connecting" | "connected" | "disconnected" | "error"
}
const t = useI18n()
const props = defineProps({
log: { type: Array as PropType<LogEntryData[]>, default: () => [] },
log: { type: Array, default: () => [] },
title: {
type: String,
default: "",
},
})
const emit = defineEmits<{
(e: "delete"): void
}>()
const t = useI18n()
const container = ref<HTMLElement | null>(null)
const logs = ref<HTMLElement | null>(null)
const autoScrollEnabled = ref(true)
const logListScroll = useScroll(logs)
// Disable autoscroll when scrolling to top
watch(logListScroll.isScrolling, (isScrolling) => {
if (isScrolling && logListScroll.directions.top)
autoScrollEnabled.value = false
})
const scrollTo = (position: "top" | "bottom") => {
if (position === "top") {
logs.value?.scroll({
behavior: "smooth",
top: 0,
})
} else if (position === "bottom") {
logs.value?.scroll({
behavior: "smooth",
top: logs.value?.scrollHeight,
})
}
}
const logsRef = ref<any | null>(null)
const BOTTOM_SCROLL_DIST_INACCURACY = 5
watch(
() => props.log,
useThrottleFn(() => {
if (autoScrollEnabled.value) scrollTo("bottom")
}, 200),
{ flush: "post" }
)
const toggleAutoscroll = () => {
autoScrollEnabled.value = !autoScrollEnabled.value
}
const toggleAutoscrollColor = computed(() =>
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
() => {
if (!logsRef.value) return
const distToBottom =
logsRef.value.scrollHeight -
logsRef.value.scrollTop -
logsRef.value.clientHeight
if (distToBottom < BOTTOM_SCROLL_DIST_INACCURACY) {
nextTick(() => (logsRef.value.scrollTop = logsRef.value.scrollHeight))
}
}
)
</script>
<style></style>
<style scoped lang="scss">
.realtime-log {
@apply p-4;
@apply bg-transparent;
@apply text-secondary;
@apply overflow-auto;
height: 256px;
&,
span {
@apply select-text;
}
span {
@apply block;
@apply break-words break-all;
}
}
</style>

View File

@@ -1,392 +0,0 @@
<template>
<div v-if="entry" class="divide-y divide-dividerLight">
<div :style="{ color: entryColor }" class="realtime-log">
<div class="flex group">
<div class="flex flex-1 divide-x divide-dividerLight">
<div class="inline-flex items-center p-2">
<SmartIcon
class="svg-icons"
:name="iconName"
:style="{ color: iconColor }"
@click.native="copyQuery(entry.payload)"
/>
</div>
<div
v-if="entry.ts !== undefined"
class="items-center hidden px-1 w-18 sm:inline-flex"
>
<span
v-tippy="{ theme: 'tooltip' }"
:title="relativeTime"
class="mx-auto truncate ts-font text-secondaryLight hover:text-secondary hover:text-center"
>
{{ new Date(entry.ts).toLocaleTimeString() }}
</span>
</div>
<div
class="items-center flex-1 min-w-0 p-2 inline-grid"
@click="toggleExpandPayload()"
>
<div class="truncate">
<span v-if="entry.prefix !== undefined" class="!inline">{{
entry.prefix
}}</span>
{{ entry.payload }}
</div>
</div>
</div>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:svg="`${copyQueryIcon}`"
class="hidden group-hover:inline-flex"
@click.native="copyQuery(entry.payload)"
/>
<ButtonSecondary
svg="chevron-down"
class="transform"
:class="{ 'rotate-180': !minimized }"
@click.native="toggleExpandPayload()"
/>
</div>
</div>
<div v-if="!minimized" class="overflow-hidden bg-primaryLight">
<SmartTabs
v-model="selectedTab"
styles="bg-primaryLight"
render-inactive-tabs
>
<SmartTab v-if="isJSON(entry.payload)" id="json" label="JSON" />
<SmartTab id="raw" label="Raw" />
</SmartTabs>
<div
class="z-10 flex items-center justify-between pl-4 border-b border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="wrap-text"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
ref="downloadResponse"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
:svg="downloadIcon"
@click.native="downloadResponse"
/>
<ButtonSecondary
ref="copyResponse"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:svg="copyIcon"
@click.native="copyResponse"
/>
</div>
</div>
<div ref="editor"></div>
<div
v-if="outlinePath && selectedTab === 'json'"
class="sticky bottom-0 z-10 flex px-2 overflow-auto border-t bg-primaryLight border-dividerLight flex-nowrap hide-scrollbar"
>
<div
v-for="(item, index) in outlinePath"
:key="`item-${index}`"
class="flex items-center"
>
<tippy
ref="outlineOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<div v-if="item.kind === 'RootObject'" class="outline-item">
{}
</div>
<div v-if="item.kind === 'RootArray'" class="outline-item">
[]
</div>
<div v-if="item.kind === 'ArrayMember'" class="outline-item">
{{ item.index }}
</div>
<div v-if="item.kind === 'ObjectMember'" class="outline-item">
{{ item.name }}
</div>
</template>
<div
v-if="item.kind === 'ArrayMember' || item.kind === 'ObjectMember'"
>
<div
v-if="item.kind === 'ArrayMember'"
class="flex flex-col"
role="menu"
>
<SmartItem
v-for="(arrayMember, astIndex) in item.astParent.values"
:key="`ast-${astIndex}`"
:label="`${astIndex}`"
@click.native="
() => {
jumpCursor(arrayMember)
outlineOptions[index].tippy().hide()
}
"
/>
</div>
<div
v-if="item.kind === 'ObjectMember'"
class="flex flex-col"
role="menu"
>
<SmartItem
v-for="(objectMember, astIndex) in item.astParent.members"
:key="`ast-${astIndex}`"
:label="objectMember.key.value"
@click.native="
() => {
jumpCursor(objectMember)
outlineOptions[index].tippy().hide()
}
"
/>
</div>
</div>
<div
v-if="item.kind === 'RootObject'"
class="flex flex-col"
role="menu"
>
<SmartItem
label="{}"
@click.native="
() => {
jumpCursor(item.astValue)
outlineOptions[index].tippy().hide()
}
"
/>
</div>
<div
v-if="item.kind === 'RootArray'"
class="flex flex-col"
role="menu"
>
<SmartItem
label="[]"
@click.native="
() => {
jumpCursor(item.astValue)
outlineOptions[index].tippy().hide()
}
"
/>
</div>
</tippy>
<i
v-if="index + 1 !== outlinePath.length"
class="opacity-50 text-secondaryLight material-icons"
>
chevron_right
</i>
</div>
</div>
</div>
</div>
<div v-else>{{ t("response.waiting_for_connection") }}</div>
</template>
<script setup lang="ts">
import * as LJSON from "lossless-json"
import * as O from "fp-ts/Option"
import { pipe } from "fp-ts/function"
import { ref, computed, reactive, watch } from "@nuxtjs/composition-api"
import { refAutoReset, useTimeAgo } from "@vueuse/core"
import { LogEntryData } from "./Log.vue"
import { useI18n } from "~/helpers/utils/composables"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { isJSON } from "~/helpers/functional/json"
import useCopyResponse from "~/helpers/lenses/composables/useCopyResponse"
import useDownloadResponse from "~/helpers/lenses/composables/useDownloadResponse"
import { useCodemirror } from "~/helpers/editor/codemirror"
import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
import { getJSONOutlineAtPos } from "~/helpers/newOutline"
import {
convertIndexToLineCh,
convertLineChToIndex,
} from "~/helpers/editor/utils"
const t = useI18n()
const props = defineProps<{ entry: LogEntryData }>()
const outlineOptions = ref<any | null>(null)
const editor = ref<any | null>(null)
const linewrapEnabled = ref(true)
const logPayload = computed(() => props.entry.payload)
const selectedTab = ref<"json" | "raw">(
isJSON(props.entry.payload) ? "json" : "raw"
)
// CodeMirror Implementation
const jsonBodyText = computed(() =>
pipe(
logPayload.value,
O.tryCatchK(LJSON.parse),
O.map((val) => LJSON.stringify(val, undefined, 2)),
O.getOrElse(() => logPayload.value)
)
)
const ast = computed(() =>
pipe(
jsonBodyText.value,
O.tryCatchK(jsonParse),
O.getOrElseW(() => null)
)
)
const editorText = computed(() => {
if (selectedTab.value === "json") return jsonBodyText.value
else return logPayload.value
})
const editorMode = computed(() => {
if (selectedTab.value === "json") return "application/ld+json"
else return "text/plain"
})
const { cursor } = useCodemirror(
editor,
editorText,
reactive({
extendedEditorConfig: {
mode: editorMode,
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: false,
})
)
const jumpCursor = (ast: JSONValue | JSONObjectMember) => {
const pos = convertIndexToLineCh(jsonBodyText.value, ast.start)
pos.line--
cursor.value = pos
}
const outlinePath = computed(() =>
pipe(
ast.value,
O.fromNullable,
O.map((ast) =>
getJSONOutlineAtPos(
ast,
convertLineChToIndex(jsonBodyText.value, cursor.value)
)
),
O.getOrElseW(() => null)
)
)
// Code for UI Changes
const minimized = ref(true)
watch(minimized, () => {
selectedTab.value = isJSON(props.entry.payload) ? "json" : "raw"
})
const toggleExpandPayload = () => {
minimized.value = !minimized.value
}
const { copyIcon, copyResponse } = useCopyResponse(logPayload)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"application/json",
logPayload
)
const copyQueryIcon = refAutoReset<"copy" | "check">("copy", 1000)
const copyQuery = (entry: string) => {
copyToClipboard(entry)
copyQueryIcon.value = "check"
}
// Relative Time
// TS could be undefined here. We're just assigning a default value to 0 because we're not showing it in the UI
const relativeTime = useTimeAgo(computed(() => props.entry.ts ?? 0))
const ENTRY_COLORS = {
connected: "#10b981",
connecting: "#10b981",
error: "#ff5555",
disconnected: "#ff5555",
} as const
// Assigns color based on entry event
const entryColor = computed(() => ENTRY_COLORS[props.entry.event])
const ICONS = {
info: {
iconName: "info-realtime",
iconColor: "#10b981",
},
client: {
iconName: "arrow-up-right",
iconColor: "#eaaa45",
},
server: {
iconName: "arrow-down-left",
iconColor: "#38d4ea",
},
disconnected: {
iconName: "info-disconnect",
iconColor: "#ff5555",
},
} as const
const iconColor = computed(() => ICONS[props.entry.source].iconColor)
const iconName = computed(() => ICONS[props.entry.source].iconName)
</script>
<style scoped lang="scss">
.realtime-log {
@apply text-secondary;
@apply overflow-hidden;
&,
span {
@apply select-text;
}
span {
@apply block;
@apply break-words break-all;
}
}
.outline-item {
@apply cursor-pointer;
@apply flex-grow-0 flex-shrink-0;
@apply text-secondaryLight;
@apply inline-flex;
@apply items-center;
@apply px-2;
@apply py-1;
@apply transition;
@apply hover: text-secondary;
}
.ts-font {
font-size: 0.6rem;
}
</style>

View File

@@ -0,0 +1,382 @@
<template>
<AppPaneLayout>
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"
>
<div class="inline-flex flex-1 space-x-2">
<input
id="mqtt-url"
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
:placeholder="$t('mqtt.url')"
:disabled="connectionState"
@keyup.enter="validUrl ? toggleConnection() : null"
/>
<ButtonPrimary
id="connect"
:disabled="!validUrl"
class="w-32"
:label="
connectionState ? $t('action.disconnect') : $t('action.connect')
"
:loading="connectingState"
@click.native="toggleConnection"
/>
</div>
<div class="flex space-x-4">
<input
id="mqtt-username"
v-model="username"
type="text"
spellcheck="false"
class="input"
:placeholder="$t('authorization.username')"
/>
<input
id="mqtt-password"
v-model="password"
type="password"
spellcheck="false"
class="input"
:placeholder="$t('authorization.password')"
/>
</div>
</div>
</template>
<template #secondary>
<RealtimeLog :title="$t('mqtt.log')" :log="log" />
</template>
<template #sidebar>
<div class="flex items-center justify-between p-4">
<label for="pub_topic" class="font-semibold text-secondaryLight">
{{ $t("mqtt.topic") }}
</label>
</div>
<div class="flex px-4">
<input
id="pub_topic"
v-model="pub_topic"
class="input"
:placeholder="$t('mqtt.topic_name')"
type="text"
autocomplete="off"
spellcheck="false"
/>
</div>
<div class="flex items-center justify-between p-4">
<label for="mqtt-message" class="font-semibold text-secondaryLight">
{{ $t("mqtt.communication") }}
</label>
</div>
<div class="flex px-4 space-x-2">
<input
id="mqtt-message"
v-model="msg"
class="input"
type="text"
autocomplete="off"
:placeholder="$t('mqtt.message')"
spellcheck="false"
/>
<ButtonPrimary
id="publish"
name="get"
:disabled="!canpublish"
:label="$t('mqtt.publish')"
@click.native="publish"
/>
</div>
<div
class="flex items-center justify-between p-4 mt-4 border-t border-dividerLight"
>
<label for="sub_topic" class="font-semibold text-secondaryLight">
{{ $t("mqtt.topic") }}
</label>
</div>
<div class="flex px-4 space-x-2">
<input
id="sub_topic"
v-model="sub_topic"
type="text"
autocomplete="off"
:placeholder="$t('mqtt.topic_name')"
spellcheck="false"
class="input"
/>
<ButtonPrimary
id="subscribe"
name="get"
:disabled="!cansubscribe"
:label="
subscriptionState ? $t('mqtt.unsubscribe') : $t('mqtt.subscribe')
"
reverse
@click.native="toggleSubscription"
/>
</div>
</template>
</AppPaneLayout>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import Paho from "paho-mqtt"
import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import {
MQTTEndpoint$,
setMQTTEndpoint,
MQTTConnectingState$,
MQTTConnectionState$,
setMQTTConnectingState,
setMQTTConnectionState,
MQTTSubscriptionState$,
setMQTTSubscriptionState,
MQTTSocket$,
setMQTTSocket,
MQTTLog$,
setMQTTLog,
addMQTTLogLine,
} from "~/newstore/MQTTSession"
import { useStream } from "~/helpers/utils/composables"
export default defineComponent({
setup() {
return {
url: useStream(MQTTEndpoint$, "", setMQTTEndpoint),
connectionState: useStream(
MQTTConnectionState$,
false,
setMQTTConnectionState
),
connectingState: useStream(
MQTTConnectingState$,
false,
setMQTTConnectingState
),
subscriptionState: useStream(
MQTTSubscriptionState$,
false,
setMQTTSubscriptionState
),
log: useStream(MQTTLog$, null, setMQTTLog),
client: useStream(MQTTSocket$, null, setMQTTSocket),
}
},
data() {
return {
isUrlValid: true,
pub_topic: "",
sub_topic: "",
msg: "",
manualDisconnect: false,
username: "",
password: "",
}
},
computed: {
validUrl() {
return this.isUrlValid
},
canpublish() {
return this.pub_topic !== "" && this.msg !== "" && this.connectionState
},
cansubscribe() {
return this.sub_topic !== "" && this.connectionState
},
},
watch: {
url() {
this.debouncer()
},
},
created() {
if (process.browser) {
this.worker = this.$worker.createRejexWorker()
this.worker.addEventListener("message", this.workerResponseHandler)
}
},
destroyed() {
this.worker.terminate()
},
methods: {
debouncer: debounce(function () {
this.worker.postMessage({ type: "ws", url: this.url })
}, 1000),
workerResponseHandler({ data }) {
if (data.url === this.url) this.isUrlValid = data.result
},
connect() {
this.connectingState = true
this.log = [
{
payload: this.$t("state.connecting_to", { name: this.url }),
source: "info",
color: "var(--accent-color)",
ts: new Date().toLocaleTimeString(),
},
]
const parseUrl = new URL(this.url)
this.client = new Paho.Client(
`${parseUrl.hostname}${
parseUrl.pathname !== "/" ? parseUrl.pathname : ""
}`,
parseUrl.port !== "" ? Number(parseUrl.port) : 8081,
"hoppscotch"
)
const connectOptions = {
onSuccess: this.onConnectionSuccess,
onFailure: this.onConnectionFailure,
useSSL: parseUrl.protocol !== "ws:",
}
if (this.username !== "") {
connectOptions.userName = this.username
}
if (this.password !== "") {
connectOptions.password = this.password
}
this.client.connect(connectOptions)
this.client.onConnectionLost = this.onConnectionLost
this.client.onMessageArrived = this.onMessageArrived
logHoppRequestRunToAnalytics({
platform: "mqtt",
})
},
onConnectionFailure() {
this.connectingState = false
this.connectionState = false
addMQTTLogLine({
payload: this.$t("error.something_went_wrong"),
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
},
onConnectionSuccess() {
this.connectingState = false
this.connectionState = true
addMQTTLogLine({
payload: this.$t("state.connected_to", { name: this.url }),
source: "info",
color: "var(--accent-color)",
ts: new Date().toLocaleTimeString(),
})
this.$toast.success(this.$t("state.connected"))
},
onMessageArrived({ payloadString, destinationName }) {
addMQTTLogLine({
payload: `Message: ${payloadString} arrived on topic: ${destinationName}`,
source: "info",
color: "var(--accent-color)",
ts: new Date().toLocaleTimeString(),
})
},
toggleConnection() {
if (this.connectionState) {
this.disconnect()
} else {
this.connect()
}
},
disconnect() {
this.manualDisconnect = true
this.client.disconnect()
addMQTTLogLine({
payload: this.$t("state.disconnected_from", { name: this.url }),
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
},
onConnectionLost() {
this.connectingState = false
this.connectionState = false
if (this.manualDisconnect) {
this.$toast.error(this.$t("state.disconnected"))
} else {
this.$toast.error(this.$t("error.something_went_wrong"))
}
this.manualDisconnect = false
this.subscriptionState = false
},
publish() {
try {
this.client.publish(this.pub_topic, this.msg, 0, false)
addMQTTLogLine({
payload: `Published message: ${this.msg} to topic: ${this.pub_topic}`,
ts: new Date().toLocaleTimeString(),
source: "info",
color: "var(--accent-color)",
})
} catch (e) {
addMQTTLogLine({
payload:
this.$t("error.something_went_wrong") +
`while publishing msg: ${this.msg} to topic: ${this.pub_topic}`,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
}
},
toggleSubscription() {
if (this.subscriptionState) {
this.unsubscribe()
} else {
this.subscribe()
}
},
subscribe() {
try {
this.client.subscribe(this.sub_topic, {
onSuccess: this.usubSuccess,
onFailure: this.usubFailure,
})
} catch (e) {
addMQTTLogLine({
payload:
this.$t("error.something_went_wrong") +
`while subscribing to topic: ${this.sub_topic}`,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
}
},
usubSuccess() {
this.subscriptionState = !this.subscriptionState
addMQTTLogLine({
payload:
`Successfully ` +
(this.subscriptionState ? "subscribed" : "unsubscribed") +
` to topic: ${this.sub_topic}`,
source: "info",
color: "var(--accent-color)",
ts: new Date().toLocaleTimeString(),
})
},
usubFailure() {
addMQTTLogLine({
payload:
`Failed to ` +
(this.subscriptionState ? "unsubscribe" : "subscribe") +
` to topic: ${this.sub_topic}`,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
},
unsubscribe() {
this.client.unsubscribe(this.sub_topic, {
onSuccess: this.usubSuccess,
onFailure: this.usubFailure,
})
},
},
})
</script>

View File

@@ -0,0 +1,521 @@
<template>
<AppPaneLayout>
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"
>
<div class="inline-flex flex-1 space-x-2">
<div class="flex flex-1">
<label for="client-version">
<tippy
ref="versionOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<span class="select-wrapper">
<input
id="client-version"
v-tippy="{ theme: 'tooltip' }"
title="socket.io-client version"
class="flex px-4 py-2 font-semibold border rounded-l cursor-pointer bg-primaryLight border-divider text-secondaryDark w-26"
:value="`Client ${clientVersion}`"
readonly
:disabled="connectionState"
/>
</span>
</template>
<div class="flex flex-col" role="menu">
<SmartItem
v-for="(_, version) in socketIoClients"
:key="`client-${version}`"
:label="`Client ${version}`"
@click.native="onSelectVersion(version)"
/>
</div>
</tippy>
</label>
<input
id="socketio-url"
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
:class="{ error: !urlValid }"
class="flex flex-1 w-full px-4 py-2 border bg-primaryLight border-divider text-secondaryDark"
:placeholder="$t('socketio.url')"
:disabled="connectionState"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
<input
id="socketio-path"
v-model="path"
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
spellcheck="false"
:disabled="connectionState"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
</div>
<ButtonPrimary
id="connect"
:disabled="!urlValid"
name="connect"
class="w-32"
:label="
!connectionState ? $t('action.connect') : $t('action.disconnect')
"
:loading="connectingState"
@click.native="toggleConnection"
/>
</div>
</div>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold text-secondaryLight">
{{ $t("authorization.type") }}
</label>
<tippy
ref="authTypeOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<span class="select-wrapper">
<ButtonSecondary
class="pr-8 ml-2 rounded-none"
:label="authType"
/>
</span>
</template>
<div class="flex flex-col" role="menu">
<SmartItem
label="None"
:icon="
authType === 'None'
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:active="authType === 'None'"
@click.native="
() => {
authType = 'None'
authTypeOptions.tippy().hide()
}
"
/>
<SmartItem
label="Bearer Token"
:icon="
authType === 'Bearer'
? 'radio_button_checked'
: 'radio_button_unchecked'
"
:active="authType === 'Bearer'"
@click.native="
() => {
authType = 'Bearer'
authTypeOptions.tippy().hide()
}
"
/>
</div>
</tippy>
</span>
<div class="flex">
<SmartCheckbox
:on="authActive"
class="px-2"
@change="authActive = !authActive"
>
{{ $t("state.enabled") }}
</SmartCheckbox>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/authorization"
blank
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear')"
svg="trash-2"
@click.native="clearContent"
/>
</div>
</div>
<div
v-if="authType === 'None'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/login.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="$t('empty.authorization')"
/>
<span class="pb-4 text-center">
This SocketIO connection does not use any authentication.
</span>
<ButtonSecondary
outline
:label="$t('app.documentation')"
to="https://docs.hoppscotch.io/features/authorization"
blank
svg="external-link"
reverse
class="mb-4"
/>
</div>
<div
v-if="authType === 'Bearer'"
class="flex flex-1 border-b border-dividerLight"
>
<div class="w-2/3 border-r border-dividerLight">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="bearerToken" placeholder="Token" />
</div>
</div>
<div
class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
>
<div class="p-2">
<div class="pb-2 text-secondaryLight">
{{ $t("helpers.authorization") }}
</div>
<SmartAnchor
class="link"
:label="`${$t('authorization.learn')} \xA0 →`"
to="https://docs.hoppscotch.io/features/authorization"
blank
/>
</div>
</div>
</div>
</template>
<template #secondary>
<RealtimeLog :title="$t('socketio.log')" :log="log" />
</template>
<template #sidebar>
<div class="flex items-center justify-between p-4">
<label for="events" class="font-semibold text-secondaryLight">
{{ $t("socketio.events") }}
</label>
</div>
<div class="flex px-4">
<input
id="event_name"
v-model="communication.eventName"
class="input"
name="event_name"
:placeholder="$t('socketio.event_name')"
type="text"
autocomplete="off"
:disabled="!connectionState"
/>
</div>
<div class="flex items-center justify-between p-4">
<label class="font-semibold text-secondaryLight">
{{ $t("socketio.communication") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('add.new')"
svg="plus"
@click.native="addCommunicationInput"
/>
</div>
</div>
<div class="flex flex-col px-4 pb-4 space-y-2">
<div
v-for="(input, index) of communication.inputs"
:key="`input-${index}`"
>
<div class="flex space-x-2">
<input
v-model="communication.inputs[index]"
class="input"
name="message"
:placeholder="$t('count.message', { count: index + 1 })"
type="text"
autocomplete="off"
:disabled="!connectionState"
@keyup.enter="connectionState ? sendMessage() : null"
/>
<ButtonSecondary
v-if="index + 1 !== communication.inputs.length"
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.remove')"
svg="trash"
color="red"
outline
@click.native="removeCommunicationInput({ index })"
/>
<ButtonPrimary
v-if="index + 1 === communication.inputs.length"
id="send"
name="send"
:disabled="!connectionState"
:label="$t('action.send')"
@click.native="sendMessage"
/>
</div>
</div>
</div>
</template>
</AppPaneLayout>
</template>
<script>
import { defineComponent, ref } from "@nuxtjs/composition-api"
// All Socket.IO client version imports
import ClientV2 from "socket.io-client-v2"
import { io as ClientV3 } from "socket.io-client-v3"
import { io as ClientV4 } from "socket.io-client-v4"
import wildcard from "socketio-wildcard"
import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import {
SIOEndpoint$,
setSIOEndpoint,
SIOVersion$,
setSIOVersion,
SIOPath$,
setSIOPath,
SIOConnectionState$,
SIOConnectingState$,
setSIOConnectionState,
setSIOConnectingState,
SIOSocket$,
setSIOSocket,
SIOLog$,
setSIOLog,
addSIOLogLine,
} from "~/newstore/SocketIOSession"
import { useStream } from "~/helpers/utils/composables"
const socketIoClients = {
v4: ClientV4,
v3: ClientV3,
v2: ClientV2,
}
export default defineComponent({
setup() {
return {
socketIoClients,
url: useStream(SIOEndpoint$, "", setSIOEndpoint),
clientVersion: useStream(SIOVersion$, "", setSIOVersion),
path: useStream(SIOPath$, "", setSIOPath),
connectingState: useStream(
SIOConnectingState$,
false,
setSIOConnectingState
),
connectionState: useStream(
SIOConnectionState$,
false,
setSIOConnectionState
),
io: useStream(SIOSocket$, null, setSIOSocket),
log: useStream(SIOLog$, [], setSIOLog),
authTypeOptions: ref(null),
}
},
data() {
return {
isUrlValid: true,
communication: {
eventName: "",
inputs: [""],
},
authType: "None",
bearerToken: "",
authActive: true,
}
},
computed: {
urlValid() {
return this.isUrlValid
},
},
watch: {
url() {
this.debouncer()
},
connectionState(connected) {
if (connected) this.$refs.versionOptions.tippy().disable()
else this.$refs.versionOptions.tippy().enable()
},
},
created() {
if (process.browser) {
this.worker = this.$worker.createRejexWorker()
this.worker.addEventListener("message", this.workerResponseHandler)
}
},
destroyed() {
this.worker.terminate()
},
methods: {
debouncer: debounce(function () {
this.worker.postMessage({ type: "socketio", url: this.url })
}, 1000),
workerResponseHandler({ data }) {
if (data.url === this.url) this.isUrlValid = data.result
},
removeCommunicationInput({ index }) {
this.$delete(this.communication.inputs, index)
},
addCommunicationInput() {
this.communication.inputs.push("")
},
toggleConnection() {
// If it is connecting:
if (!this.connectionState) return this.connect()
// Otherwise, it's disconnecting.
else return this.disconnect()
},
connect() {
this.connectingState = true
this.log = [
{
payload: this.$t("state.connecting_to", { name: this.url }),
source: "info",
color: "var(--accent-color)",
},
]
try {
if (!this.path) {
this.path = "/socket.io"
}
const Client = socketIoClients[this.clientVersion]
if (this.authActive && this.authType === "Bearer") {
this.io = new Client(this.url, {
path: this.path,
auth: {
token: this.bearerToken,
},
})
} else {
this.io = new Client(this.url, { path: this.path })
}
// Add ability to listen to all events
wildcard(Client.Manager)(this.io)
this.io.on("connect", () => {
this.connectingState = false
this.connectionState = true
this.log = [
{
payload: this.$t("state.connected_to", { name: this.url }),
source: "info",
color: "var(--accent-color)",
ts: new Date().toLocaleTimeString(),
},
]
this.$toast.success(this.$t("state.connected"))
})
this.io.on("*", ({ data }) => {
const [eventName, message] = data
addSIOLogLine({
payload: `[${eventName}] ${message ? JSON.stringify(message) : ""}`,
source: "server",
ts: new Date().toLocaleTimeString(),
})
})
this.io.on("connect_error", (error) => {
this.handleError(error)
})
this.io.on("reconnect_error", (error) => {
this.handleError(error)
})
this.io.on("error", () => {
this.handleError()
})
this.io.on("disconnect", () => {
this.connectingState = false
this.connectionState = false
addSIOLogLine({
payload: this.$t("state.disconnected_from", { name: this.url }),
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
this.$toast.error(this.$t("state.disconnected"))
})
} catch (e) {
this.handleError(e)
this.$toast.error(this.$t("error.something_went_wrong"))
}
logHoppRequestRunToAnalytics({
platform: "socketio",
})
},
disconnect() {
this.io.close()
},
handleError(error) {
this.disconnect()
this.connectingState = false
this.connectionState = false
addSIOLogLine({
payload: this.$t("error.something_went_wrong"),
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
if (error !== null)
addSIOLogLine({
payload: error,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
},
sendMessage() {
const eventName = this.communication.eventName
const messages = (this.communication.inputs || [])
.map((input) => {
try {
return JSON.parse(input)
} catch (e) {
return input
}
})
.filter((message) => !!message)
if (this.io) {
this.io.emit(eventName, ...messages, (data) => {
// receive response from server
addSIOLogLine({
payload: `[${eventName}] ${JSON.stringify(data)}`,
source: "server",
ts: new Date().toLocaleTimeString(),
})
})
addSIOLogLine({
payload: `[${eventName}] ${JSON.stringify(messages)}`,
source: "client",
ts: new Date().toLocaleTimeString(),
})
this.communication.inputs = [""]
}
},
onSelectVersion(version) {
this.clientVersion = version
this.$refs.versionOptions.tippy().hide()
},
},
})
</script>

View File

@@ -0,0 +1,222 @@
<template>
<AppPaneLayout>
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"
>
<div class="inline-flex flex-1 space-x-2">
<div class="flex flex-1">
<input
id="server"
v-model="server"
type="url"
autocomplete="off"
:class="{ error: !serverValid }"
class="flex flex-1 w-full px-4 py-2 border rounded-l bg-primaryLight border-divider text-secondaryDark"
:placeholder="$t('sse.url')"
:disabled="connectionSSEState"
@keyup.enter="serverValid ? toggleSSEConnection() : null"
/>
<label
for="event-type"
class="px-4 py-2 font-semibold truncate border-t border-b bg-primaryLight border-divider text-secondaryLight"
>
{{ $t("sse.event_type") }}
</label>
<input
id="event-type"
v-model="eventType"
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
spellcheck="false"
:disabled="connectionSSEState"
@keyup.enter="serverValid ? toggleSSEConnection() : null"
/>
</div>
<ButtonPrimary
id="start"
:disabled="!serverValid"
name="start"
class="w-32"
:label="
!connectionSSEState ? $t('action.start') : $t('action.stop')
"
:loading="connectingState"
@click.native="toggleSSEConnection"
/>
</div>
</div>
</template>
<template #secondary>
<RealtimeLog :title="$t('sse.log')" :log="log" />
</template>
</AppPaneLayout>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import "splitpanes/dist/splitpanes.css"
import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import {
SSEEndpoint$,
setSSEEndpoint,
SSEEventType$,
setSSEEventType,
SSESocket$,
setSSESocket,
SSEConnectingState$,
SSEConnectionState$,
setSSEConnectionState,
setSSEConnectingState,
SSELog$,
setSSELog,
addSSELogLine,
} from "~/newstore/SSESession"
import { useStream } from "~/helpers/utils/composables"
export default defineComponent({
setup() {
return {
connectionSSEState: useStream(
SSEConnectionState$,
false,
setSSEConnectionState
),
connectingState: useStream(
SSEConnectingState$,
false,
setSSEConnectingState
),
server: useStream(SSEEndpoint$, "", setSSEEndpoint),
eventType: useStream(SSEEventType$, "", setSSEEventType),
sse: useStream(SSESocket$, null, setSSESocket),
log: useStream(SSELog$, [], setSSELog),
}
},
data() {
return {
isUrlValid: true,
}
},
computed: {
serverValid() {
return this.isUrlValid
},
},
watch: {
server() {
this.debouncer()
},
},
created() {
if (process.browser) {
this.worker = this.$worker.createRejexWorker()
this.worker.addEventListener("message", this.workerResponseHandler)
}
},
destroyed() {
this.worker.terminate()
},
methods: {
debouncer: debounce(function () {
this.worker.postMessage({ type: "sse", url: this.server })
}, 1000),
workerResponseHandler({ data }) {
if (data.url === this.url) this.isUrlValid = data.result
},
toggleSSEConnection() {
// If it is connecting:
if (!this.connectionSSEState) return this.start()
// Otherwise, it's disconnecting.
else return this.stop()
},
start() {
this.connectingState = true
this.log = [
{
payload: this.$t("state.connecting_to", { name: this.server }),
source: "info",
color: "var(--accent-color)",
},
]
if (typeof EventSource !== "undefined") {
try {
this.sse = new EventSource(this.server)
this.sse.onopen = () => {
this.connectingState = false
this.connectionSSEState = true
this.log = [
{
payload: this.$t("state.connected_to", { name: this.server }),
source: "info",
color: "var(--accent-color)",
ts: new Date().toLocaleTimeString(),
},
]
this.$toast.success(this.$t("state.connected"))
}
this.sse.onerror = () => {
this.handleSSEError()
}
this.sse.onclose = () => {
this.connectionSSEState = false
addSSELogLine({
payload: this.$t("state.disconnected_from", {
name: this.server,
}),
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
this.$toast.error(this.$t("state.disconnected"))
}
this.sse.addEventListener(this.eventType, ({ data }) => {
addSSELogLine({
payload: data,
source: "server",
ts: new Date().toLocaleTimeString(),
})
})
} catch (e) {
this.handleSSEError(e)
this.$toast.error(this.$t("error.something_went_wrong"))
}
} else {
this.log = [
{
payload: this.$t("error.browser_support_sse"),
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
},
]
}
logHoppRequestRunToAnalytics({
platform: "sse",
})
},
handleSSEError(error) {
this.stop()
this.connectionSSEState = false
addSSELogLine({
payload: this.$t("error.something_went_wrong"),
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
if (error !== null)
addSSELogLine({
payload: error,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
},
stop() {
this.sse.close()
this.sse.onclose()
},
},
})
</script>

View File

@@ -0,0 +1,433 @@
<template>
<AppPaneLayout>
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"
>
<div class="inline-flex flex-1 space-x-2">
<input
id="websocket-url"
v-model="url"
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
type="url"
autocomplete="off"
spellcheck="false"
:class="{ error: !urlValid }"
:placeholder="$t('websocket.url')"
:disabled="connectionState"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
<ButtonPrimary
id="connect"
:disabled="!urlValid"
class="w-32"
name="connect"
:label="
!connectionState ? $t('action.connect') : $t('action.disconnect')
"
:loading="connectingState"
@click.native="toggleConnection"
/>
</div>
</div>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ $t("websocket.protocols") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear_all')"
svg="trash-2"
@click.native="clearContent"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('add.new')"
svg="plus"
@click.native="addProtocol"
/>
</div>
</div>
<draggable
v-model="protocols"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<div
v-for="(protocol, index) of protocols"
:key="`protocol-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<ButtonSecondary
svg="grip-vertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== protocols?.length - 1,
}"
tabindex="-1"
/>
</span>
<input
v-model="protocol.value"
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="$t('count.protocol', { count: index + 1 })"
name="message"
type="text"
autocomplete="off"
@change="
updateProtocol(index, {
value: $event.target.value,
active: protocol.active,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
protocol.hasOwnProperty('active')
? protocol.active
? $t('action.turn_off')
: $t('action.turn_on')
: $t('action.turn_off')
"
:svg="
protocol.hasOwnProperty('active')
? protocol.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateProtocol(index, {
value: protocol.value,
active: !protocol.active,
})
"
/>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.remove')"
svg="trash"
color="red"
@click.native="deleteProtocol({ index })"
/>
</span>
</div>
</draggable>
<div
v-if="protocols.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_category.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="$t('empty.protocols')"
/>
<span class="mb-4 text-center">
{{ $t("empty.protocols") }}
</span>
</div>
</template>
<template #secondary>
<RealtimeLog :title="$t('websocket.log')" :log="log" />
</template>
<template #sidebar>
<div class="flex items-center justify-between p-4">
<label
for="websocket-message"
class="font-semibold text-secondaryLight"
>
{{ $t("websocket.communication") }}
</label>
</div>
<div class="flex px-4 space-x-2">
<input
id="websocket-message"
v-model="communication.input"
name="message"
type="text"
autocomplete="off"
:disabled="!connectionState"
:placeholder="$t('websocket.message')"
class="input"
@keyup.enter="connectionState ? sendMessage() : null"
@keyup.up="connectionState ? walkHistory('up') : null"
@keyup.down="connectionState ? walkHistory('down') : null"
/>
<ButtonPrimary
id="send"
name="send"
:disabled="!connectionState"
:label="$t('action.send')"
@click.native="sendMessage"
/>
</div>
</template>
</AppPaneLayout>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import debounce from "lodash/debounce"
import draggable from "vuedraggable"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import {
setWSEndpoint,
WSEndpoint$,
WSProtocols$,
setWSProtocols,
addWSProtocol,
deleteWSProtocol,
updateWSProtocol,
deleteAllWSProtocols,
WSSocket$,
setWSSocket,
setWSConnectionState,
setWSConnectingState,
WSConnectionState$,
WSConnectingState$,
addWSLogLine,
WSLog$,
setWSLog,
} from "~/newstore/WebSocketSession"
import { useStream } from "~/helpers/utils/composables"
export default defineComponent({
components: {
draggable,
},
setup() {
return {
url: useStream(WSEndpoint$, "", setWSEndpoint),
protocols: useStream(WSProtocols$, [], setWSProtocols),
connectionState: useStream(
WSConnectionState$,
false,
setWSConnectionState
),
connectingState: useStream(
WSConnectingState$,
false,
setWSConnectingState
),
socket: useStream(WSSocket$, null, setWSSocket),
log: useStream(WSLog$, [], setWSLog),
}
},
data() {
return {
isUrlValid: true,
communication: {
input: "",
},
currentIndex: -1, // index of the message log array to put in input box
activeProtocols: [],
}
},
computed: {
urlValid() {
return this.isUrlValid
},
},
watch: {
url() {
this.debouncer()
},
protocols: {
handler(newVal) {
this.activeProtocols = newVal
.filter((item) =>
Object.prototype.hasOwnProperty.call(item, "active")
? item.active === true
: true
)
.map(({ value }) => value)
},
deep: true,
},
},
created() {
if (process.browser) {
this.worker = this.$worker.createRejexWorker()
this.worker.addEventListener("message", this.workerResponseHandler)
}
},
destroyed() {
this.worker.terminate()
},
methods: {
clearContent() {
deleteAllWSProtocols()
},
debouncer: debounce(function () {
this.worker.postMessage({ type: "ws", url: this.url })
}, 1000),
workerResponseHandler({ data }) {
if (data.url === this.url) this.isUrlValid = data.result
},
toggleConnection() {
// If it is connecting:
if (!this.connectionState) return this.connect()
// Otherwise, it's disconnecting.
else return this.disconnect()
},
connect() {
this.log = [
{
payload: this.$t("state.connecting_to", { name: this.url }),
source: "info",
color: "var(--accent-color)",
},
]
try {
this.connectingState = true
this.socket = new WebSocket(this.url, this.activeProtocols)
this.socket.onopen = () => {
this.connectingState = false
this.connectionState = true
this.log = [
{
payload: this.$t("state.connected_to", { name: this.url }),
source: "info",
color: "var(--accent-color)",
ts: new Date().toLocaleTimeString(),
},
]
this.$toast.success(this.$t("state.connected"))
}
this.socket.onerror = () => {
this.handleError()
}
this.socket.onclose = () => {
this.connectionState = false
addWSLogLine({
payload: this.$t("state.disconnected_from", { name: this.url }),
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
this.$toast.error(this.$t("state.disconnected"))
}
this.socket.onmessage = ({ data }) => {
addWSLogLine({
payload: data,
source: "server",
ts: new Date().toLocaleTimeString(),
})
}
} catch (e) {
this.handleError(e)
this.$toast.error(this.$t("error.something_went_wrong"))
}
logHoppRequestRunToAnalytics({
platform: "wss",
})
},
disconnect() {
if (this.socket) {
this.socket.close()
this.connectionState = false
this.connectingState = false
}
},
handleError(error) {
this.disconnect()
this.connectionState = false
addWSLogLine({
payload: this.$t("error.something_went_wrong"),
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
if (error !== null)
addWSLogLine({
payload: error,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString(),
})
},
sendMessage() {
const message = this.communication.input
this.socket.send(message)
addWSLogLine({
payload: message,
source: "client",
ts: new Date().toLocaleTimeString(),
})
this.communication.input = ""
},
walkHistory(direction) {
const clientMessages = this.log.filter(
({ source }) => source === "client"
)
const length = clientMessages.length
switch (direction) {
case "up":
if (length > 0 && this.currentIndex !== 0) {
// does nothing if message log is empty or the currentIndex is 0 when up arrow is pressed
if (this.currentIndex === -1) {
this.currentIndex = length - 1
this.communication.input =
clientMessages[this.currentIndex].payload
} else if (this.currentIndex === 0) {
this.communication.input = clientMessages[0].payload
} else if (this.currentIndex > 0) {
this.currentIndex = this.currentIndex - 1
this.communication.input =
clientMessages[this.currentIndex].payload
}
}
break
case "down":
if (length > 0 && this.currentIndex > -1) {
if (this.currentIndex === length - 1) {
this.currentIndex = -1
this.communication.input = ""
} else if (this.currentIndex < length - 1) {
this.currentIndex = this.currentIndex + 1
this.communication.input =
clientMessages[this.currentIndex].payload
}
}
break
}
},
addProtocol() {
addWSProtocol({ value: "", active: true })
},
deleteProtocol({ index }) {
const oldProtocols = this.protocols.slice()
deleteWSProtocol(index)
this.$toast.success(this.$t("state.deleted"), {
action: {
text: this.$t("action.undo"),
duration: 4000,
onClick: (_, toastObject) => {
this.protocols = oldProtocols
toastObject.remove()
},
},
})
},
updateProtocol(index, updated) {
updateWSProtocol(index, updated)
},
},
})
</script>

View File

@@ -16,12 +16,7 @@
</template>
<template #footer>
<span>
<ButtonPrimary
v-focus
:label="yes"
:loading="!!loadingState"
@click.native="resolve"
/>
<ButtonPrimary v-focus :label="yes" @click.native="resolve" />
<ButtonSecondary :label="no" @click.native="hideModal" />
</span>
</template>
@@ -47,15 +42,14 @@ export default defineComponent({
return this.$t("action.no")
},
},
loadingState: { type: Boolean, default: null },
},
methods: {
hideModal() {
this.$emit("hide-modal")
},
resolve() {
this.$emit("resolve", this.title)
if (this.loadingState === null) this.$emit("hide-modal")
this.$emit("resolve")
this.$emit("hide-modal")
},
},
})

View File

@@ -29,19 +29,14 @@ import {
placeholder as placeholderExt,
ViewPlugin,
ViewUpdate,
keymap,
} from "@codemirror/view"
import { EditorState, Extension } from "@codemirror/state"
import clone from "lodash/clone"
import { tooltips } from "@codemirror/tooltip"
import { history, historyKeymap } from "@codemirror/history"
import { HoppRESTVar } from "@hoppscotch/data"
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { HoppReactiveVarPlugin } from "~/helpers/editor/extensions/HoppVariable"
import { restVars$ } from "~/newstore/RESTSession"
const props = withDefaults(
defineProps<{
@@ -49,18 +44,14 @@ const props = withDefaults(
placeholder: string
styles: string
envs: { key: string; value: string; source: string }[] | null
vars: { key: string; value: string }[] | null
focus: boolean
readonly: boolean
}>(),
{
value: "",
placeholder: "",
styles: "",
envs: null,
vars: null,
focus: false,
readonly: false,
}
)
@@ -114,7 +105,6 @@ let pastedValue: string | null = null
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
AggregateEnvironment[]
>
const aggregateVars = useReadonlyStream(restVars$, []) as Ref<HoppRESTVar[]>
const envVars = computed(() =>
props.envs
@@ -126,43 +116,15 @@ const envVars = computed(() =>
: aggregateEnvs.value
)
const varVars = computed(() =>
props.vars
? props.vars.map((x) => ({
key: x.key,
value: x.value,
}))
: aggregateVars.value
)
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
const varTooltipPlugin = new HoppReactiveVarPlugin(varVars, view)
const initView = (el: any) => {
const extensions: Extension = [
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",
}),
envTooltipPlugin,
varTooltipPlugin,
placeholderExt(props.placeholder),
EditorView.domEventHandlers({
paste(ev) {
@@ -176,8 +138,6 @@ const initView = (el: any) => {
ViewPlugin.fromClass(
class {
update(update: ViewUpdate) {
if (props.readonly) return
if (update.docChanged) {
const prevValue = clone(cachedValue.value)
@@ -212,8 +172,6 @@ const initView = (el: any) => {
}
}
),
history(),
keymap.of([...historyKeymap]),
]
view.value = new EditorView({

View File

@@ -1,31 +1,33 @@
<template>
<SmartItem
:label="label"
:icon="selected ? 'radio_button_checked' : 'radio_button_unchecked'"
:active="selected"
:icon="
value === selected ? 'radio_button_checked' : 'radio_button_unchecked'
"
:active="value === selected"
role="radio"
:aria-checked="selected"
@click.native="emit('change', value)"
:aria-checked="value === selected"
@click.native="$emit('change', value)"
/>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(e: "change", value: string): void
}>()
<script>
import { defineComponent } from "@nuxtjs/composition-api"
defineProps({
value: {
type: String,
default: "",
},
label: {
type: String,
default: "",
},
selected: {
type: Boolean,
default: false,
export default defineComponent({
props: {
value: {
type: String,
default: "",
},
label: {
type: String,
default: "",
},
selected: {
type: String,
default: "",
},
},
})
</script>

View File

@@ -5,22 +5,18 @@
:key="`radio-${index}`"
:value="radio.value"
:label="radio.label"
:selected="value === radio.value"
@change="emit('input', radio.value)"
:selected="selected"
@change="$emit('change', radio.value)"
/>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(e: "input", value: string): void
}>()
defineProps<{
radios: Array<{
value: string // The key of the radio option
value: string
label: string
}>
value: string // Should be a radio key given in the radios array
selected: string
}>()
</script>

View File

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

View File

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

View File

@@ -149,7 +149,7 @@ export class GQLConnection {
.forEach((x) => (finalHeaders[x.key] = x.value))
const reqOptions = {
method: "POST",
method: "post",
url,
headers: {
...finalHeaders,
@@ -213,7 +213,7 @@ export class GQLConnection {
.forEach(({ key, value }) => (finalHeaders[key] = value))
const reqOptions = {
method: "POST",
method: "post",
url,
headers: {
...finalHeaders,

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
mutation DeleteShortcode($code: ID!) {
revokeShortcode(code: $code)
}

View File

@@ -1,7 +0,0 @@
query GetUserShortcodes($cursor: ID) {
myShortcodes(cursor: $cursor) {
id
request
createdOn
}
}

View File

@@ -1,7 +0,0 @@
subscription ShortcodeCreated {
myShortcodesCreated {
id
request
createdOn
}
}

View File

@@ -1,5 +0,0 @@
subscription ShortcodeDeleted {
myShortcodesRevoked {
id
}
}

View File

@@ -17,7 +17,7 @@ import {
GetCollectionTitleDocument,
} from "./graphql"
export const BACKEND_PAGE_SIZE = 10
const BACKEND_PAGE_SIZE = 10
const getCollectionChildrenIDs = async (collID: string) => {
const collsList: string[] = []

View File

@@ -4,13 +4,8 @@ import {
CreateShortcodeDocument,
CreateShortcodeMutation,
CreateShortcodeMutationVariables,
DeleteShortcodeDocument,
DeleteShortcodeMutation,
DeleteShortcodeMutationVariables,
} from "../graphql"
type DeleteShortcodeErrors = "shortcode/not_found"
export const createShortcode = (request: HoppRESTRequest) =>
runMutation<CreateShortcodeMutation, CreateShortcodeMutationVariables, "">(
CreateShortcodeDocument,
@@ -18,12 +13,3 @@ export const createShortcode = (request: HoppRESTRequest) =>
request: JSON.stringify(request),
}
)
export const deleteShortcode = (code: string) =>
runMutation<
DeleteShortcodeMutation,
DeleteShortcodeMutationVariables,
DeleteShortcodeErrors
>(DeleteShortcodeDocument, {
code,
})

View File

@@ -768,83 +768,11 @@ 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: "",
}),
},
{
command: `curl https://example.com -d "alpha=beta&request_id=4"`,
response: makeRESTRequest({
method: "POST",
name: "Untitled request",
endpoint: "https://example.com/",
auth: {
authType: "none",
authActive: true,
},
body: {
contentType: "application/x-www-form-urlencoded",
body: rawKeyValueEntriesToString([
{
active: true,
key: "alpha",
value: "beta",
},
{
active: true,
key: "request_id",
value: "4",
},
]),
},
params: [],
headers: [],
preRequestScript: "",
testScript: "",
}),
},
]
describe("Parse curl command to Hopp REST Request", () => {
describe("parseCurlToHoppRESTReq", () => {
for (const [i, { command, response }] of samples.entries()) {
test(`for sample #${i + 1}:\n\n${command}`, () => {
test(`matches expectation for sample #${i + 1}`, () => {
expect(parseCurlToHoppRESTReq(command)).toEqual(response)
})
}

View File

@@ -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, getURLObject } from "./sub_helpers/url"
import { concatParams, parseURL } 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 = getURLObject(parsedArguments)
const urlObject = parseURL(parsedArguments)
const auth = getAuthObject(parsedArguments, headers, urlObject)
let rawData: string | string[] = pipe(
@@ -93,8 +93,7 @@ export const parseCurlCommand = (curlCommand: string) => {
hasBodyBeenParsed = true
} else if (
rawContentType.includes("application/x-www-form-urlencoded") &&
!!pairs &&
Array.isArray(rawData)
!!pairs
) {
body = pairs.map((p) => p.join(": ")).join("\n") || null
contentType = "application/x-www-form-urlencoded"

View File

@@ -161,7 +161,8 @@ const getXMLBody = (rawData: string) =>
const getFormattedJSON = flow(
safeParseJSON,
O.map((parsedJSON) => JSON.stringify(parsedJSON, null, 2)),
O.getOrElse(() => "{ }")
O.getOrElse(() => "{}"),
O.of
)
const getXWWWFormUrlEncodedBody = flow(
@@ -188,7 +189,7 @@ export function parseBody(
case "application/ld+json":
case "application/vnd.api+json":
case "application/json":
return O.some(getFormattedJSON(rawData))
return getFormattedJSON(rawData)
case "application/x-www-form-urlencoded":
return getXWWWFormUrlEncodedBody(rawData)

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