Compare commits
42 Commits
fix/save-o
...
feat/realt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d13b381097 | ||
|
|
663da34e08 | ||
|
|
eb6c4f1a05 | ||
|
|
b132077cdd | ||
|
|
f6950bac0f | ||
|
|
83bdd03f43 | ||
|
|
b1a2c9e9d5 | ||
|
|
346ac8bde9 | ||
|
|
cfa89a6ded | ||
|
|
184914ba4f | ||
|
|
432337b801 | ||
|
|
d04520698d | ||
|
|
450af983e2 | ||
|
|
c36e421d13 | ||
|
|
fb1da491d8 | ||
|
|
127bd7318f | ||
|
|
c3b784c680 | ||
|
|
df55807fa4 | ||
|
|
4ef2844a22 | ||
|
|
c20339d222 | ||
|
|
514210e167 | ||
|
|
50744136d0 | ||
|
|
07735994e1 | ||
|
|
00acf06700 | ||
|
|
9486279b02 | ||
|
|
021908c8d1 | ||
|
|
bf97d8811c | ||
|
|
2452b1be4b | ||
|
|
7bf76a5812 | ||
|
|
d1b339df5d | ||
|
|
06937fe9e8 | ||
|
|
62a5beb52f | ||
|
|
100e562dcc | ||
|
|
09af858fbc | ||
|
|
01acbc8db6 | ||
|
|
f1c42f28de | ||
|
|
c99b224829 | ||
|
|
ede27e0600 | ||
|
|
99148a0a0e | ||
|
|
9232aad184 | ||
|
|
f7ca3f8bd1 | ||
|
|
ff51b7e5df |
34
.github/workflows/deploy-netlify.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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: Install pnpm
|
||||||
|
run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Setup Environment
|
||||||
|
run: mv packages/hoppscotch-app/.env.example packages/hoppscotch-app/.env
|
||||||
|
|
||||||
|
- name: Build Site
|
||||||
|
run: pnpm run generate
|
||||||
|
|
||||||
|
# Deploy the site with netlify-cli
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
uses: netlify/actions/cli@master
|
||||||
|
env:
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
with:
|
||||||
|
args: deploy --dir=packages/hoppscotch-app/dist --prod
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"lint-staged": "^12.3.7"
|
"lint-staged": "^12.3.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^16.2.3",
|
"@commitlint/cli": "^16.2.3",
|
||||||
"@commitlint/config-conventional": "^16.2.1",
|
"@commitlint/config-conventional": "^16.2.1",
|
||||||
"@types/node": "^17.0.23"
|
"@types/node": "^17.0.24"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^0.15.4",
|
"@lezer/generator": "^0.15.4",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
"rollup": "^2.70.1",
|
"rollup": "^2.70.2",
|
||||||
"rollup-plugin-dts": "^4.2.1",
|
"rollup-plugin-dts": "^4.2.1",
|
||||||
"rollup-plugin-ts": "^2.0.5",
|
"rollup-plugin-ts": "^2.0.7",
|
||||||
"typescript": "^4.6.3"
|
"typescript": "^4.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,15 +338,17 @@ TypeSystemDirectiveLocation {
|
|||||||
| @specialize<Name, "INPUT_FIELD_DEFINITION">
|
| @specialize<Name, "INPUT_FIELD_DEFINITION">
|
||||||
}
|
}
|
||||||
|
|
||||||
@skip { Whitespace | Comment }
|
@skip { whitespace | Comment }
|
||||||
|
|
||||||
@tokens {
|
@tokens {
|
||||||
Whitespace {
|
whitespace {
|
||||||
std.whitespace+
|
std.whitespace+
|
||||||
}
|
}
|
||||||
|
|
||||||
StringValue {
|
StringValue {
|
||||||
"\"\"\"" (!["] | "\\n" | "\"" "\""? !["])* "\"\"\"" | "\"" !["\\\n]* "\""
|
"\"\"\"" (!["] | "\\n" | "\"" "\""? !["])* "\"\"\"" | "\"" !["\\\n]* "\""
|
||||||
}
|
}
|
||||||
|
|
||||||
IntValue {
|
IntValue {
|
||||||
"-"? "0"
|
"-"? "0"
|
||||||
| "-"? std.digit+
|
| "-"? std.digit+
|
||||||
@@ -367,6 +369,8 @@ TypeSystemDirectiveLocation {
|
|||||||
Comma {
|
Comma {
|
||||||
","
|
","
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"{" "}" "[" "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
@detectDelim
|
@detectDelim
|
||||||
4
packages/hoppscotch-app/assets/icons/arrow-down-left.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 283 B |
4
packages/hoppscotch-app/assets/icons/arrow-down.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 285 B |
4
packages/hoppscotch-app/assets/icons/arrow-up-right.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 283 B |
4
packages/hoppscotch-app/assets/icons/arrow-up.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 284 B |
4
packages/hoppscotch-app/assets/icons/chevrons-down.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 286 B |
4
packages/hoppscotch-app/assets/icons/chevrons-up.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 287 B |
5
packages/hoppscotch-app/assets/icons/info-disconnect.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 329 B |
5
packages/hoppscotch-app/assets/icons/info-realtime.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 329 B |
4
packages/hoppscotch-app/assets/icons/send.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 292 B |
@@ -8,11 +8,7 @@
|
|||||||
{{ t("settings.interceptor_description") }}
|
{{ t("settings.interceptor_description") }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<SmartRadioGroup
|
<SmartRadioGroup v-model="interceptorSelection" :radios="interceptors" />
|
||||||
:radios="interceptors"
|
|
||||||
:selected="interceptorSelection"
|
|
||||||
@change="toggleSettingKey"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion"
|
v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion"
|
||||||
class="flex space-x-2"
|
class="flex space-x-2"
|
||||||
@@ -38,58 +34,29 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watchEffect } from "@nuxtjs/composition-api"
|
import { computed } from "@nuxtjs/composition-api"
|
||||||
import { KeysMatching } from "~/types/ts-utils"
|
import { applySetting, toggleSetting, useSetting } from "~/newstore/settings"
|
||||||
import {
|
import { useI18n, useReadonlyStream } from "~/helpers/utils/composables"
|
||||||
applySetting,
|
import { extensionStatus$ } from "~/newstore/HoppExtension"
|
||||||
SettingsType,
|
|
||||||
toggleSetting,
|
|
||||||
useSetting,
|
|
||||||
} from "~/newstore/settings"
|
|
||||||
import { hasExtensionInstalled } from "~/helpers/strategies/ExtensionStrategy"
|
|
||||||
import { useI18n, usePolled } from "~/helpers/utils/composables"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
|
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
|
||||||
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
|
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
|
||||||
|
|
||||||
const toggleSettingKey = <
|
const currentExtensionStatus = useReadonlyStream(extensionStatus$, null)
|
||||||
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 = usePolled(5000, (stopPolling) => {
|
const extensionVersion = computed(() => {
|
||||||
const result = hasExtensionInstalled()
|
return currentExtensionStatus.value === "available"
|
||||||
? window.__POSTWOMAN_EXTENSION_HOOK__.getVersion()
|
? window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() ?? null
|
||||||
: null
|
: null
|
||||||
|
|
||||||
// We don't need to poll anymore after we get value
|
|
||||||
if (result) stopPolling()
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const interceptors = computed(() => [
|
const interceptors = computed(() => [
|
||||||
{ value: "BROWSER_ENABLED", label: t("state.none") },
|
{ value: "BROWSER_ENABLED" as const, label: t("state.none") },
|
||||||
{ value: "PROXY_ENABLED", label: t("settings.proxy") },
|
{ value: "PROXY_ENABLED" as const, label: t("settings.proxy") },
|
||||||
{
|
{
|
||||||
value: "EXTENSIONS_ENABLED",
|
value: "EXTENSIONS_ENABLED" as const,
|
||||||
label:
|
label:
|
||||||
`${t("settings.extensions")}: ` +
|
`${t("settings.extensions")}: ` +
|
||||||
(extensionVersion.value !== null
|
(extensionVersion.value !== null
|
||||||
@@ -98,15 +65,27 @@ const interceptors = computed(() => [
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const interceptorSelection = ref("")
|
type InterceptorMode = typeof interceptors["value"][number]["value"]
|
||||||
|
|
||||||
watchEffect(() => {
|
const interceptorSelection = computed<InterceptorMode>({
|
||||||
if (PROXY_ENABLED.value) {
|
get() {
|
||||||
interceptorSelection.value = "PROXY_ENABLED"
|
if (PROXY_ENABLED.value) return "PROXY_ENABLED"
|
||||||
} else if (EXTENSIONS_ENABLED.value) {
|
if (EXTENSIONS_ENABLED.value) return "EXTENSIONS_ENABLED"
|
||||||
interceptorSelection.value = "EXTENSIONS_ENABLED"
|
return "BROWSER_ENABLED"
|
||||||
} else {
|
},
|
||||||
interceptorSelection.value = "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)
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</Splitpanes>
|
</Splitpanes>
|
||||||
</Pane>
|
</Pane>
|
||||||
<Pane
|
<Pane
|
||||||
v-if="SIDEBAR"
|
v-if="SIDEBAR && hasSidebar"
|
||||||
size="25"
|
size="25"
|
||||||
min-size="20"
|
min-size="20"
|
||||||
class="hide-scrollbar !overflow-auto flex flex-col"
|
class="hide-scrollbar !overflow-auto flex flex-col"
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
import { Splitpanes, Pane } from "splitpanes"
|
import { Splitpanes, Pane } from "splitpanes"
|
||||||
import "splitpanes/dist/splitpanes.css"
|
import "splitpanes/dist/splitpanes.css"
|
||||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||||
|
import { computed, useSlots } from "@nuxtjs/composition-api"
|
||||||
import { useSetting } from "~/newstore/settings"
|
import { useSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
||||||
@@ -52,4 +53,8 @@ const mdAndLarger = breakpoints.greater("md")
|
|||||||
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
||||||
|
|
||||||
const SIDEBAR = useSetting("SIDEBAR")
|
const SIDEBAR = useSetting("SIDEBAR")
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
|
||||||
|
const hasSidebar = computed(() => !!slots.sidebar)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
]"
|
]"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:tabindex="loading ? '-1' : '0'"
|
:tabindex="loading ? '-1' : '0'"
|
||||||
:type="type"
|
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -67,79 +66,41 @@
|
|||||||
</SmartLink>
|
</SmartLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
interface Props {
|
||||||
|
to: string
|
||||||
export default defineComponent({
|
exact: boolean
|
||||||
props: {
|
blank: boolean
|
||||||
to: {
|
label: string
|
||||||
type: String,
|
icon: string
|
||||||
default: "",
|
svg: string
|
||||||
},
|
color: string
|
||||||
exact: {
|
disabled: boolean
|
||||||
type: Boolean,
|
loading: boolean
|
||||||
default: true,
|
large: boolean
|
||||||
},
|
shadow: boolean
|
||||||
blank: {
|
reverse: boolean
|
||||||
type: Boolean,
|
rounded: boolean
|
||||||
default: false,
|
gradient: boolean
|
||||||
},
|
outline: boolean
|
||||||
label: {
|
shortcut: string[]
|
||||||
type: String,
|
}
|
||||||
default: "",
|
withDefaults(defineProps<Props>(), {
|
||||||
},
|
to: "",
|
||||||
icon: {
|
exact: true,
|
||||||
type: String,
|
blank: false,
|
||||||
default: "",
|
label: "",
|
||||||
},
|
icon: "",
|
||||||
svg: {
|
svg: "",
|
||||||
type: String,
|
color: "",
|
||||||
default: "",
|
disabled: false,
|
||||||
},
|
loading: false,
|
||||||
color: {
|
large: false,
|
||||||
type: String,
|
shadow: false,
|
||||||
default: "",
|
reverse: false,
|
||||||
},
|
rounded: false,
|
||||||
disabled: {
|
gradient: false,
|
||||||
type: Boolean,
|
outline: false,
|
||||||
default: false,
|
shortcut: () => [],
|
||||||
},
|
|
||||||
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>
|
</script>
|
||||||
|
|||||||
@@ -66,71 +66,39 @@
|
|||||||
</SmartLink>
|
</SmartLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
interface Props {
|
||||||
|
to: string
|
||||||
export default defineComponent({
|
exact: boolean
|
||||||
props: {
|
blank: boolean
|
||||||
to: {
|
label: string
|
||||||
type: String,
|
icon: string
|
||||||
default: "",
|
svg: string
|
||||||
},
|
color: string
|
||||||
exact: {
|
disabled: boolean
|
||||||
type: Boolean,
|
loading: boolean
|
||||||
default: true,
|
reverse: boolean
|
||||||
},
|
rounded: boolean
|
||||||
blank: {
|
large: boolean
|
||||||
type: Boolean,
|
outline: boolean
|
||||||
default: false,
|
shortcut: string[]
|
||||||
},
|
filled: boolean
|
||||||
label: {
|
}
|
||||||
type: String,
|
withDefaults(defineProps<Props>(), {
|
||||||
default: "",
|
to: "",
|
||||||
},
|
exact: true,
|
||||||
icon: {
|
blank: false,
|
||||||
type: String,
|
label: "",
|
||||||
default: "",
|
icon: "",
|
||||||
},
|
svg: "",
|
||||||
svg: {
|
color: "",
|
||||||
type: String,
|
disabled: false,
|
||||||
default: "",
|
loading: false,
|
||||||
},
|
reverse: false,
|
||||||
color: {
|
rounded: false,
|
||||||
type: String,
|
large: false,
|
||||||
default: "",
|
outline: false,
|
||||||
},
|
shortcut: () => [],
|
||||||
disabled: {
|
filled: false,
|
||||||
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>
|
</script>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<span>
|
<span>
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
:label="$t('action.save')"
|
:label="$t('action.save')"
|
||||||
|
:loading="loadingState"
|
||||||
@click.native="addNewCollection"
|
@click.native="addNewCollection"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
@@ -43,6 +44,7 @@ import { defineComponent } from "@nuxtjs/composition-api"
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
|
loadingState: Boolean,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -56,7 +58,6 @@ export default defineComponent({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$emit("submit", this.name)
|
this.$emit("submit", this.name)
|
||||||
this.hideModal()
|
|
||||||
},
|
},
|
||||||
hideModal() {
|
hideModal() {
|
||||||
this.name = null
|
this.name = null
|
||||||
|
|||||||
@@ -24,7 +24,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span>
|
<span>
|
||||||
<ButtonPrimary :label="$t('action.save')" @click.native="addFolder" />
|
<ButtonPrimary
|
||||||
|
:label="$t('action.save')"
|
||||||
|
:loading="loadingState"
|
||||||
|
@click.native="addFolder"
|
||||||
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="$t('action.cancel')"
|
:label="$t('action.cancel')"
|
||||||
@click.native="hideModal"
|
@click.native="hideModal"
|
||||||
@@ -43,6 +47,7 @@ export default defineComponent({
|
|||||||
folder: { type: Object, default: () => {} },
|
folder: { type: Object, default: () => {} },
|
||||||
folderPath: { type: String, default: null },
|
folderPath: { type: String, default: null },
|
||||||
collectionIndex: { type: Number, default: null },
|
collectionIndex: { type: Number, default: null },
|
||||||
|
loadingState: Boolean,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -60,7 +65,6 @@ export default defineComponent({
|
|||||||
folder: this.folder,
|
folder: this.folder,
|
||||||
path: this.folderPath || `${this.collectionIndex}`,
|
path: this.folderPath || `${this.collectionIndex}`,
|
||||||
})
|
})
|
||||||
this.hideModal()
|
|
||||||
},
|
},
|
||||||
hideModal() {
|
hideModal() {
|
||||||
this.name = null
|
this.name = null
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<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>
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
<span>
|
<span>
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
:label="$t('action.save')"
|
:label="$t('action.save')"
|
||||||
|
:loading="loadingState"
|
||||||
@click.native="saveCollection"
|
@click.native="saveCollection"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
@@ -44,6 +45,7 @@ export default defineComponent({
|
|||||||
props: {
|
props: {
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
editingCollectionName: { type: String, default: null },
|
editingCollectionName: { type: String, default: null },
|
||||||
|
loadingState: Boolean,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -62,7 +64,6 @@ export default defineComponent({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$emit("submit", this.name)
|
this.$emit("submit", this.name)
|
||||||
this.hideModal()
|
|
||||||
},
|
},
|
||||||
hideModal() {
|
hideModal() {
|
||||||
this.name = null
|
this.name = null
|
||||||
|
|||||||
@@ -24,7 +24,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span>
|
<span>
|
||||||
<ButtonPrimary :label="$t('action.save')" @click.native="editFolder" />
|
<ButtonPrimary
|
||||||
|
:label="$t('action.save')"
|
||||||
|
:loading="loadingState"
|
||||||
|
@click.native="editFolder"
|
||||||
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="$t('action.cancel')"
|
:label="$t('action.cancel')"
|
||||||
@click.native="hideModal"
|
@click.native="hideModal"
|
||||||
@@ -41,6 +45,7 @@ export default defineComponent({
|
|||||||
props: {
|
props: {
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
editingFolderName: { type: String, default: null },
|
editingFolderName: { type: String, default: null },
|
||||||
|
loadingState: Boolean,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -59,7 +64,6 @@ export default defineComponent({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$emit("submit", this.name)
|
this.$emit("submit", this.name)
|
||||||
this.hideModal()
|
|
||||||
},
|
},
|
||||||
hideModal() {
|
hideModal() {
|
||||||
this.name = null
|
this.name = null
|
||||||
|
|||||||
@@ -24,7 +24,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span>
|
<span>
|
||||||
<ButtonPrimary :label="$t('action.save')" @click.native="saveRequest" />
|
<ButtonPrimary
|
||||||
|
:label="$t('action.save')"
|
||||||
|
:loading="loadingState"
|
||||||
|
@click.native="saveRequest"
|
||||||
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="$t('action.cancel')"
|
:label="$t('action.cancel')"
|
||||||
@click.native="hideModal"
|
@click.native="hideModal"
|
||||||
@@ -41,6 +45,7 @@ export default defineComponent({
|
|||||||
props: {
|
props: {
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
editingRequestName: { type: String, default: null },
|
editingRequestName: { type: String, default: null },
|
||||||
|
loadingState: Boolean,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -61,7 +66,6 @@ export default defineComponent({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$emit("submit", this.requestUpdateData)
|
this.$emit("submit", this.requestUpdateData)
|
||||||
this.hideModal()
|
|
||||||
},
|
},
|
||||||
hideModal() {
|
hideModal() {
|
||||||
this.requestUpdateData = { name: null }
|
this.requestUpdateData = { name: null }
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ const saveRequestAs = async () => {
|
|||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: picked.value.folderPath,
|
folderPath: picked.value.folderPath,
|
||||||
requestIndex: picked.value.requestIndex,
|
requestIndex: picked.value.requestIndex,
|
||||||
|
req: cloneDeep(requestUpdated),
|
||||||
})
|
})
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
@@ -249,6 +250,7 @@ const saveRequestAs = async () => {
|
|||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: picked.value.folderPath,
|
folderPath: picked.value.folderPath,
|
||||||
requestIndex: insertionIndex,
|
requestIndex: insertionIndex,
|
||||||
|
req: cloneDeep(requestUpdated),
|
||||||
})
|
})
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
@@ -265,6 +267,7 @@ const saveRequestAs = async () => {
|
|||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: `${picked.value.collectionIndex}`,
|
folderPath: `${picked.value.collectionIndex}`,
|
||||||
requestIndex: insertionIndex,
|
requestIndex: insertionIndex,
|
||||||
|
req: cloneDeep(requestUpdated),
|
||||||
})
|
})
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
@@ -293,6 +296,7 @@ const saveRequestAs = async () => {
|
|||||||
setRESTSaveContext({
|
setRESTSaveContext({
|
||||||
originLocation: "team-collection",
|
originLocation: "team-collection",
|
||||||
requestID: picked.value.requestID,
|
requestID: picked.value.requestID,
|
||||||
|
req: cloneDeep(requestUpdated),
|
||||||
})
|
})
|
||||||
} else if (picked.value.pickedType === "teams-folder") {
|
} else if (picked.value.pickedType === "teams-folder") {
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
@@ -319,6 +323,7 @@ const saveRequestAs = async () => {
|
|||||||
requestID: result.right.createRequestInCollection.id,
|
requestID: result.right.createRequestInCollection.id,
|
||||||
teamID: collectionsType.value.selectedTeam.id,
|
teamID: collectionsType.value.selectedTeam.id,
|
||||||
collectionID: picked.value.folderID,
|
collectionID: picked.value.folderID,
|
||||||
|
req: cloneDeep(requestUpdated),
|
||||||
})
|
})
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
@@ -348,6 +353,7 @@ const saveRequestAs = async () => {
|
|||||||
requestID: result.right.createRequestInCollection.id,
|
requestID: result.right.createRequestInCollection.id,
|
||||||
teamID: collectionsType.value.selectedTeam.id,
|
teamID: collectionsType.value.selectedTeam.id,
|
||||||
collectionID: picked.value.collectionID,
|
collectionID: picked.value.collectionID,
|
||||||
|
req: cloneDeep(requestUpdated),
|
||||||
})
|
})
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
|
|||||||
@@ -50,12 +50,16 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addFolder() {
|
addFolder() {
|
||||||
// TODO: Blocking when name is null ?
|
if (!this.name) {
|
||||||
|
this.$toast.error(`${this.$t("folder.name_length_insufficient")}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.$emit("add-folder", {
|
this.$emit("add-folder", {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
path: this.folderPath || `${this.collectionIndex}`,
|
path: this.folderPath || `${this.collectionIndex}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.hideModal()
|
this.hideModal()
|
||||||
},
|
},
|
||||||
hideModal() {
|
hideModal() {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<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>
|
||||||
@@ -29,6 +29,17 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex">
|
<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
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
svg="folder-plus"
|
svg="folder-plus"
|
||||||
@@ -61,11 +72,26 @@
|
|||||||
class="flex flex-col focus:outline-none"
|
class="flex flex-col focus:outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="menu"
|
role="menu"
|
||||||
|
@keyup.r="requestAction.$el.click()"
|
||||||
@keyup.n="folderAction.$el.click()"
|
@keyup.n="folderAction.$el.click()"
|
||||||
@keyup.e="edit.$el.click()"
|
@keyup.e="edit.$el.click()"
|
||||||
@keyup.delete="deleteAction.$el.click()"
|
@keyup.delete="deleteAction.$el.click()"
|
||||||
@keyup.escape="options.tippy().hide()"
|
@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
|
<SmartItem
|
||||||
ref="folderAction"
|
ref="folderAction"
|
||||||
svg="folder-plus"
|
svg="folder-plus"
|
||||||
@@ -126,6 +152,7 @@
|
|||||||
:collection-index="collectionIndex"
|
:collection-index="collectionIndex"
|
||||||
:doc="doc"
|
:doc="doc"
|
||||||
:is-filtered="isFiltered"
|
:is-filtered="isFiltered"
|
||||||
|
@add-request="$emit('add-request', $event)"
|
||||||
@add-folder="$emit('add-folder', $event)"
|
@add-folder="$emit('add-folder', $event)"
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
@edit-folder="$emit('edit-folder', $event)"
|
||||||
@edit-request="$emit('edit-request', $event)"
|
@edit-request="$emit('edit-request', $event)"
|
||||||
@@ -196,6 +223,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
tippyActions: ref<any | null>(null),
|
tippyActions: ref<any | null>(null),
|
||||||
options: ref<any | null>(null),
|
options: ref<any | null>(null),
|
||||||
|
requestAction: ref<any | null>(null),
|
||||||
folderAction: ref<any | null>(null),
|
folderAction: ref<any | null>(null),
|
||||||
edit: ref<any | null>(null),
|
edit: ref<any | null>(null),
|
||||||
deleteAction: ref<any | null>(null),
|
deleteAction: ref<any | null>(null),
|
||||||
|
|||||||
@@ -29,6 +29,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex">
|
<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
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
svg="folder-plus"
|
svg="folder-plus"
|
||||||
@@ -57,11 +64,24 @@
|
|||||||
class="flex flex-col focus:outline-none"
|
class="flex flex-col focus:outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="menu"
|
role="menu"
|
||||||
|
@keyup.r="requestAction.$el.click()"
|
||||||
@keyup.n="folderAction.$el.click()"
|
@keyup.n="folderAction.$el.click()"
|
||||||
@keyup.e="edit.$el.click()"
|
@keyup.e="edit.$el.click()"
|
||||||
@keyup.delete="deleteAction.$el.click()"
|
@keyup.delete="deleteAction.$el.click()"
|
||||||
@keyup.escape="options.tippy().hide()"
|
@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
|
<SmartItem
|
||||||
ref="folderAction"
|
ref="folderAction"
|
||||||
svg="folder-plus"
|
svg="folder-plus"
|
||||||
@@ -120,6 +140,7 @@
|
|||||||
:collection-index="collectionIndex"
|
:collection-index="collectionIndex"
|
||||||
:doc="doc"
|
:doc="doc"
|
||||||
:is-filtered="isFiltered"
|
:is-filtered="isFiltered"
|
||||||
|
@add-request="$emit('add-request', $event)"
|
||||||
@add-folder="$emit('add-folder', $event)"
|
@add-folder="$emit('add-folder', $event)"
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
@edit-folder="$emit('edit-folder', $event)"
|
||||||
@edit-request="$emit('edit-request', $event)"
|
@edit-request="$emit('edit-request', $event)"
|
||||||
@@ -193,6 +214,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
tippyActions: ref<any | null>(null),
|
tippyActions: ref<any | null>(null),
|
||||||
options: ref<any | null>(null),
|
options: ref<any | null>(null),
|
||||||
|
requestAction: ref<any | null>(null),
|
||||||
folderAction: ref<any | null>(null),
|
folderAction: ref<any | null>(null),
|
||||||
edit: ref<any | null>(null),
|
edit: ref<any | null>(null),
|
||||||
deleteAction: ref<any | null>(null),
|
deleteAction: ref<any | null>(null),
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
:is-filtered="filterText.length > 0"
|
:is-filtered="filterText.length > 0"
|
||||||
:saving-mode="savingMode"
|
:saving-mode="savingMode"
|
||||||
@edit-collection="editCollection(collection, index)"
|
@edit-collection="editCollection(collection, index)"
|
||||||
|
@add-request="addRequest($event)"
|
||||||
@add-folder="addFolder($event)"
|
@add-folder="addFolder($event)"
|
||||||
@edit-folder="editFolder($event)"
|
@edit-folder="editFolder($event)"
|
||||||
@edit-request="editRequest($event)"
|
@edit-request="editRequest($event)"
|
||||||
@@ -96,6 +97,12 @@
|
|||||||
:editing-collection-name="editingCollection ? editingCollection.name : ''"
|
:editing-collection-name="editingCollection ? editingCollection.name : ''"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
|
<CollectionsGraphqlAddRequest
|
||||||
|
:show="showModalAddRequest"
|
||||||
|
:folder-path="editingFolderPath"
|
||||||
|
@add-request="onAddRequest($event)"
|
||||||
|
@hide-modal="displayModalAddRequest(false)"
|
||||||
|
/>
|
||||||
<CollectionsGraphqlAddFolder
|
<CollectionsGraphqlAddFolder
|
||||||
:show="showModalAddFolder"
|
:show="showModalAddFolder"
|
||||||
:folder-path="editingFolderPath"
|
:folder-path="editingFolderPath"
|
||||||
@@ -136,6 +143,7 @@ import {
|
|||||||
addGraphqlFolder,
|
addGraphqlFolder,
|
||||||
saveGraphqlRequestAs,
|
saveGraphqlRequestAs,
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
|
import { getGQLSession, setGQLSession } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -156,6 +164,7 @@ export default defineComponent({
|
|||||||
showModalAdd: false,
|
showModalAdd: false,
|
||||||
showModalEdit: false,
|
showModalEdit: false,
|
||||||
showModalImportExport: false,
|
showModalImportExport: false,
|
||||||
|
showModalAddRequest: false,
|
||||||
showModalAddFolder: false,
|
showModalAddFolder: false,
|
||||||
showModalEditFolder: false,
|
showModalEditFolder: false,
|
||||||
showModalEditRequest: false,
|
showModalEditRequest: false,
|
||||||
@@ -222,6 +231,11 @@ export default defineComponent({
|
|||||||
displayModalImportExport(shouldDisplay) {
|
displayModalImportExport(shouldDisplay) {
|
||||||
this.showModalImportExport = shouldDisplay
|
this.showModalImportExport = shouldDisplay
|
||||||
},
|
},
|
||||||
|
displayModalAddRequest(shouldDisplay) {
|
||||||
|
this.showModalAddRequest = shouldDisplay
|
||||||
|
|
||||||
|
if (!shouldDisplay) this.resetSelectedData()
|
||||||
|
},
|
||||||
displayModalAddFolder(shouldDisplay) {
|
displayModalAddFolder(shouldDisplay) {
|
||||||
this.showModalAddFolder = shouldDisplay
|
this.showModalAddFolder = shouldDisplay
|
||||||
|
|
||||||
@@ -242,6 +256,26 @@ export default defineComponent({
|
|||||||
this.$data.editingCollectionIndex = collectionIndex
|
this.$data.editingCollectionIndex = collectionIndex
|
||||||
this.displayModalEdit(true)
|
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 }) {
|
onAddFolder({ name, path }) {
|
||||||
addGraphqlFolder(name, path)
|
addGraphqlFolder(name, path)
|
||||||
this.displayModalAddFolder(false)
|
this.displayModalAddFolder(false)
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
:picked="picked"
|
:picked="picked"
|
||||||
:loading-collection-i-ds="loadingCollectionIDs"
|
:loading-collection-i-ds="loadingCollectionIDs"
|
||||||
@edit-collection="editCollection(collection, index)"
|
@edit-collection="editCollection(collection, index)"
|
||||||
|
@add-request="addRequest($event)"
|
||||||
@add-folder="addFolder($event)"
|
@add-folder="addFolder($event)"
|
||||||
@edit-folder="editFolder($event)"
|
@edit-folder="editFolder($event)"
|
||||||
@edit-request="editRequest($event)"
|
@edit-request="editRequest($event)"
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
@expand-collection="expandCollection"
|
@expand-collection="expandCollection"
|
||||||
@remove-collection="removeCollection"
|
@remove-collection="removeCollection"
|
||||||
@remove-request="removeRequest"
|
@remove-request="removeRequest"
|
||||||
|
@remove-folder="removeFolder"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -146,6 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<CollectionsAdd
|
<CollectionsAdd
|
||||||
:show="showModalAdd"
|
:show="showModalAdd"
|
||||||
|
:loading-state="modalLoadingState"
|
||||||
@submit="addNewRootCollection"
|
@submit="addNewRootCollection"
|
||||||
@hide-modal="displayModalAdd(false)"
|
@hide-modal="displayModalAdd(false)"
|
||||||
/>
|
/>
|
||||||
@@ -156,13 +159,23 @@
|
|||||||
? editingCollection.name || editingCollection.title
|
? editingCollection.name || editingCollection.title
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
|
:loading-state="modalLoadingState"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
@submit="updateEditingCollection"
|
@submit="updateEditingCollection"
|
||||||
/>
|
/>
|
||||||
|
<CollectionsAddRequest
|
||||||
|
:show="showModalAddRequest"
|
||||||
|
:folder="editingFolder"
|
||||||
|
:folder-path="editingFolderPath"
|
||||||
|
:loading-state="modalLoadingState"
|
||||||
|
@add-request="onAddRequest($event)"
|
||||||
|
@hide-modal="displayModalAddRequest(false)"
|
||||||
|
/>
|
||||||
<CollectionsAddFolder
|
<CollectionsAddFolder
|
||||||
:show="showModalAddFolder"
|
:show="showModalAddFolder"
|
||||||
:folder="editingFolder"
|
:folder="editingFolder"
|
||||||
:folder-path="editingFolderPath"
|
:folder-path="editingFolderPath"
|
||||||
|
:loading-state="modalLoadingState"
|
||||||
@add-folder="onAddFolder($event)"
|
@add-folder="onAddFolder($event)"
|
||||||
@hide-modal="displayModalAddFolder(false)"
|
@hide-modal="displayModalAddFolder(false)"
|
||||||
/>
|
/>
|
||||||
@@ -171,12 +184,14 @@
|
|||||||
:editing-folder-name="
|
:editing-folder-name="
|
||||||
editingFolder ? editingFolder.name || editingFolder.title : ''
|
editingFolder ? editingFolder.name || editingFolder.title : ''
|
||||||
"
|
"
|
||||||
|
:loading-state="modalLoadingState"
|
||||||
@submit="updateEditingFolder"
|
@submit="updateEditingFolder"
|
||||||
@hide-modal="displayModalEditFolder(false)"
|
@hide-modal="displayModalEditFolder(false)"
|
||||||
/>
|
/>
|
||||||
<CollectionsEditRequest
|
<CollectionsEditRequest
|
||||||
:show="showModalEditRequest"
|
:show="showModalEditRequest"
|
||||||
:editing-request-name="editingRequest ? editingRequest.name : ''"
|
:editing-request-name="editingRequest ? editingRequest.name : ''"
|
||||||
|
:loading-state="modalLoadingState"
|
||||||
@submit="updateEditingRequest"
|
@submit="updateEditingRequest"
|
||||||
@hide-modal="displayModalEditRequest(false)"
|
@hide-modal="displayModalEditRequest(false)"
|
||||||
/>
|
/>
|
||||||
@@ -186,6 +201,13 @@
|
|||||||
@hide-modal="displayModalImportExport(false)"
|
@hide-modal="displayModalImportExport(false)"
|
||||||
@update-team-collections="updateTeamCollections"
|
@update-team-collections="updateTeamCollections"
|
||||||
/>
|
/>
|
||||||
|
<SmartConfirmModal
|
||||||
|
:show="showConfirmModal"
|
||||||
|
:title="confirmModalTitle"
|
||||||
|
:loading-state="modalLoadingState"
|
||||||
|
@hide-modal="showConfirmModal = false"
|
||||||
|
@resolve="resolveConfirmModal"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -204,11 +226,17 @@ import {
|
|||||||
editRESTCollection,
|
editRESTCollection,
|
||||||
addRESTFolder,
|
addRESTFolder,
|
||||||
removeRESTCollection,
|
removeRESTCollection,
|
||||||
|
removeRESTFolder,
|
||||||
editRESTFolder,
|
editRESTFolder,
|
||||||
removeRESTRequest,
|
removeRESTRequest,
|
||||||
editRESTRequest,
|
editRESTRequest,
|
||||||
saveRESTRequestAs,
|
saveRESTRequestAs,
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
|
import {
|
||||||
|
setRESTRequest,
|
||||||
|
getRESTRequest,
|
||||||
|
getRESTSaveContext,
|
||||||
|
} from "~/newstore/RESTSession"
|
||||||
import {
|
import {
|
||||||
useReadonlyStream,
|
useReadonlyStream,
|
||||||
useStreamSubscriber,
|
useStreamSubscriber,
|
||||||
@@ -250,17 +278,22 @@ export default defineComponent({
|
|||||||
showModalAdd: false,
|
showModalAdd: false,
|
||||||
showModalEdit: false,
|
showModalEdit: false,
|
||||||
showModalImportExport: false,
|
showModalImportExport: false,
|
||||||
|
showModalAddRequest: false,
|
||||||
showModalAddFolder: false,
|
showModalAddFolder: false,
|
||||||
showModalEditFolder: false,
|
showModalEditFolder: false,
|
||||||
showModalEditRequest: false,
|
showModalEditRequest: false,
|
||||||
|
showConfirmModal: false,
|
||||||
|
modalLoadingState: false,
|
||||||
editingCollection: undefined,
|
editingCollection: undefined,
|
||||||
editingCollectionIndex: undefined,
|
editingCollectionIndex: undefined,
|
||||||
|
editingCollectionID: undefined,
|
||||||
editingFolder: undefined,
|
editingFolder: undefined,
|
||||||
editingFolderName: undefined,
|
editingFolderName: undefined,
|
||||||
editingFolderIndex: undefined,
|
editingFolderIndex: undefined,
|
||||||
editingFolderPath: undefined,
|
editingFolderPath: undefined,
|
||||||
editingRequest: undefined,
|
editingRequest: undefined,
|
||||||
editingRequestIndex: undefined,
|
editingRequestIndex: undefined,
|
||||||
|
confirmModalTitle: undefined,
|
||||||
filterText: "",
|
filterText: "",
|
||||||
collectionsType: {
|
collectionsType: {
|
||||||
type: "my-collections",
|
type: "my-collections",
|
||||||
@@ -375,14 +408,18 @@ export default defineComponent({
|
|||||||
requests: [],
|
requests: [],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.displayModalAdd(false)
|
||||||
} else if (
|
} else if (
|
||||||
this.collectionsType.type === "team-collections" &&
|
this.collectionsType.type === "team-collections" &&
|
||||||
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||||
) {
|
) {
|
||||||
|
this.modalLoadingState = true
|
||||||
runMutation(CreateNewRootCollectionDocument, {
|
runMutation(CreateNewRootCollectionDocument, {
|
||||||
title: name,
|
title: name,
|
||||||
teamID: this.collectionsType.selectedTeam.id,
|
teamID: this.collectionsType.selectedTeam.id,
|
||||||
})().then((result) => {
|
})().then((result) => {
|
||||||
|
this.modalLoadingState = false
|
||||||
if (E.isLeft(result)) {
|
if (E.isLeft(result)) {
|
||||||
if (result.left.error === "team_coll/short_title")
|
if (result.left.error === "team_coll/short_title")
|
||||||
this.$toast.error(this.$t("collection.name_length_insufficient"))
|
this.$toast.error(this.$t("collection.name_length_insufficient"))
|
||||||
@@ -390,10 +427,10 @@ export default defineComponent({
|
|||||||
console.error(result.left.error)
|
console.error(result.left.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(this.$t("collection.created"))
|
this.$toast.success(this.$t("collection.created"))
|
||||||
|
this.displayModalAdd(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.displayModalAdd(false)
|
|
||||||
},
|
},
|
||||||
// Intented to be called by CollectionEdit modal submit event
|
// Intented to be called by CollectionEdit modal submit event
|
||||||
updateEditingCollection(newName) {
|
updateEditingCollection(newName) {
|
||||||
@@ -408,66 +445,107 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
editRESTCollection(this.editingCollectionIndex, collectionUpdated)
|
editRESTCollection(this.editingCollectionIndex, collectionUpdated)
|
||||||
|
this.displayModalEdit(false)
|
||||||
} else if (
|
} else if (
|
||||||
this.collectionsType.type === "team-collections" &&
|
this.collectionsType.type === "team-collections" &&
|
||||||
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||||
) {
|
) {
|
||||||
|
this.modalLoadingState = true
|
||||||
|
|
||||||
runMutation(RenameCollectionDocument, {
|
runMutation(RenameCollectionDocument, {
|
||||||
collectionID: this.editingCollection.id,
|
collectionID: this.editingCollection.id,
|
||||||
newTitle: newName,
|
newTitle: newName,
|
||||||
})().then((result) => {
|
})().then((result) => {
|
||||||
|
this.modalLoadingState = false
|
||||||
|
|
||||||
if (E.isLeft(result)) {
|
if (E.isLeft(result)) {
|
||||||
this.$toast.error(this.$t("error.something_went_wrong"))
|
this.$toast.error(this.$t("error.something_went_wrong"))
|
||||||
console.error(e)
|
console.error(result.left.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(this.$t("collection.renamed"))
|
this.$toast.success(this.$t("collection.renamed"))
|
||||||
|
this.displayModalEdit(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.displayModalEdit(false)
|
|
||||||
},
|
},
|
||||||
// Intended to be called by CollectionEditFolder modal submit event
|
// Intended to be called by CollectionEditFolder modal submit event
|
||||||
updateEditingFolder(name) {
|
updateEditingFolder(name) {
|
||||||
if (this.collectionsType.type === "my-collections") {
|
if (this.collectionsType.type === "my-collections") {
|
||||||
editRESTFolder(this.editingFolderPath, { ...this.editingFolder, name })
|
editRESTFolder(this.editingFolderPath, { ...this.editingFolder, name })
|
||||||
|
this.displayModalEditFolder(false)
|
||||||
} else if (
|
} else if (
|
||||||
this.collectionsType.type === "team-collections" &&
|
this.collectionsType.type === "team-collections" &&
|
||||||
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||||
) {
|
) {
|
||||||
|
this.modalLoadingState = true
|
||||||
|
|
||||||
runMutation(RenameCollectionDocument, {
|
runMutation(RenameCollectionDocument, {
|
||||||
collectionID: this.editingFolder.id,
|
collectionID: this.editingFolder.id,
|
||||||
newTitle: name,
|
newTitle: name,
|
||||||
})().then((result) => {
|
})().then((result) => {
|
||||||
|
this.modalLoadingState = false
|
||||||
|
|
||||||
if (E.isLeft(result)) {
|
if (E.isLeft(result)) {
|
||||||
this.$toast.error(this.$t("error.something_went_wrong"))
|
if (result.left.error === "team_coll/short_title")
|
||||||
console.error(e)
|
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 {
|
} else {
|
||||||
this.$toast.success(this.$t("folder.renamed"))
|
this.$toast.success(this.$t("folder.renamed"))
|
||||||
|
this.displayModalEditFolder(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.displayModalEditFolder(false)
|
|
||||||
},
|
},
|
||||||
// Intented to by called by CollectionsEditRequest modal submit event
|
// Intented to by called by CollectionsEditRequest modal submit event
|
||||||
updateEditingRequest(requestUpdateData) {
|
updateEditingRequest(requestUpdateData) {
|
||||||
|
const saveCtx = getRESTSaveContext()
|
||||||
|
|
||||||
const requestUpdated = {
|
const requestUpdated = {
|
||||||
...this.editingRequest,
|
...this.editingRequest,
|
||||||
name: requestUpdateData.name || this.editingRequest.name,
|
name: requestUpdateData.name || this.editingRequest.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.collectionsType.type === "my-collections") {
|
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(
|
editRESTRequest(
|
||||||
this.editingFolderPath,
|
this.editingFolderPath,
|
||||||
this.editingRequestIndex,
|
this.editingRequestIndex,
|
||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
this.displayModalEditRequest(false)
|
||||||
} else if (
|
} else if (
|
||||||
this.collectionsType.type === "team-collections" &&
|
this.collectionsType.type === "team-collections" &&
|
||||||
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||||
) {
|
) {
|
||||||
|
this.modalLoadingState = true
|
||||||
|
|
||||||
const requestName = requestUpdateData.name || this.editingRequest.name
|
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, {
|
runMutation(UpdateRequestDocument, {
|
||||||
data: {
|
data: {
|
||||||
request: JSON.stringify(requestUpdated),
|
request: JSON.stringify(requestUpdated),
|
||||||
@@ -475,17 +553,18 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
requestID: this.editingRequestIndex,
|
requestID: this.editingRequestIndex,
|
||||||
})().then((result) => {
|
})().then((result) => {
|
||||||
|
this.modalLoadingState = false
|
||||||
|
|
||||||
if (E.isLeft(result)) {
|
if (E.isLeft(result)) {
|
||||||
this.$toast.error(this.$t("error.something_went_wrong"))
|
this.$toast.error(this.$t("error.something_went_wrong"))
|
||||||
console.error(e)
|
console.error(result.left.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(this.$t("request.renamed"))
|
this.$toast.success(this.$t("request.renamed"))
|
||||||
this.$emit("update-team-collections")
|
this.$emit("update-team-collections")
|
||||||
|
this.displayModalEditRequest(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.displayModalEditRequest(false)
|
|
||||||
},
|
},
|
||||||
displayModalAdd(shouldDisplay) {
|
displayModalAdd(shouldDisplay) {
|
||||||
this.showModalAdd = shouldDisplay
|
this.showModalAdd = shouldDisplay
|
||||||
@@ -498,6 +577,11 @@ export default defineComponent({
|
|||||||
displayModalImportExport(shouldDisplay) {
|
displayModalImportExport(shouldDisplay) {
|
||||||
this.showModalImportExport = shouldDisplay
|
this.showModalImportExport = shouldDisplay
|
||||||
},
|
},
|
||||||
|
displayModalAddRequest(shouldDisplay) {
|
||||||
|
this.showModalAddRequest = shouldDisplay
|
||||||
|
|
||||||
|
if (!shouldDisplay) this.resetSelectedData()
|
||||||
|
},
|
||||||
displayModalAddFolder(shouldDisplay) {
|
displayModalAddFolder(shouldDisplay) {
|
||||||
this.showModalAddFolder = shouldDisplay
|
this.showModalAddFolder = shouldDisplay
|
||||||
|
|
||||||
@@ -513,6 +597,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
if (!shouldDisplay) this.resetSelectedData()
|
if (!shouldDisplay) this.resetSelectedData()
|
||||||
},
|
},
|
||||||
|
displayConfirmModal(shouldDisplay) {
|
||||||
|
this.showConfirmModal = shouldDisplay
|
||||||
|
|
||||||
|
if (!shouldDisplay) this.resetSelectedData()
|
||||||
|
},
|
||||||
editCollection(collection, collectionIndex) {
|
editCollection(collection, collectionIndex) {
|
||||||
this.$data.editingCollection = collection
|
this.$data.editingCollection = collection
|
||||||
this.$data.editingCollectionIndex = collectionIndex
|
this.$data.editingCollectionIndex = collectionIndex
|
||||||
@@ -521,12 +610,17 @@ export default defineComponent({
|
|||||||
onAddFolder({ name, folder, path }) {
|
onAddFolder({ name, folder, path }) {
|
||||||
if (this.collectionsType.type === "my-collections") {
|
if (this.collectionsType.type === "my-collections") {
|
||||||
addRESTFolder(name, path)
|
addRESTFolder(name, path)
|
||||||
} else if (this.collectionsType.type === "team-collections") {
|
this.displayModalAddFolder(false)
|
||||||
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
|
} else if (
|
||||||
|
this.collectionsType.type === "team-collections" &&
|
||||||
|
this.collectionsType.selectedTeam.myRole !== "VIEWER"
|
||||||
|
) {
|
||||||
|
this.modalLoadingState = true
|
||||||
runMutation(CreateChildCollectionDocument, {
|
runMutation(CreateChildCollectionDocument, {
|
||||||
childTitle: name,
|
childTitle: name,
|
||||||
collectionID: folder.id,
|
collectionID: folder.id,
|
||||||
})().then((result) => {
|
})().then((result) => {
|
||||||
|
this.modalLoadingState = false
|
||||||
if (E.isLeft(result)) {
|
if (E.isLeft(result)) {
|
||||||
if (result.left.error === "team_coll/short_title")
|
if (result.left.error === "team_coll/short_title")
|
||||||
this.$toast.error(this.$t("folder.name_length_insufficient"))
|
this.$toast.error(this.$t("folder.name_length_insufficient"))
|
||||||
@@ -534,13 +628,11 @@ export default defineComponent({
|
|||||||
console.error(result.left.error)
|
console.error(result.left.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(this.$t("folder.created"))
|
this.$toast.success(this.$t("folder.created"))
|
||||||
|
this.displayModalAddFolder(false)
|
||||||
this.$emit("update-team-collections")
|
this.$emit("update-team-collections")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.displayModalAddFolder(false)
|
|
||||||
},
|
},
|
||||||
addFolder(payload) {
|
addFolder(payload) {
|
||||||
const { folder, path } = payload
|
const { folder, path } = payload
|
||||||
@@ -578,16 +670,30 @@ export default defineComponent({
|
|||||||
resetSelectedData() {
|
resetSelectedData() {
|
||||||
this.$data.editingCollection = undefined
|
this.$data.editingCollection = undefined
|
||||||
this.$data.editingCollectionIndex = undefined
|
this.$data.editingCollectionIndex = undefined
|
||||||
|
this.$data.editingCollectionID = undefined
|
||||||
this.$data.editingFolder = undefined
|
this.$data.editingFolder = undefined
|
||||||
|
this.$data.editingFolderPath = undefined
|
||||||
this.$data.editingFolderIndex = undefined
|
this.$data.editingFolderIndex = undefined
|
||||||
this.$data.editingRequest = undefined
|
this.$data.editingRequest = undefined
|
||||||
this.$data.editingRequestIndex = undefined
|
this.$data.editingRequestIndex = undefined
|
||||||
|
|
||||||
|
this.$data.confirmModalTitle = undefined
|
||||||
},
|
},
|
||||||
expandCollection(collectionID) {
|
expandCollection(collectionID) {
|
||||||
this.teamCollectionAdapter.expandCollection(collectionID)
|
this.teamCollectionAdapter.expandCollection(collectionID)
|
||||||
},
|
},
|
||||||
removeCollection({ collectionsType, collectionIndex, collectionID }) {
|
removeCollection({ collectionIndex, collectionID }) {
|
||||||
if (collectionsType.type === "my-collections") {
|
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") {
|
||||||
// Cancel pick if picked collection is deleted
|
// Cancel pick if picked collection is deleted
|
||||||
if (
|
if (
|
||||||
this.picked &&
|
this.picked &&
|
||||||
@@ -598,8 +704,12 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeRESTCollection(collectionIndex)
|
removeRESTCollection(collectionIndex)
|
||||||
|
|
||||||
this.$toast.success(this.$t("state.deleted"))
|
this.$toast.success(this.$t("state.deleted"))
|
||||||
} else if (collectionsType.type === "team-collections") {
|
this.displayConfirmModal(false)
|
||||||
|
} else if (this.collectionsType.type === "team-collections") {
|
||||||
|
this.modalLoadingState = true
|
||||||
|
|
||||||
// Cancel pick if picked collection is deleted
|
// Cancel pick if picked collection is deleted
|
||||||
if (
|
if (
|
||||||
this.picked &&
|
this.picked &&
|
||||||
@@ -609,21 +719,89 @@ export default defineComponent({
|
|||||||
this.$emit("select", { picked: null })
|
this.$emit("select", { picked: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collectionsType.selectedTeam.myRole !== "VIEWER") {
|
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
|
||||||
runMutation(DeleteCollectionDocument, {
|
runMutation(DeleteCollectionDocument, {
|
||||||
collectionID,
|
collectionID,
|
||||||
})().then((result) => {
|
})().then((result) => {
|
||||||
|
this.modalLoadingState = false
|
||||||
if (E.isLeft(result)) {
|
if (E.isLeft(result)) {
|
||||||
this.$toast.error(this.$t("error.something_went_wrong"))
|
this.$toast.error(this.$t("error.something_went_wrong"))
|
||||||
console.error(e)
|
console.error(result.left.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(this.$t("state.deleted"))
|
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 }) {
|
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") {
|
if (this.collectionsType.type === "my-collections") {
|
||||||
// Cancel pick if the picked item is being deleted
|
// Cancel pick if the picked item is being deleted
|
||||||
if (
|
if (
|
||||||
@@ -635,8 +813,11 @@ export default defineComponent({
|
|||||||
this.$emit("select", { picked: null })
|
this.$emit("select", { picked: null })
|
||||||
}
|
}
|
||||||
removeRESTRequest(folderPath, requestIndex)
|
removeRESTRequest(folderPath, requestIndex)
|
||||||
|
|
||||||
this.$toast.success(this.$t("state.deleted"))
|
this.$toast.success(this.$t("state.deleted"))
|
||||||
|
this.displayConfirmModal(false)
|
||||||
} else if (this.collectionsType.type === "team-collections") {
|
} else if (this.collectionsType.type === "team-collections") {
|
||||||
|
this.modalLoadingState = true
|
||||||
// Cancel pick if the picked item is being deleted
|
// Cancel pick if the picked item is being deleted
|
||||||
if (
|
if (
|
||||||
this.picked &&
|
this.picked &&
|
||||||
@@ -649,11 +830,68 @@ export default defineComponent({
|
|||||||
runMutation(DeleteRequestDocument, {
|
runMutation(DeleteRequestDocument, {
|
||||||
requestID: requestIndex,
|
requestID: requestIndex,
|
||||||
})().then((result) => {
|
})().then((result) => {
|
||||||
|
this.modalLoadingState = false
|
||||||
if (E.isLeft(result)) {
|
if (E.isLeft(result)) {
|
||||||
this.$toast.error(this.$t("error.something_went_wrong"))
|
this.$toast.error(this.$t("error.something_went_wrong"))
|
||||||
console.error(e)
|
console.error(result.left.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(this.$t("state.deleted"))
|
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -681,6 +919,22 @@ 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>
|
</script>
|
||||||
|
|||||||
@@ -45,6 +45,18 @@
|
|||||||
color="green"
|
color="green"
|
||||||
@click.native="$emit('unselect-collection')"
|
@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
|
<ButtonSecondary
|
||||||
v-if="!doc"
|
v-if="!doc"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -79,12 +91,27 @@
|
|||||||
class="flex flex-col focus:outline-none"
|
class="flex flex-col focus:outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="menu"
|
role="menu"
|
||||||
|
@keyup.r="requestAction.$el.click()"
|
||||||
@keyup.n="folderAction.$el.click()"
|
@keyup.n="folderAction.$el.click()"
|
||||||
@keyup.e="edit.$el.click()"
|
@keyup.e="edit.$el.click()"
|
||||||
@keyup.delete="deleteAction.$el.click()"
|
@keyup.delete="deleteAction.$el.click()"
|
||||||
@keyup.x="exportAction.$el.click()"
|
@keyup.x="exportAction.$el.click()"
|
||||||
@keyup.escape="options.tippy().hide()"
|
@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
|
<SmartItem
|
||||||
ref="folderAction"
|
ref="folderAction"
|
||||||
svg="folder-plus"
|
svg="folder-plus"
|
||||||
@@ -131,7 +158,7 @@
|
|||||||
:shortcut="['⌫']"
|
:shortcut="['⌫']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
confirmRemove = true
|
removeCollection()
|
||||||
options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -159,12 +186,14 @@
|
|||||||
:collections-type="collectionsType"
|
:collections-type="collectionsType"
|
||||||
:is-filtered="isFiltered"
|
:is-filtered="isFiltered"
|
||||||
:picked="picked"
|
:picked="picked"
|
||||||
|
@add-request="$emit('add-request', $event)"
|
||||||
@add-folder="$emit('add-folder', $event)"
|
@add-folder="$emit('add-folder', $event)"
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
@edit-folder="$emit('edit-folder', $event)"
|
||||||
@edit-request="$emit('edit-request', $event)"
|
@edit-request="$emit('edit-request', $event)"
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
@duplicate-request="$emit('duplicate-request', $event)"
|
||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
@remove-request="$emit('remove-request', $event)"
|
@remove-request="$emit('remove-request', $event)"
|
||||||
|
@remove-folder="$emit('remove-folder', $event)"
|
||||||
/>
|
/>
|
||||||
<CollectionsMyRequest
|
<CollectionsMyRequest
|
||||||
v-for="(request, index) in collection.requests"
|
v-for="(request, index) in collection.requests"
|
||||||
@@ -205,12 +234,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartConfirmModal
|
|
||||||
:show="confirmRemove"
|
|
||||||
:title="$t('confirm.remove_collection')"
|
|
||||||
@hide-modal="confirmRemove = false"
|
|
||||||
@resolve="removeCollection"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -233,6 +256,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
tippyActions: ref<any | null>(null),
|
tippyActions: ref<any | null>(null),
|
||||||
options: ref<any | null>(null),
|
options: ref<any | null>(null),
|
||||||
|
requestAction: ref<any | null>(null),
|
||||||
folderAction: ref<any | null>(null),
|
folderAction: ref<any | null>(null),
|
||||||
edit: ref<any | null>(null),
|
edit: ref<any | null>(null),
|
||||||
deleteAction: ref<any | null>(null),
|
deleteAction: ref<any | null>(null),
|
||||||
@@ -244,7 +268,6 @@ export default defineComponent({
|
|||||||
showChildren: false,
|
showChildren: false,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
selectedFolder: {},
|
selectedFolder: {},
|
||||||
confirmRemove: false,
|
|
||||||
prevCursor: "",
|
prevCursor: "",
|
||||||
cursor: "",
|
cursor: "",
|
||||||
pageNo: 0,
|
pageNo: 0,
|
||||||
@@ -297,7 +320,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
removeCollection() {
|
removeCollection() {
|
||||||
this.$emit("remove-collection", {
|
this.$emit("remove-collection", {
|
||||||
collectionsType: this.collectionsType,
|
|
||||||
collectionIndex: this.collectionIndex,
|
collectionIndex: this.collectionIndex,
|
||||||
collectionID: this.collection.id,
|
collectionID: this.collection.id,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,6 +29,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex">
|
<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
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
svg="folder-plus"
|
svg="folder-plus"
|
||||||
@@ -57,12 +64,25 @@
|
|||||||
class="flex flex-col focus:outline-none"
|
class="flex flex-col focus:outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="menu"
|
role="menu"
|
||||||
|
@keyup.r="requestAction.$el.click()"
|
||||||
@keyup.n="folderAction.$el.click()"
|
@keyup.n="folderAction.$el.click()"
|
||||||
@keyup.e="edit.$el.click()"
|
@keyup.e="edit.$el.click()"
|
||||||
@keyup.delete="deleteAction.$el.click()"
|
@keyup.delete="deleteAction.$el.click()"
|
||||||
@keyup.x="exportAction.$el.click()"
|
@keyup.x="exportAction.$el.click()"
|
||||||
@keyup.escape="options.tippy().hide()"
|
@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
|
<SmartItem
|
||||||
ref="folderAction"
|
ref="folderAction"
|
||||||
svg="folder-plus"
|
svg="folder-plus"
|
||||||
@@ -111,7 +131,7 @@
|
|||||||
:shortcut="['⌫']"
|
:shortcut="['⌫']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
confirmRemove = true
|
removeFolder()
|
||||||
options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -138,13 +158,15 @@
|
|||||||
:collections-type="collectionsType"
|
:collections-type="collectionsType"
|
||||||
:folder-path="`${folderPath}/${subFolderIndex}`"
|
:folder-path="`${folderPath}/${subFolderIndex}`"
|
||||||
:picked="picked"
|
:picked="picked"
|
||||||
|
@add-request="$emit('add-request', $event)"
|
||||||
@add-folder="$emit('add-folder', $event)"
|
@add-folder="$emit('add-folder', $event)"
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
@edit-folder="$emit('edit-folder', $event)"
|
||||||
@edit-request="$emit('edit-request', $event)"
|
@edit-request="$emit('edit-request', $event)"
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
@duplicate-request="$emit('duplicate-request', $event)"
|
||||||
@update-team-collections="$emit('update-team-collections')"
|
@update-team-collections="$emit('update-team-collections')"
|
||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
@remove-request="removeRequest"
|
@remove-request="$emit('remove-request', $event)"
|
||||||
|
@remove-folder="$emit('remove-folder', $event)"
|
||||||
/>
|
/>
|
||||||
<CollectionsMyRequest
|
<CollectionsMyRequest
|
||||||
v-for="(request, index) in folder.requests"
|
v-for="(request, index) in folder.requests"
|
||||||
@@ -162,7 +184,7 @@
|
|||||||
@edit-request="$emit('edit-request', $event)"
|
@edit-request="$emit('edit-request', $event)"
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
@duplicate-request="$emit('duplicate-request', $event)"
|
||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
@remove-request="removeRequest"
|
@remove-request="$emit('remove-request', $event)"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
@@ -185,23 +207,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartConfirmModal
|
|
||||||
:show="confirmRemove"
|
|
||||||
:title="t('confirm.remove_folder')"
|
|
||||||
@hide-modal="confirmRemove = false"
|
|
||||||
@resolve="removeFolder"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from "@nuxtjs/composition-api"
|
import { defineComponent, ref } from "@nuxtjs/composition-api"
|
||||||
import { useI18n } from "~/helpers/utils/composables"
|
import { useI18n } from "~/helpers/utils/composables"
|
||||||
import {
|
import { moveRESTRequest } from "~/newstore/collections"
|
||||||
removeRESTFolder,
|
|
||||||
removeRESTRequest,
|
|
||||||
moveRESTRequest,
|
|
||||||
} from "~/newstore/collections"
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Folder",
|
name: "Folder",
|
||||||
@@ -222,6 +234,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
tippyActions: ref<any | null>(null),
|
tippyActions: ref<any | null>(null),
|
||||||
options: ref<any | null>(null),
|
options: ref<any | null>(null),
|
||||||
|
requestAction: ref<any | null>(null),
|
||||||
folderAction: ref<any | null>(null),
|
folderAction: ref<any | null>(null),
|
||||||
edit: ref<any | null>(null),
|
edit: ref<any | null>(null),
|
||||||
deleteAction: ref<any | null>(null),
|
deleteAction: ref<any | null>(null),
|
||||||
@@ -233,7 +246,6 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
showChildren: false,
|
showChildren: false,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
confirmRemove: false,
|
|
||||||
prevCursor: "",
|
prevCursor: "",
|
||||||
cursor: "",
|
cursor: "",
|
||||||
}
|
}
|
||||||
@@ -284,17 +296,10 @@ export default defineComponent({
|
|||||||
this.showChildren = !this.showChildren
|
this.showChildren = !this.showChildren
|
||||||
},
|
},
|
||||||
removeFolder() {
|
removeFolder() {
|
||||||
// TODO: Bubble it up ?
|
this.$emit("remove-folder", {
|
||||||
// Cancel pick if picked folder was deleted
|
folder: this.folder,
|
||||||
if (
|
folderPath: this.folderPath,
|
||||||
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 }) {
|
dropEvent({ dataTransfer }) {
|
||||||
this.dragging = !this.dragging
|
this.dragging = !this.dragging
|
||||||
@@ -302,19 +307,6 @@ export default defineComponent({
|
|||||||
const requestIndex = dataTransfer.getData("requestIndex")
|
const requestIndex = dataTransfer.getData("requestIndex")
|
||||||
moveRESTRequest(folderPath, requestIndex, this.folderPath)
|
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>
|
</script>
|
||||||
|
|||||||
@@ -126,7 +126,7 @@
|
|||||||
:shortcut="['⌫']"
|
:shortcut="['⌫']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
confirmRemove = true
|
removeRequest()
|
||||||
options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -136,12 +136,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartConfirmModal
|
|
||||||
:show="confirmRemove"
|
|
||||||
:title="t('confirm.remove_request')"
|
|
||||||
@hide-modal="confirmRemove = false"
|
|
||||||
@resolve="removeRequest"
|
|
||||||
/>
|
|
||||||
<HttpReqChangeConfirmModal
|
<HttpReqChangeConfirmModal
|
||||||
:show="confirmChange"
|
:show="confirmChange"
|
||||||
@hide-modal="confirmChange = false"
|
@hide-modal="confirmChange = false"
|
||||||
@@ -165,6 +159,7 @@ import {
|
|||||||
isEqualHoppRESTRequest,
|
isEqualHoppRESTRequest,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
import {
|
import {
|
||||||
useI18n,
|
useI18n,
|
||||||
useToast,
|
useToast,
|
||||||
@@ -221,8 +216,6 @@ const emit = defineEmits<{
|
|||||||
(
|
(
|
||||||
e: "remove-request",
|
e: "remove-request",
|
||||||
data: {
|
data: {
|
||||||
collectionIndex: number
|
|
||||||
folderName: string
|
|
||||||
folderPath: string
|
folderPath: string
|
||||||
requestIndex: number
|
requestIndex: number
|
||||||
}
|
}
|
||||||
@@ -264,7 +257,6 @@ const requestMethodLabels = {
|
|||||||
delete: "text-red-500",
|
delete: "text-red-500",
|
||||||
default: "text-gray-500",
|
default: "text-gray-500",
|
||||||
}
|
}
|
||||||
const confirmRemove = ref(false)
|
|
||||||
const confirmChange = ref(false)
|
const confirmChange = ref(false)
|
||||||
const showSaveRequestModal = ref(false)
|
const showSaveRequestModal = ref(false)
|
||||||
|
|
||||||
@@ -303,8 +295,6 @@ const dragStart = ({ dataTransfer }: DragEvent) => {
|
|||||||
|
|
||||||
const removeRequest = () => {
|
const removeRequest = () => {
|
||||||
emit("remove-request", {
|
emit("remove-request", {
|
||||||
collectionIndex: props.collectionIndex,
|
|
||||||
folderName: props.folderName,
|
|
||||||
folderPath: props.folderPath,
|
folderPath: props.folderPath,
|
||||||
requestIndex: props.requestIndex,
|
requestIndex: props.requestIndex,
|
||||||
})
|
})
|
||||||
@@ -317,15 +307,17 @@ const getRequestLabelColor = (method: string) =>
|
|||||||
|
|
||||||
const setRestReq = (request: any) => {
|
const setRestReq = (request: any) => {
|
||||||
setRESTRequest(
|
setRESTRequest(
|
||||||
|
cloneDeep(
|
||||||
safelyExtractRESTRequest(
|
safelyExtractRESTRequest(
|
||||||
translateToNewRequest(request),
|
translateToNewRequest(request),
|
||||||
getDefaultRESTRequest()
|
getDefaultRESTRequest()
|
||||||
|
)
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: props.folderPath,
|
folderPath: props.folderPath,
|
||||||
requestIndex: props.requestIndex,
|
requestIndex: props.requestIndex,
|
||||||
req: request,
|
req: cloneDeep(request),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -397,7 +389,7 @@ const discardRequestChange = () => {
|
|||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: props.folderPath,
|
folderPath: props.folderPath,
|
||||||
requestIndex: props.requestIndex,
|
requestIndex: props.requestIndex,
|
||||||
req: props.request,
|
req: cloneDeep(props.request),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,19 @@
|
|||||||
color="green"
|
color="green"
|
||||||
@click.native="$emit('unselect-collection')"
|
@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
|
<ButtonSecondary
|
||||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -80,12 +93,28 @@
|
|||||||
class="flex flex-col focus:outline-none"
|
class="flex flex-col focus:outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="menu"
|
role="menu"
|
||||||
|
@keyup.r="requestAction.$el.click()"
|
||||||
@keyup.n="folderAction.$el.click()"
|
@keyup.n="folderAction.$el.click()"
|
||||||
@keyup.e="edit.$el.click()"
|
@keyup.e="edit.$el.click()"
|
||||||
@keyup.delete="deleteAction.$el.click()"
|
@keyup.delete="deleteAction.$el.click()"
|
||||||
@keyup.x="exportAction.$el.click()"
|
@keyup.x="exportAction.$el.click()"
|
||||||
@keyup.escape="options.tippy().hide()"
|
@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
|
<SmartItem
|
||||||
ref="folderAction"
|
ref="folderAction"
|
||||||
svg="folder-plus"
|
svg="folder-plus"
|
||||||
@@ -128,7 +157,7 @@
|
|||||||
:shortcut="['⌫']"
|
:shortcut="['⌫']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
confirmRemove = true
|
removeCollection()
|
||||||
options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -157,12 +186,14 @@
|
|||||||
:is-filtered="isFiltered"
|
:is-filtered="isFiltered"
|
||||||
:picked="picked"
|
:picked="picked"
|
||||||
:loading-collection-i-ds="loadingCollectionIDs"
|
:loading-collection-i-ds="loadingCollectionIDs"
|
||||||
|
@add-request="$emit('add-request', $event)"
|
||||||
@add-folder="$emit('add-folder', $event)"
|
@add-folder="$emit('add-folder', $event)"
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
@edit-folder="$emit('edit-folder', $event)"
|
||||||
@edit-request="$emit('edit-request', $event)"
|
@edit-request="$emit('edit-request', $event)"
|
||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
@expand-collection="expandCollection"
|
@expand-collection="expandCollection"
|
||||||
@remove-request="removeRequest"
|
@remove-request="$emit('remove-request', $event)"
|
||||||
|
@remove-folder="$emit('remove-folder', $event)"
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
@duplicate-request="$emit('duplicate-request', $event)"
|
||||||
/>
|
/>
|
||||||
<CollectionsTeamsRequest
|
<CollectionsTeamsRequest
|
||||||
@@ -180,7 +211,7 @@
|
|||||||
:picked="picked"
|
:picked="picked"
|
||||||
@edit-request="editRequest($event)"
|
@edit-request="editRequest($event)"
|
||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
@remove-request="removeRequest"
|
@remove-request="$emit('remove-request', $event)"
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
@duplicate-request="$emit('duplicate-request', $event)"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -211,12 +242,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartConfirmModal
|
|
||||||
:show="confirmRemove"
|
|
||||||
:title="t('confirm.remove_collection')"
|
|
||||||
@hide-modal="confirmRemove = false"
|
|
||||||
@resolve="removeCollection"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -248,6 +273,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
tippyActions: ref<any | null>(null),
|
tippyActions: ref<any | null>(null),
|
||||||
options: ref<any | null>(null),
|
options: ref<any | null>(null),
|
||||||
|
requestAction: ref<any | null>(null),
|
||||||
folderAction: ref<any | null>(null),
|
folderAction: ref<any | null>(null),
|
||||||
edit: ref<any | null>(null),
|
edit: ref<any | null>(null),
|
||||||
deleteAction: ref<any | null>(null),
|
deleteAction: ref<any | null>(null),
|
||||||
@@ -261,7 +287,6 @@ export default defineComponent({
|
|||||||
showChildren: false,
|
showChildren: false,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
selectedFolder: {},
|
selectedFolder: {},
|
||||||
confirmRemove: false,
|
|
||||||
prevCursor: "",
|
prevCursor: "",
|
||||||
cursor: "",
|
cursor: "",
|
||||||
pageNo: 0,
|
pageNo: 0,
|
||||||
@@ -344,7 +369,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
removeCollection() {
|
removeCollection() {
|
||||||
this.$emit("remove-collection", {
|
this.$emit("remove-collection", {
|
||||||
collectionsType: this.collectionsType,
|
|
||||||
collectionIndex: this.collectionIndex,
|
collectionIndex: this.collectionIndex,
|
||||||
collectionID: this.collection.id,
|
collectionID: this.collection.id,
|
||||||
})
|
})
|
||||||
@@ -362,13 +386,6 @@ export default defineComponent({
|
|||||||
if (E.isLeft(moveRequestResult))
|
if (E.isLeft(moveRequestResult))
|
||||||
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
|
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
|
||||||
},
|
},
|
||||||
removeRequest({ collectionIndex, folderName, requestIndex }: any) {
|
|
||||||
this.$emit("remove-request", {
|
|
||||||
collectionIndex,
|
|
||||||
folderName,
|
|
||||||
requestIndex,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -29,6 +29,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex">
|
<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
|
<ButtonSecondary
|
||||||
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
v-if="collectionsType.selectedTeam.myRole !== 'VIEWER'"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -59,12 +67,25 @@
|
|||||||
class="flex flex-col focus:outline-none"
|
class="flex flex-col focus:outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="menu"
|
role="menu"
|
||||||
|
@keyup.r="requestAction.$el.click()"
|
||||||
@keyup.n="folderAction.$el.click()"
|
@keyup.n="folderAction.$el.click()"
|
||||||
@keyup.e="edit.$el.click()"
|
@keyup.e="edit.$el.click()"
|
||||||
@keyup.delete="deleteAction.$el.click()"
|
@keyup.delete="deleteAction.$el.click()"
|
||||||
@keyup.x="exportAction.$el.click()"
|
@keyup.x="exportAction.$el.click()"
|
||||||
@keyup.escape="options.tippy().hide()"
|
@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
|
<SmartItem
|
||||||
ref="folderAction"
|
ref="folderAction"
|
||||||
svg="folder-plus"
|
svg="folder-plus"
|
||||||
@@ -109,7 +130,7 @@
|
|||||||
:shortcut="['⌫']"
|
:shortcut="['⌫']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
confirmRemove = true
|
removeFolder()
|
||||||
options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -137,13 +158,15 @@
|
|||||||
:folder-path="`${folderPath}/${subFolderIndex}`"
|
:folder-path="`${folderPath}/${subFolderIndex}`"
|
||||||
:picked="picked"
|
:picked="picked"
|
||||||
:loading-collection-i-ds="loadingCollectionIDs"
|
:loading-collection-i-ds="loadingCollectionIDs"
|
||||||
|
@add-request="$emit('add-request', $event)"
|
||||||
@add-folder="$emit('add-folder', $event)"
|
@add-folder="$emit('add-folder', $event)"
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
@edit-folder="$emit('edit-folder', $event)"
|
||||||
@edit-request="$emit('edit-request', $event)"
|
@edit-request="$emit('edit-request', $event)"
|
||||||
@update-team-collections="$emit('update-team-collections')"
|
@update-team-collections="$emit('update-team-collections')"
|
||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
@expand-collection="expandCollection"
|
@expand-collection="expandCollection"
|
||||||
@remove-request="removeRequest"
|
@remove-request="$emit('remove-request', $event)"
|
||||||
|
@remove-folder="$emit('remove-folder', $event)"
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
@duplicate-request="$emit('duplicate-request', $event)"
|
||||||
/>
|
/>
|
||||||
<CollectionsTeamsRequest
|
<CollectionsTeamsRequest
|
||||||
@@ -161,7 +184,7 @@
|
|||||||
:collection-i-d="folder.id"
|
:collection-i-d="folder.id"
|
||||||
@edit-request="$emit('edit-request', $event)"
|
@edit-request="$emit('edit-request', $event)"
|
||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
@remove-request="removeRequest"
|
@remove-request="$emit('remove-request', $event)"
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
@duplicate-request="$emit('duplicate-request', $event)"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -190,20 +213,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartConfirmModal
|
|
||||||
:show="confirmRemove"
|
|
||||||
:title="$t('confirm.remove_folder')"
|
|
||||||
@hide-modal="confirmRemove = false"
|
|
||||||
@resolve="removeFolder"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from "@nuxtjs/composition-api"
|
import { defineComponent, ref } from "@nuxtjs/composition-api"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
|
||||||
import { DeleteCollectionDocument } from "~/helpers/backend/graphql"
|
|
||||||
import {
|
import {
|
||||||
getCompleteCollectionTree,
|
getCompleteCollectionTree,
|
||||||
teamCollToHoppRESTColl,
|
teamCollToHoppRESTColl,
|
||||||
@@ -228,6 +243,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
tippyActions: ref<any | null>(null),
|
tippyActions: ref<any | null>(null),
|
||||||
options: ref<any | null>(null),
|
options: ref<any | null>(null),
|
||||||
|
requestAction: ref<any | null>(null),
|
||||||
folderAction: ref<any | null>(null),
|
folderAction: ref<any | null>(null),
|
||||||
edit: ref<any | null>(null),
|
edit: ref<any | null>(null),
|
||||||
deleteAction: ref<any | null>(null),
|
deleteAction: ref<any | null>(null),
|
||||||
@@ -239,7 +255,6 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
showChildren: false,
|
showChildren: false,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
confirmRemove: false,
|
|
||||||
prevCursor: "",
|
prevCursor: "",
|
||||||
cursor: "",
|
cursor: "",
|
||||||
}
|
}
|
||||||
@@ -310,30 +325,10 @@ export default defineComponent({
|
|||||||
this.showChildren = !this.showChildren
|
this.showChildren = !this.showChildren
|
||||||
},
|
},
|
||||||
removeFolder() {
|
removeFolder() {
|
||||||
if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
|
this.$emit("remove-folder", {
|
||||||
// Cancel pick if picked collection folder was deleted
|
collectionsType: this.collectionsType,
|
||||||
if (
|
folder: this.folder,
|
||||||
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) {
|
expandCollection(collectionID: number) {
|
||||||
this.$emit("expand-collection", collectionID)
|
this.$emit("expand-collection", collectionID)
|
||||||
@@ -348,13 +343,6 @@ export default defineComponent({
|
|||||||
if (E.isLeft(moveRequestResult))
|
if (E.isLeft(moveRequestResult))
|
||||||
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
|
this.$toast.error(`${this.$t("error.something_went_wrong")}`)
|
||||||
},
|
},
|
||||||
removeRequest({ collectionIndex, folderName, requestIndex }: any) {
|
|
||||||
this.$emit("remove-request", {
|
|
||||||
collectionIndex,
|
|
||||||
folderName,
|
|
||||||
requestIndex,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
:shortcut="['⌫']"
|
:shortcut="['⌫']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
confirmRemove = true
|
removeRequest()
|
||||||
options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -133,12 +133,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartConfirmModal
|
|
||||||
:show="confirmRemove"
|
|
||||||
:title="$t('confirm.remove_request')"
|
|
||||||
@hide-modal="confirmRemove = false"
|
|
||||||
@resolve="removeRequest"
|
|
||||||
/>
|
|
||||||
<HttpReqChangeConfirmModal
|
<HttpReqChangeConfirmModal
|
||||||
:show="confirmChange"
|
:show="confirmChange"
|
||||||
@hide-modal="confirmChange = false"
|
@hide-modal="confirmChange = false"
|
||||||
@@ -215,8 +209,7 @@ const emit = defineEmits<{
|
|||||||
(
|
(
|
||||||
e: "remove-request",
|
e: "remove-request",
|
||||||
data: {
|
data: {
|
||||||
collectionIndex: number
|
folderPath: string | undefined
|
||||||
folderName: string | undefined
|
|
||||||
requestIndex: string
|
requestIndex: string
|
||||||
}
|
}
|
||||||
): void
|
): void
|
||||||
@@ -253,7 +246,6 @@ const requestMethodLabels = {
|
|||||||
delete: "text-red-500",
|
delete: "text-red-500",
|
||||||
default: "text-gray-500",
|
default: "text-gray-500",
|
||||||
}
|
}
|
||||||
const confirmRemove = ref(false)
|
|
||||||
const confirmChange = ref(false)
|
const confirmChange = ref(false)
|
||||||
const showSaveRequestModal = ref(false)
|
const showSaveRequestModal = ref(false)
|
||||||
|
|
||||||
@@ -289,8 +281,7 @@ const dragStart = ({ dataTransfer }: DragEvent) => {
|
|||||||
|
|
||||||
const removeRequest = () => {
|
const removeRequest = () => {
|
||||||
emit("remove-request", {
|
emit("remove-request", {
|
||||||
collectionIndex: props.collectionIndex,
|
folderPath: props.folderName,
|
||||||
folderName: props.folderName,
|
|
||||||
requestIndex: props.requestIndex,
|
requestIndex: props.requestIndex,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<SmartModal
|
<SmartModal
|
||||||
v-if="show"
|
v-if="show"
|
||||||
dialog
|
dialog
|
||||||
:title="$t(`environment.${action}`)"
|
:title="t(`environment.${action}`)"
|
||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
@@ -20,24 +20,24 @@
|
|||||||
@keyup.enter="saveEnvironment"
|
@keyup.enter="saveEnvironment"
|
||||||
/>
|
/>
|
||||||
<label for="selectLabelEnvEdit">
|
<label for="selectLabelEnvEdit">
|
||||||
{{ $t("action.label") }}
|
{{ t("action.label") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between flex-1">
|
<div class="flex items-center justify-between flex-1">
|
||||||
<label for="variableList" class="p-4">
|
<label for="variableList" class="p-4">
|
||||||
{{ $t("environment.variable_list") }}
|
{{ t("environment.variable_list") }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="$t('action.clear_all')"
|
:title="t('action.clear_all')"
|
||||||
:svg="clearIcon"
|
:svg="clearIcon"
|
||||||
@click.native="clearContent()"
|
@click.native="clearContent()"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
svg="plus"
|
svg="plus"
|
||||||
:title="$t('add.new')"
|
:title="t('add.new')"
|
||||||
@click.native="addEnvironmentVariable"
|
@click.native="addEnvironmentVariable"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
v-if="evnExpandError"
|
v-if="evnExpandError"
|
||||||
class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
|
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>
|
||||||
<div class="border rounded divide-y divide-dividerLight border-divider">
|
<div class="border rounded divide-y divide-dividerLight border-divider">
|
||||||
<div
|
<div
|
||||||
@@ -57,12 +57,12 @@
|
|||||||
<input
|
<input
|
||||||
v-model="variable.key"
|
v-model="variable.key"
|
||||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
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"
|
:name="'param' + index"
|
||||||
/>
|
/>
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="variable.value"
|
v-model="variable.value"
|
||||||
:placeholder="`${$t('count.value', { count: index + 1 })}`"
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
:envs="liveEnvs"
|
:envs="liveEnvs"
|
||||||
:name="'value' + index"
|
:name="'value' + index"
|
||||||
/>
|
/>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
id="variable"
|
id="variable"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="$t('action.remove')"
|
:title="t('action.remove')"
|
||||||
svg="trash"
|
svg="trash"
|
||||||
color="red"
|
color="red"
|
||||||
@click.native="removeEnvironmentVariable(index)"
|
@click.native="removeEnvironmentVariable(index)"
|
||||||
@@ -85,13 +85,13 @@
|
|||||||
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
|
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
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">
|
<span class="pb-4 text-center">
|
||||||
{{ $t("empty.environments") }}
|
{{ t("empty.environments") }}
|
||||||
</span>
|
</span>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="`${$t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click.native="addEnvironmentVariable"
|
@click.native="addEnvironmentVariable"
|
||||||
@@ -103,11 +103,11 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<span>
|
<span>
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
:label="`${$t('action.save')}`"
|
:label="`${t('action.save')}`"
|
||||||
@click.native="saveEnvironment"
|
@click.native="saveEnvironment"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="`${$t('action.cancel')}`"
|
:label="`${t('action.cancel')}`"
|
||||||
@click.native="hideModal"
|
@click.native="hideModal"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -115,9 +115,9 @@
|
|||||||
</SmartModal>
|
</SmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import clone from "lodash/clone"
|
import clone from "lodash/clone"
|
||||||
import { computed, defineComponent, PropType } from "@nuxtjs/composition-api"
|
import { computed, ref, watch } from "@nuxtjs/composition-api"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
|
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
|
||||||
import {
|
import {
|
||||||
@@ -130,25 +130,38 @@ import {
|
|||||||
setGlobalEnvVariables,
|
setGlobalEnvVariables,
|
||||||
updateEnvironment,
|
updateEnvironment,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
import {
|
||||||
|
useReadonlyStream,
|
||||||
|
useI18n,
|
||||||
|
useToast,
|
||||||
|
} from "~/helpers/utils/composables"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
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 name = ref<string | null>(null)
|
||||||
|
const vars = ref([{ key: "", value: "" }])
|
||||||
|
const clearIcon = ref("trash-2")
|
||||||
|
|
||||||
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 globalVars = useReadonlyStream(globalEnv$, [])
|
||||||
|
|
||||||
const workingEnv = computed(() => {
|
const workingEnv = computed(() => {
|
||||||
@@ -169,23 +182,11 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
const envList = useReadonlyStream(environments$, []) || props.envVars()
|
||||||
globalVars,
|
|
||||||
workingEnv,
|
const evnExpandError = computed(() => {
|
||||||
envList: useReadonlyStream(environments$, []) || props.envVars(),
|
for (const variable of vars.value) {
|
||||||
}
|
const result = parseTemplateStringE(variable.value.toString(), vars.value)
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
name: null as string | null,
|
|
||||||
vars: [] as { key: string; value: string }[],
|
|
||||||
clearIcon: "trash-2",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
evnExpandError(): boolean {
|
|
||||||
for (const variable of this.vars) {
|
|
||||||
const result = parseTemplateStringE(variable.value, this.vars)
|
|
||||||
|
|
||||||
if (E.isLeft(result)) {
|
if (E.isLeft(result)) {
|
||||||
console.error("error", result.left)
|
console.error("error", result.left)
|
||||||
@@ -193,73 +194,83 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
},
|
})
|
||||||
liveEnvs(): Array<{ key: string; value: string; source: string }> {
|
|
||||||
if (this.evnExpandError) {
|
const liveEnvs = computed(() => {
|
||||||
|
if (evnExpandError) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.$props.editingEnvironmentIndex === "Global") {
|
if (props.editingEnvironmentIndex === "Global") {
|
||||||
return [...this.vars.map((x) => ({ ...x, source: this.name! }))]
|
return [...vars.value.map((x) => ({ ...x, source: name.value! }))]
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
...this.vars.map((x) => ({ ...x, source: this.name! })),
|
...vars.value.map((x) => ({ ...x, source: name.value! })),
|
||||||
...this.globalVars.map((x) => ({ ...x, source: "Global" })),
|
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
},
|
|
||||||
watch: {
|
watch(
|
||||||
show() {
|
() => props.show,
|
||||||
this.name = this.workingEnv?.name ?? null
|
(show) => {
|
||||||
this.vars = clone(this.workingEnv?.variables ?? [])
|
if (show) {
|
||||||
},
|
name.value = workingEnv.value?.name ?? null
|
||||||
},
|
vars.value = clone(workingEnv.value?.variables ?? [])
|
||||||
methods: {
|
}
|
||||||
clearContent() {
|
}
|
||||||
this.vars = []
|
)
|
||||||
this.clearIcon = "check"
|
|
||||||
this.$toast.success(`${this.$t("state.cleared")}`)
|
const clearContent = () => {
|
||||||
setTimeout(() => (this.clearIcon = "trash-2"), 1000)
|
vars.value = []
|
||||||
},
|
clearIcon.value = "check"
|
||||||
addEnvironmentVariable() {
|
toast.success(`${t("state.cleared")}`)
|
||||||
this.vars.push({
|
setTimeout(() => (clearIcon.value = "trash-2"), 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addEnvironmentVariable = () => {
|
||||||
|
vars.value.push({
|
||||||
key: "",
|
key: "",
|
||||||
value: "",
|
value: "",
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
removeEnvironmentVariable(index: number) {
|
|
||||||
this.vars.splice(index, 1)
|
const removeEnvironmentVariable = (index: number) => {
|
||||||
},
|
vars.value.splice(index, 1)
|
||||||
saveEnvironment() {
|
}
|
||||||
if (!this.name) {
|
|
||||||
this.$toast.error(`${this.$t("environment.invalid_name")}`)
|
const saveEnvironment = () => {
|
||||||
|
if (!name.value) {
|
||||||
|
toast.error(`${t("environment.invalid_name")}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.action === "new") {
|
|
||||||
createEnvironment(this.name)
|
|
||||||
setCurrentEnvironment(this.envList.length - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const environmentUpdated: Environment = {
|
const environmentUpdated: Environment = {
|
||||||
name: this.name,
|
name: name.value,
|
||||||
variables: this.vars,
|
variables: vars.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.editingEnvironmentIndex === "Global")
|
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)
|
setGlobalEnvVariables(environmentUpdated.variables)
|
||||||
else if (this.action === "new") {
|
toast.success(`${t("environment.updated")}`)
|
||||||
updateEnvironment(this.envList.length - 1, environmentUpdated)
|
} else if (props.editingEnvironmentIndex !== null) {
|
||||||
} else {
|
// Editing an environment
|
||||||
updateEnvironment(this.editingEnvironmentIndex!, environmentUpdated)
|
updateEnvironment(props.editingEnvironmentIndex, environmentUpdated)
|
||||||
|
toast.success(`${t("environment.updated")}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
hideModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideModal = () => {
|
||||||
|
name.value = null
|
||||||
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
this.hideModal()
|
|
||||||
},
|
|
||||||
hideModal() {
|
|
||||||
this.name = null
|
|
||||||
this.$emit("hide-modal")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex items-center justify-center px-4 cursor-pointer"
|
class="flex items-center justify-center px-4 cursor-pointer"
|
||||||
@click="$emit('edit-environment')"
|
@click="emit('edit-environment')"
|
||||||
>
|
>
|
||||||
<SmartIcon class="svg-icons" name="layers" />
|
<SmartIcon class="svg-icons" name="layers" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
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">
|
<span class="truncate">
|
||||||
{{ environment.name }}
|
{{ environment.name }}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="$t('action.more')"
|
:title="t('action.more')"
|
||||||
svg="more-vertical"
|
svg="more-vertical"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -48,11 +48,11 @@
|
|||||||
<SmartItem
|
<SmartItem
|
||||||
ref="edit"
|
ref="edit"
|
||||||
svg="edit"
|
svg="edit"
|
||||||
:label="`${$t('action.edit')}`"
|
:label="`${t('action.edit')}`"
|
||||||
:shortcut="['E']"
|
:shortcut="['E']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
$emit('edit-environment')
|
emit('edit-environment')
|
||||||
options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -60,11 +60,11 @@
|
|||||||
<SmartItem
|
<SmartItem
|
||||||
ref="duplicate"
|
ref="duplicate"
|
||||||
svg="copy"
|
svg="copy"
|
||||||
:label="`${$t('action.duplicate')}`"
|
:label="`${t('action.duplicate')}`"
|
||||||
:shortcut="['D']"
|
:shortcut="['D']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
duplicateEnvironment()
|
duplicateEnvironments()
|
||||||
options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
v-if="!(environmentIndex === 'Global')"
|
v-if="!(environmentIndex === 'Global')"
|
||||||
ref="deleteAction"
|
ref="deleteAction"
|
||||||
svg="trash-2"
|
svg="trash-2"
|
||||||
:label="`${$t('action.delete')}`"
|
:label="`${t('action.delete')}`"
|
||||||
:shortcut="['⌫']"
|
:shortcut="['⌫']"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
@@ -87,15 +87,17 @@
|
|||||||
</span>
|
</span>
|
||||||
<SmartConfirmModal
|
<SmartConfirmModal
|
||||||
:show="confirmRemove"
|
:show="confirmRemove"
|
||||||
:title="`${$t('confirm.remove_environment')}`"
|
:title="`${t('confirm.remove_environment')}`"
|
||||||
@hide-modal="confirmRemove = false"
|
@hide-modal="confirmRemove = false"
|
||||||
@resolve="removeEnvironment"
|
@resolve="removeEnvironment"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, PropType, ref } from "@nuxtjs/composition-api"
|
import { ref } from "@nuxtjs/composition-api"
|
||||||
|
import { Environment } from "@hoppscotch/data"
|
||||||
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
import {
|
import {
|
||||||
deleteEnvironment,
|
deleteEnvironment,
|
||||||
duplicateEnvironment,
|
duplicateEnvironment,
|
||||||
@@ -104,47 +106,43 @@ import {
|
|||||||
getGlobalVariables,
|
getGlobalVariables,
|
||||||
environmentsStore,
|
environmentsStore,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
|
import { useI18n, useToast } from "~/helpers/utils/composables"
|
||||||
|
|
||||||
export default defineComponent({
|
const t = useI18n()
|
||||||
props: {
|
const toast = useToast()
|
||||||
environment: { type: Object, default: () => {} },
|
|
||||||
environmentIndex: {
|
const props = defineProps<{
|
||||||
type: [Number, String] as PropType<number | "Global">,
|
environment: Environment
|
||||||
default: null,
|
environmentIndex: number | "Global" | null
|
||||||
},
|
}>()
|
||||||
},
|
|
||||||
setup() {
|
const emit = defineEmits<{
|
||||||
return {
|
(e: "edit-environment"): void
|
||||||
tippyActions: ref<any | null>(null),
|
}>()
|
||||||
options: ref<any | null>(null),
|
|
||||||
edit: ref<any | null>(null),
|
const confirmRemove = ref(false)
|
||||||
duplicate: ref<any | null>(null),
|
|
||||||
deleteAction: ref<any | null>(null),
|
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")}`)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
data() {
|
const duplicateEnvironments = () => {
|
||||||
return {
|
if (props.environmentIndex === null) return
|
||||||
confirmRemove: false,
|
if (props.environmentIndex === "Global") {
|
||||||
}
|
createEnvironment(`Global - ${t("action.duplicate")}`)
|
||||||
},
|
|
||||||
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(
|
setEnvironmentVariables(
|
||||||
environmentsStore.value.environments.length - 1,
|
environmentsStore.value.environments.length - 1,
|
||||||
getGlobalVariables().reduce((gVars, gVar) => {
|
cloneDeep(getGlobalVariables())
|
||||||
gVars.push({ key: gVar.key, value: gVar.value })
|
|
||||||
return gVars
|
|
||||||
}, [])
|
|
||||||
)
|
)
|
||||||
} else duplicateEnvironment(this.environmentIndex)
|
} else duplicateEnvironment(props.environmentIndex)
|
||||||
},
|
}
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<span
|
<span
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="`${$t('environment.select')}`"
|
:title="`${t('environment.select')}`"
|
||||||
class="flex-1 bg-transparent border-b border-dividerLight select-wrapper"
|
class="flex-1 bg-transparent border-b border-dividerLight select-wrapper"
|
||||||
>
|
>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
@@ -15,20 +15,20 @@
|
|||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-else
|
v-else
|
||||||
:label="`${$t('environment.select')}`"
|
:label="`${t('environment.select')}`"
|
||||||
class="flex-1 !justify-start pr-8 rounded-none"
|
class="flex-1 !justify-start pr-8 rounded-none"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col" role="menu">
|
<div class="flex flex-col" role="menu">
|
||||||
<SmartItem
|
<SmartItem
|
||||||
:label="`${$t('environment.no_environment')}`"
|
:label="`${t('environment.no_environment')}`"
|
||||||
:info-icon="selectedEnvironmentIndex === -1 ? 'done' : ''"
|
:info-icon="selectedEnvironmentIndex === -1 ? 'done' : ''"
|
||||||
:active-info-icon="selectedEnvironmentIndex === -1"
|
:active-info-icon="selectedEnvironmentIndex === -1"
|
||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
selectedEnvironmentIndex = -1
|
selectedEnvironmentIndex = -1
|
||||||
$refs.options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
@click.native="
|
@click.native="
|
||||||
() => {
|
() => {
|
||||||
selectedEnvironmentIndex = index
|
selectedEnvironmentIndex = index
|
||||||
$refs.options.tippy().hide()
|
options.tippy().hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<div class="flex justify-between flex-1 border-b border-dividerLight">
|
<div class="flex justify-between flex-1 border-b border-dividerLight">
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
svg="plus"
|
svg="plus"
|
||||||
:label="`${$t('action.new')}`"
|
:label="`${t('action.new')}`"
|
||||||
class="!rounded-none"
|
class="!rounded-none"
|
||||||
@click.native="displayModalAdd(true)"
|
@click.native="displayModalAdd(true)"
|
||||||
/>
|
/>
|
||||||
@@ -60,13 +60,13 @@
|
|||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
to="https://docs.hoppscotch.io/features/environments"
|
to="https://docs.hoppscotch.io/features/environments"
|
||||||
blank
|
blank
|
||||||
:title="$t('app.wiki')"
|
:title="t('app.wiki')"
|
||||||
svg="help-circle"
|
svg="help-circle"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
svg="archive"
|
svg="archive"
|
||||||
:title="$t('modal.import_export')"
|
:title="t('modal.import_export')"
|
||||||
@click.native="displayModalImportExport(true)"
|
@click.native="displayModalImportExport(true)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,13 +95,13 @@
|
|||||||
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
|
:src="`/images/states/${$colorMode.value}/blockchain.svg`"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
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">
|
<span class="pb-4 text-center">
|
||||||
{{ $t("empty.environments") }}
|
{{ t("empty.environments") }}
|
||||||
</span>
|
</span>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="`${$t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click.native="displayModalAdd(true)"
|
@click.native="displayModalAdd(true)"
|
||||||
@@ -120,9 +120,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineComponent } from "@nuxtjs/composition-api"
|
import { computed, ref } from "@nuxtjs/composition-api"
|
||||||
import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
|
import {
|
||||||
|
useReadonlyStream,
|
||||||
|
useStream,
|
||||||
|
useI18n,
|
||||||
|
} from "~/helpers/utils/composables"
|
||||||
import {
|
import {
|
||||||
environments$,
|
environments$,
|
||||||
setCurrentEnvironment,
|
setCurrentEnvironment,
|
||||||
@@ -130,8 +134,10 @@ import {
|
|||||||
globalEnv$,
|
globalEnv$,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
|
|
||||||
export default defineComponent({
|
const t = useI18n()
|
||||||
setup() {
|
|
||||||
|
const options = ref<any | null>(null)
|
||||||
|
|
||||||
const globalEnv = useReadonlyStream(globalEnv$, [])
|
const globalEnv = useReadonlyStream(globalEnv$, [])
|
||||||
|
|
||||||
const globalEnvironment = computed(() => ({
|
const globalEnvironment = computed(() => ({
|
||||||
@@ -139,46 +145,38 @@ export default defineComponent({
|
|||||||
variables: globalEnv.value,
|
variables: globalEnv.value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return {
|
const environments = useReadonlyStream(environments$, [])
|
||||||
environments: useReadonlyStream(environments$, []),
|
|
||||||
globalEnvironment,
|
const selectedEnvironmentIndex = useStream(
|
||||||
selectedEnvironmentIndex: useStream(
|
|
||||||
selectedEnvIndex$,
|
selectedEnvIndex$,
|
||||||
-1,
|
-1,
|
||||||
setCurrentEnvironment
|
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
|
|
||||||
|
|
||||||
if (!shouldDisplay) this.resetSelectedData()
|
const showModalImportExport = ref(false)
|
||||||
},
|
const showModalDetails = ref(false)
|
||||||
displayModalImportExport(shouldDisplay: boolean) {
|
const action = ref<"new" | "edit">("edit")
|
||||||
this.showModalImportExport = shouldDisplay
|
const editingEnvironmentIndex = ref<number | "Global" | null>(null)
|
||||||
},
|
|
||||||
editEnvironment(environmentIndex: number | "Global") {
|
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||||
this.$data.editingEnvironmentIndex = environmentIndex
|
action.value = "new"
|
||||||
this.action = "edit"
|
showModalDetails.value = shouldDisplay
|
||||||
this.displayModalEdit(true)
|
}
|
||||||
},
|
const displayModalEdit = (shouldDisplay: boolean) => {
|
||||||
resetSelectedData() {
|
action.value = "edit"
|
||||||
this.$data.editingEnvironmentIndex = undefined
|
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
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -137,6 +137,47 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</draggable>
|
||||||
<div
|
<div
|
||||||
v-if="workingHeaders.length === 0"
|
v-if="workingHeaders.length === 0"
|
||||||
@@ -162,7 +203,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ref, ref, watch } from "@nuxtjs/composition-api"
|
import { computed, Ref, ref, watch } from "@nuxtjs/composition-api"
|
||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual"
|
||||||
import {
|
import {
|
||||||
HoppRESTHeader,
|
HoppRESTHeader,
|
||||||
@@ -177,13 +218,29 @@ import * as O from "fp-ts/Option"
|
|||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import cloneDeep from "lodash/cloneDeep"
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
import draggable from "vuedraggable"
|
import draggable from "vuedraggable"
|
||||||
|
import { RequestOptionTabs } from "./RequestOptions.vue"
|
||||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { restHeaders$, setRESTHeaders } from "~/newstore/RESTSession"
|
import {
|
||||||
|
getRESTRequest,
|
||||||
|
restHeaders$,
|
||||||
|
restRequest$,
|
||||||
|
setRESTHeaders,
|
||||||
|
} from "~/newstore/RESTSession"
|
||||||
import { commonHeaders } from "~/helpers/headers"
|
import { commonHeaders } from "~/helpers/headers"
|
||||||
import { useI18n, useStream, useToast } from "~/helpers/utils/composables"
|
import {
|
||||||
|
useI18n,
|
||||||
|
useReadonlyStream,
|
||||||
|
useStream,
|
||||||
|
useToast,
|
||||||
|
} from "~/helpers/utils/composables"
|
||||||
import linter from "~/helpers/editor/linting/rawKeyValue"
|
import linter from "~/helpers/editor/linting/rawKeyValue"
|
||||||
import { throwError } from "~/helpers/functional/error"
|
import { throwError } from "~/helpers/functional/error"
|
||||||
import { objRemoveKey } from "~/helpers/functional/object"
|
import { objRemoveKey } from "~/helpers/functional/object"
|
||||||
|
import {
|
||||||
|
ComputedHeader,
|
||||||
|
getComputedHeaders,
|
||||||
|
} from "~/helpers/utils/EffectiveURL"
|
||||||
|
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -196,6 +253,10 @@ const bulkEditor = ref<any | null>(null)
|
|||||||
|
|
||||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "change-tab", value: RequestOptionTabs): void
|
||||||
|
}>()
|
||||||
|
|
||||||
useCodemirror(bulkEditor, bulkHeaders, {
|
useCodemirror(bulkEditor, bulkHeaders, {
|
||||||
extendedEditorConfig: {
|
extendedEditorConfig: {
|
||||||
mode: "text/x-yaml",
|
mode: "text/x-yaml",
|
||||||
@@ -379,4 +440,28 @@ const clearContent = () => {
|
|||||||
|
|
||||||
bulkHeaders.value = ""
|
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>
|
</script>
|
||||||
|
|||||||
@@ -171,6 +171,12 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<SmartItem
|
||||||
|
svg="link-2"
|
||||||
|
:label="`${t('request.view_my_links')}`"
|
||||||
|
to="/profile"
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
<SmartItem
|
<SmartItem
|
||||||
ref="saveRequestAction"
|
ref="saveRequestAction"
|
||||||
:label="`${t('request.save_as')}`"
|
:label="`${t('request.save_as')}`"
|
||||||
@@ -208,6 +214,7 @@
|
|||||||
import { computed, ref, watch } from "@nuxtjs/composition-api"
|
import { computed, ref, watch } from "@nuxtjs/composition-api"
|
||||||
import { isLeft, isRight } from "fp-ts/lib/Either"
|
import { isLeft, isRight } from "fp-ts/lib/Either"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
import {
|
import {
|
||||||
updateRESTResponse,
|
updateRESTResponse,
|
||||||
restEndpoint$,
|
restEndpoint$,
|
||||||
@@ -477,14 +484,21 @@ const saveRequest = () => {
|
|||||||
showSaveRequestModal.value = true
|
showSaveRequestModal.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveCtx.originLocation === "user-collection") {
|
if (saveCtx.originLocation === "user-collection") {
|
||||||
|
const req = getRESTRequest()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
editRESTRequest(
|
editRESTRequest(
|
||||||
saveCtx.folderPath,
|
saveCtx.folderPath,
|
||||||
saveCtx.requestIndex,
|
saveCtx.requestIndex,
|
||||||
getRESTRequest()
|
getRESTRequest()
|
||||||
)
|
)
|
||||||
|
setRESTSaveContext({
|
||||||
|
originLocation: "user-collection",
|
||||||
|
folderPath: saveCtx.folderPath,
|
||||||
|
requestIndex: saveCtx.requestIndex,
|
||||||
|
req: cloneDeep(req),
|
||||||
|
})
|
||||||
toast.success(`${t("request.saved")}`)
|
toast.success(`${t("request.saved")}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setRESTSaveContext(null)
|
setRESTSaveContext(null)
|
||||||
@@ -505,6 +519,11 @@ const saveRequest = () => {
|
|||||||
if (E.isLeft(result)) {
|
if (E.isLeft(result)) {
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
toast.error(`${t("profile.no_permission")}`)
|
||||||
} else {
|
} else {
|
||||||
|
setRESTSaveContext({
|
||||||
|
originLocation: "team-collection",
|
||||||
|
requestID: saveCtx.requestID,
|
||||||
|
req: cloneDeep(req),
|
||||||
|
})
|
||||||
toast.success(`${t("request.saved")}`)
|
toast.success(`${t("request.saved")}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
:label="`${$t('tab.headers')}`"
|
:label="`${$t('tab.headers')}`"
|
||||||
:info="`${newActiveHeadersCount$}`"
|
:info="`${newActiveHeadersCount$}`"
|
||||||
>
|
>
|
||||||
<HttpHeaders />
|
<HttpHeaders @change-tab="changeTab" />
|
||||||
</SmartTab>
|
</SmartTab>
|
||||||
<SmartTab :id="'authorization'" :label="`${$t('tab.authorization')}`">
|
<SmartTab :id="'authorization'" :label="`${$t('tab.authorization')}`">
|
||||||
<HttpAuthorization />
|
<HttpAuthorization />
|
||||||
|
|||||||
@@ -17,26 +17,11 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<LensesHeadersRendererEntry
|
||||||
v-for="(header, index) in headers"
|
v-for="(header, index) in headers"
|
||||||
:key="`header-${index}`"
|
:key="index"
|
||||||
class="flex border-b divide-x divide-dividerLight border-dividerLight group"
|
:header="header"
|
||||||
>
|
/>
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<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 { ref } from "@nuxtjs/composition-api"
|
||||||
|
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 = ref("copy")
|
||||||
|
|
||||||
|
const copyHeader = (headerValue: string) => {
|
||||||
|
copyToClipboard(headerValue)
|
||||||
|
copyIcon.value = "check"
|
||||||
|
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||||
|
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
135
packages/hoppscotch-app/components/profile/Shortcode.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<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 { 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 = ref<"copy" | "check">("copy")
|
||||||
|
|
||||||
|
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"
|
||||||
|
setTimeout(() => (copyIconRefs.value = "copy"), 1000)
|
||||||
|
}
|
||||||
|
</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>
|
||||||
221
packages/hoppscotch-app/components/realtime/Communication.vue
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<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-upperMobileSecondaryStickyFold sm: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 { 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 = ref<"wand" | "check" | "info">("wand")
|
||||||
|
|
||||||
|
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")}`)
|
||||||
|
}
|
||||||
|
setTimeout(() => (prettifyIcon.value = "wand"), 1000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,77 +1,164 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1">
|
<div ref="container" class="flex flex-col flex-1 overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight"
|
class="sticky top-0 z-10 flex items-center justify-between flex-none pl-4 border-b bg-primary border-dividerLight"
|
||||||
>
|
>
|
||||||
<label for="log" class="py-2 font-semibold text-secondaryLight">
|
<label for="log" class="py-2 font-semibold text-secondaryLight">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</label>
|
</label>
|
||||||
|
<div>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.search')"
|
||||||
|
svg="search"
|
||||||
|
@click.native="toggleSearch = !toggleSearch"
|
||||||
|
/>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.delete')"
|
||||||
|
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 ref="logsRef" name="log" class="realtime-log">
|
</div>
|
||||||
<span v-if="log" class="space-y-2">
|
|
||||||
<span
|
<div
|
||||||
v-for="(entry, index) in log"
|
v-if="toggleSearch"
|
||||||
:key="`entry-${index}`"
|
class="w-full p-2 sticky top-0 z-10 text-center border-b border-dividerLight"
|
||||||
:style="{ color: entry.color }"
|
|
||||||
class="font-mono"
|
|
||||||
>{{ entry.ts }}{{ source(entry.source) }}{{ entry.payload }}</span
|
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
class="bg-primaryLight border-divider text-secondaryDark rounded inline-flex"
|
||||||
|
>
|
||||||
|
<ButtonSecondary svg="search" class="item-center" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
id=""
|
||||||
|
v-model="pattern"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter search pattern"
|
||||||
|
class="rounded w-64 bg-primaryLight text-secondaryDark text-center"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>{{ t("response.waiting_for_connection") }}</span>
|
</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
|
||||||
|
v-for="(entry, index) in logEntries"
|
||||||
|
:key="`entry-${index}`"
|
||||||
|
:entry="entry"
|
||||||
|
:highlight-regex="pattern === '' ? undefined : patternRegex"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, ref, watch } from "@nuxtjs/composition-api"
|
import { ref, computed, watch } from "@nuxtjs/composition-api"
|
||||||
import { getSourcePrefix as source } from "~/helpers/utils/string"
|
import { useThrottleFn, useScroll } from "@vueuse/core"
|
||||||
|
import { regexEscape } from "~/helpers/functional/regex"
|
||||||
import { useI18n } from "~/helpers/utils/composables"
|
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 props = defineProps<{ log: LogEntryData[]; title: string }>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "delete"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const props = defineProps({
|
const container = ref<HTMLElement | null>(null)
|
||||||
log: { type: Array, default: () => [] },
|
const logs = ref<HTMLElement | null>(null)
|
||||||
title: {
|
|
||||||
type: String,
|
const autoScrollEnabled = ref(true)
|
||||||
default: "",
|
|
||||||
},
|
const logListScroll = useScroll(logs)
|
||||||
|
|
||||||
|
// Disable autoscroll when scrolling to top
|
||||||
|
watch(logListScroll.isScrolling, (isScrolling) => {
|
||||||
|
if (isScrolling && logListScroll.directions.top)
|
||||||
|
autoScrollEnabled.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
const logsRef = ref<any | null>(null)
|
const scrollTo = (position: "top" | "bottom") => {
|
||||||
const BOTTOM_SCROLL_DIST_INACCURACY = 5
|
if (position === "top") {
|
||||||
|
logs.value?.scroll({
|
||||||
|
behavior: "smooth",
|
||||||
|
top: 0,
|
||||||
|
})
|
||||||
|
} else if (position === "bottom") {
|
||||||
|
logs.value?.scroll({
|
||||||
|
behavior: "smooth",
|
||||||
|
top: logs.value?.scrollHeight,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.log,
|
() => props.log,
|
||||||
() => {
|
useThrottleFn(() => {
|
||||||
if (!logsRef.value) return
|
if (autoScrollEnabled.value) scrollTo("bottom")
|
||||||
const distToBottom =
|
}, 200),
|
||||||
logsRef.value.scrollHeight -
|
{ flush: "post" }
|
||||||
logsRef.value.scrollTop -
|
)
|
||||||
logsRef.value.clientHeight
|
|
||||||
if (distToBottom < BOTTOM_SCROLL_DIST_INACCURACY) {
|
const toggleAutoscroll = () => {
|
||||||
nextTick(() => (logsRef.value.scrollTop = logsRef.value.scrollHeight))
|
autoScrollEnabled.value = !autoScrollEnabled.value
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pattern = ref("")
|
||||||
|
const toggleSearch = ref(false)
|
||||||
|
|
||||||
|
const patternRegex = computed(
|
||||||
|
() => new RegExp(regexEscape(pattern.value), "gi")
|
||||||
|
)
|
||||||
|
|
||||||
|
const logEntries = computed(() => {
|
||||||
|
if (patternRegex.value) {
|
||||||
|
return props.log.filter((entry) => entry.payload.match(patternRegex.value))
|
||||||
|
} else return props.log
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleAutoscrollColor = computed(() =>
|
||||||
|
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style></style>
|
||||||
.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>
|
|
||||||
|
|||||||
442
packages/hoppscotch-app/components/realtime/LogEntry.vue
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
<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>
|
||||||
|
<span
|
||||||
|
v-for="(section, index) in highlightingSections"
|
||||||
|
:key="index"
|
||||||
|
class="!inline"
|
||||||
|
:class="section.mode === 'highlight' ? 'highlight' : ''"
|
||||||
|
>{{ section.text }}</span
|
||||||
|
>
|
||||||
|
</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">
|
||||||
|
<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 { useTimeAgo } from "@vueuse/core"
|
||||||
|
import { LogEntryData } from "./Log.vue"
|
||||||
|
import { useI18n } from "~/helpers/utils/composables"
|
||||||
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import { isJSON } from "~/helpers/functional/json"
|
||||||
|
import { regexFindAllMatches } from "~/helpers/functional/regex"
|
||||||
|
import useCopyResponse from "~/helpers/lenses/composables/useCopyResponse"
|
||||||
|
import useDownloadResponse from "~/helpers/lenses/composables/useDownloadResponse"
|
||||||
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
|
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
|
||||||
|
highlightRegex?: RegExp
|
||||||
|
}>()
|
||||||
|
const outlineOptions = ref<any | null>(null)
|
||||||
|
const editor = ref<any | null>(null)
|
||||||
|
const linewrapEnabled = ref(true)
|
||||||
|
const logPayload = computed(() => props.entry.payload)
|
||||||
|
|
||||||
|
type HighlightSection = {
|
||||||
|
mode: "normal" | "highlight"
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightingSections = computed<HighlightSection[]>(() => {
|
||||||
|
if (!props.highlightRegex)
|
||||||
|
return [{ mode: "normal", text: props.entry.payload }]
|
||||||
|
|
||||||
|
const line = props.entry.payload.split("\n")[0]
|
||||||
|
|
||||||
|
const ranges = pipe(line, regexFindAllMatches(props.highlightRegex))
|
||||||
|
|
||||||
|
const result: HighlightSection[] = []
|
||||||
|
let point = 0
|
||||||
|
|
||||||
|
ranges.forEach(({ startIndex, endIndex }) => {
|
||||||
|
if (point < startIndex)
|
||||||
|
result.push({
|
||||||
|
mode: "normal",
|
||||||
|
text: line.slice(point, startIndex),
|
||||||
|
})
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
mode: "highlight",
|
||||||
|
text: line.slice(startIndex, endIndex + 1),
|
||||||
|
})
|
||||||
|
|
||||||
|
point = endIndex + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (point < line.length)
|
||||||
|
result.push({
|
||||||
|
mode: "normal",
|
||||||
|
text: line.slice(point, line.length),
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedTab = ref<"json" | "raw">(
|
||||||
|
isJSON(props.entry.payload) ? "json" : "raw"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 = ref("copy")
|
||||||
|
const copyQuery = (entry: string) => {
|
||||||
|
copyToClipboard(entry)
|
||||||
|
copyQueryIcon.value = "check"
|
||||||
|
setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,17 +13,22 @@
|
|||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
|
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
|
||||||
:placeholder="$t('mqtt.url')"
|
:placeholder="$t('mqtt.url')"
|
||||||
:disabled="connectionState"
|
:disabled="
|
||||||
@keyup.enter="validUrl ? toggleConnection() : null"
|
connectionState === 'CONNECTED' ||
|
||||||
|
connectionState === 'CONNECTING'
|
||||||
|
"
|
||||||
|
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||||
/>
|
/>
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
id="connect"
|
id="connect"
|
||||||
:disabled="!validUrl"
|
:disabled="!isUrlValid"
|
||||||
class="w-32"
|
class="w-32"
|
||||||
:label="
|
:label="
|
||||||
connectionState ? $t('action.disconnect') : $t('action.connect')
|
connectionState === 'DISCONNECTED'
|
||||||
|
? t('action.connect')
|
||||||
|
: t('action.disconnect')
|
||||||
"
|
"
|
||||||
:loading="connectingState"
|
:loading="connectionState === 'CONNECTING'"
|
||||||
@click.native="toggleConnection"
|
@click.native="toggleConnection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,18 +53,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #secondary>
|
<template #secondary>
|
||||||
<RealtimeLog :title="$t('mqtt.log')" :log="log" />
|
<RealtimeLog
|
||||||
|
:title="$t('mqtt.log')"
|
||||||
|
:log="log"
|
||||||
|
@delete="clearLogEntries()"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #sidebar>
|
<template #sidebar>
|
||||||
<div class="flex items-center justify-between p-4">
|
<div class="flex items-center justify-between p-4">
|
||||||
<label for="pub_topic" class="font-semibold text-secondaryLight">
|
<label for="pubTopic" class="font-semibold text-secondaryLight">
|
||||||
{{ $t("mqtt.topic") }}
|
{{ $t("mqtt.topic") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex px-4">
|
<div class="flex px-4">
|
||||||
<input
|
<input
|
||||||
id="pub_topic"
|
id="pubTopic"
|
||||||
v-model="pub_topic"
|
v-model="pubTopic"
|
||||||
class="input"
|
class="input"
|
||||||
:placeholder="$t('mqtt.topic_name')"
|
:placeholder="$t('mqtt.topic_name')"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -75,7 +84,7 @@
|
|||||||
<div class="flex px-4 space-x-2">
|
<div class="flex px-4 space-x-2">
|
||||||
<input
|
<input
|
||||||
id="mqtt-message"
|
id="mqtt-message"
|
||||||
v-model="msg"
|
v-model="message"
|
||||||
class="input"
|
class="input"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -85,7 +94,7 @@
|
|||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
id="publish"
|
id="publish"
|
||||||
name="get"
|
name="get"
|
||||||
:disabled="!canpublish"
|
:disabled="!canPublish"
|
||||||
:label="$t('mqtt.publish')"
|
:label="$t('mqtt.publish')"
|
||||||
@click.native="publish"
|
@click.native="publish"
|
||||||
/>
|
/>
|
||||||
@@ -93,14 +102,14 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-4 mt-4 border-t border-dividerLight"
|
class="flex items-center justify-between p-4 mt-4 border-t border-dividerLight"
|
||||||
>
|
>
|
||||||
<label for="sub_topic" class="font-semibold text-secondaryLight">
|
<label for="subTopic" class="font-semibold text-secondaryLight">
|
||||||
{{ $t("mqtt.topic") }}
|
{{ $t("mqtt.topic") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex px-4 space-x-2">
|
<div class="flex px-4 space-x-2">
|
||||||
<input
|
<input
|
||||||
id="sub_topic"
|
id="subTopic"
|
||||||
v-model="sub_topic"
|
v-model="subTopic"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:placeholder="$t('mqtt.topic_name')"
|
:placeholder="$t('mqtt.topic_name')"
|
||||||
@@ -110,7 +119,7 @@
|
|||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
id="subscribe"
|
id="subscribe"
|
||||||
name="get"
|
name="get"
|
||||||
:disabled="!cansubscribe"
|
:disabled="!canSubscribe"
|
||||||
:label="
|
:label="
|
||||||
subscriptionState ? $t('mqtt.unsubscribe') : $t('mqtt.subscribe')
|
subscriptionState ? $t('mqtt.unsubscribe') : $t('mqtt.subscribe')
|
||||||
"
|
"
|
||||||
@@ -122,261 +131,220 @@
|
|||||||
</AppPaneLayout>
|
</AppPaneLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import {
|
||||||
import Paho from "paho-mqtt"
|
computed,
|
||||||
import debounce from "lodash/debounce"
|
onMounted,
|
||||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
onUnmounted,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from "@nuxtjs/composition-api"
|
||||||
|
import debounce from "lodash/debounce"
|
||||||
|
import { MQTTConnection, MQTTError } from "~/helpers/realtime/MQTTConnection"
|
||||||
|
import {
|
||||||
|
useI18n,
|
||||||
|
useNuxt,
|
||||||
|
useReadonlyStream,
|
||||||
|
useStream,
|
||||||
|
useStreamSubscriber,
|
||||||
|
useToast,
|
||||||
|
} from "~/helpers/utils/composables"
|
||||||
import {
|
import {
|
||||||
MQTTEndpoint$,
|
|
||||||
setMQTTEndpoint,
|
|
||||||
MQTTConnectingState$,
|
|
||||||
MQTTConnectionState$,
|
|
||||||
setMQTTConnectingState,
|
|
||||||
setMQTTConnectionState,
|
|
||||||
MQTTSubscriptionState$,
|
|
||||||
setMQTTSubscriptionState,
|
|
||||||
MQTTSocket$,
|
|
||||||
setMQTTSocket,
|
|
||||||
MQTTLog$,
|
|
||||||
setMQTTLog,
|
|
||||||
addMQTTLogLine,
|
addMQTTLogLine,
|
||||||
|
MQTTConn$,
|
||||||
|
MQTTEndpoint$,
|
||||||
|
MQTTLog$,
|
||||||
|
setMQTTConn,
|
||||||
|
setMQTTEndpoint,
|
||||||
|
setMQTTLog,
|
||||||
} from "~/newstore/MQTTSession"
|
} from "~/newstore/MQTTSession"
|
||||||
import { useStream } from "~/helpers/utils/composables"
|
|
||||||
|
|
||||||
export default defineComponent({
|
const t = useI18n()
|
||||||
setup() {
|
const nuxt = useNuxt()
|
||||||
return {
|
const toast = useToast()
|
||||||
url: useStream(MQTTEndpoint$, "", setMQTTEndpoint),
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
connectionState: useStream(
|
|
||||||
MQTTConnectionState$,
|
const url = useStream(MQTTEndpoint$, "", setMQTTEndpoint)
|
||||||
false,
|
const log = useStream(MQTTLog$, [], setMQTTLog)
|
||||||
setMQTTConnectionState
|
const socket = useStream(MQTTConn$, new MQTTConnection(), setMQTTConn)
|
||||||
),
|
const connectionState = useReadonlyStream(
|
||||||
connectingState: useStream(
|
socket.value.connectionState$,
|
||||||
MQTTConnectingState$,
|
"DISCONNECTED"
|
||||||
false,
|
)
|
||||||
setMQTTConnectingState
|
const subscriptionState = useReadonlyStream(
|
||||||
),
|
socket.value.subscriptionState$,
|
||||||
subscriptionState: useStream(
|
false
|
||||||
MQTTSubscriptionState$,
|
)
|
||||||
false,
|
|
||||||
setMQTTSubscriptionState
|
const isUrlValid = ref(true)
|
||||||
),
|
const pubTopic = ref("")
|
||||||
log: useStream(MQTTLog$, null, setMQTTLog),
|
const subTopic = ref("")
|
||||||
client: useStream(MQTTSocket$, null, setMQTTSocket),
|
const message = ref("")
|
||||||
|
const username = ref("")
|
||||||
|
const password = ref("")
|
||||||
|
|
||||||
|
let worker: Worker
|
||||||
|
|
||||||
|
const canPublish = computed(
|
||||||
|
() =>
|
||||||
|
pubTopic.value !== "" &&
|
||||||
|
message.value !== "" &&
|
||||||
|
connectionState.value === "CONNECTED"
|
||||||
|
)
|
||||||
|
const canSubscribe = computed(
|
||||||
|
() => subTopic.value !== "" && connectionState.value === "CONNECTED"
|
||||||
|
)
|
||||||
|
|
||||||
|
const workerResponseHandler = ({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: { url: string; result: boolean }
|
||||||
|
}) => {
|
||||||
|
if (data.url === url.value) isUrlValid.value = data.result
|
||||||
}
|
}
|
||||||
},
|
|
||||||
data() {
|
onMounted(() => {
|
||||||
return {
|
worker = nuxt.value.$worker.createRejexWorker()
|
||||||
isUrlValid: true,
|
worker.addEventListener("message", workerResponseHandler)
|
||||||
pub_topic: "",
|
|
||||||
sub_topic: "",
|
subscribeToStream(socket.value.event$, (event) => {
|
||||||
msg: "",
|
switch (event?.type) {
|
||||||
manualDisconnect: false,
|
case "CONNECTING":
|
||||||
username: "",
|
log.value = [
|
||||||
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 }),
|
payload: `${t("state.connecting_to", { name: url.value })}`,
|
||||||
source: "info",
|
source: "info",
|
||||||
color: "var(--accent-color)",
|
color: "var(--accent-color)",
|
||||||
ts: new Date().toLocaleTimeString(),
|
ts: undefined,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const parseUrl = new URL(this.url)
|
break
|
||||||
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({
|
case "CONNECTED":
|
||||||
platform: "mqtt",
|
log.value = [
|
||||||
})
|
{
|
||||||
|
payload: `${t("state.connected_to", { name: url.value })}`,
|
||||||
|
source: "info",
|
||||||
|
color: "var(--accent-color)",
|
||||||
|
ts: Date.now(),
|
||||||
},
|
},
|
||||||
onConnectionFailure() {
|
]
|
||||||
this.connectingState = false
|
toast.success(`${t("state.connected")}`)
|
||||||
this.connectionState = false
|
break
|
||||||
|
|
||||||
|
case "MESSAGE_SENT":
|
||||||
addMQTTLogLine({
|
addMQTTLogLine({
|
||||||
payload: this.$t("error.something_went_wrong"),
|
prefix: `${event.message.topic}`,
|
||||||
|
payload: event.message.message,
|
||||||
|
source: "client",
|
||||||
|
ts: Date.now(),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case "MESSAGE_RECEIVED":
|
||||||
|
addMQTTLogLine({
|
||||||
|
prefix: `${event.message.topic}`,
|
||||||
|
payload: event.message.message,
|
||||||
|
source: "server",
|
||||||
|
ts: event.time,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case "SUBSCRIBED":
|
||||||
|
addMQTTLogLine({
|
||||||
|
payload: subscriptionState.value
|
||||||
|
? `${t("state.subscribed_success", { topic: subTopic.value })}`
|
||||||
|
: `${t("state.unsubscribed_success", { topic: subTopic.value })}`,
|
||||||
|
source: "server",
|
||||||
|
ts: event.time,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case "SUBSCRIPTION_FAILED":
|
||||||
|
addMQTTLogLine({
|
||||||
|
payload: subscriptionState.value
|
||||||
|
? `${t("state.subscribed_failed", { topic: subTopic.value })}`
|
||||||
|
: `${t("state.unsubscribed_failed", { topic: subTopic.value })}`,
|
||||||
|
source: "server",
|
||||||
|
ts: event.time,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case "ERROR":
|
||||||
|
addMQTTLogLine({
|
||||||
|
payload: getI18nError(event.error),
|
||||||
source: "info",
|
source: "info",
|
||||||
color: "#ff5555",
|
color: "#ff5555",
|
||||||
ts: new Date().toLocaleTimeString(),
|
ts: event.time,
|
||||||
})
|
})
|
||||||
},
|
break
|
||||||
onConnectionSuccess() {
|
|
||||||
this.connectingState = false
|
case "DISCONNECTED":
|
||||||
this.connectionState = true
|
|
||||||
addMQTTLogLine({
|
addMQTTLogLine({
|
||||||
payload: this.$t("state.connected_to", { name: this.url }),
|
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
||||||
source: "info",
|
source: "info",
|
||||||
color: "var(--accent-color)",
|
color: "#ff5555",
|
||||||
ts: new Date().toLocaleTimeString(),
|
ts: event.time,
|
||||||
})
|
})
|
||||||
this.$toast.success(this.$t("state.connected"))
|
toast.error(`${t("state.disconnected")}`)
|
||||||
},
|
break
|
||||||
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) {
|
const debouncer = debounce(function () {
|
||||||
this.disconnect()
|
worker.postMessage({ type: "ws", url: url.value })
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
watch(url, (newUrl) => {
|
||||||
|
if (newUrl) debouncer()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
worker.terminate()
|
||||||
|
})
|
||||||
|
|
||||||
|
// METHODS
|
||||||
|
const toggleConnection = () => {
|
||||||
|
// If it is connecting:
|
||||||
|
if (connectionState.value === "DISCONNECTED") {
|
||||||
|
return socket.value.connect(url.value, username.value, password.value)
|
||||||
|
}
|
||||||
|
// Otherwise, it's disconnecting.
|
||||||
|
socket.value.disconnect()
|
||||||
|
}
|
||||||
|
const publish = () => {
|
||||||
|
socket.value?.publish(pubTopic.value, message.value)
|
||||||
|
}
|
||||||
|
const toggleSubscription = () => {
|
||||||
|
if (subscriptionState.value) {
|
||||||
|
socket.value.unsubscribe(subTopic.value)
|
||||||
} else {
|
} else {
|
||||||
this.connect()
|
socket.value.subscribe(subTopic.value)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
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
|
const getI18nError = (error: MQTTError): string => {
|
||||||
},
|
if (typeof error === "string") return error
|
||||||
publish() {
|
|
||||||
try {
|
switch (error.type) {
|
||||||
this.client.publish(this.pub_topic, this.msg, 0, false)
|
case "CONNECTION_NOT_ESTABLISHED":
|
||||||
addMQTTLogLine({
|
return t("state.connection_lost").toString()
|
||||||
payload: `Published message: ${this.msg} to topic: ${this.pub_topic}`,
|
case "SUBSCRIPTION_FAILED":
|
||||||
ts: new Date().toLocaleTimeString(),
|
return t("state.mqtt_subscription_failed", {
|
||||||
source: "info",
|
topic: error.topic,
|
||||||
color: "var(--accent-color)",
|
}).toString()
|
||||||
})
|
case "PUBLISH_ERROR":
|
||||||
} catch (e) {
|
return t("state.publish_error", { topic: error.topic }).toString()
|
||||||
addMQTTLogLine({
|
case "CONNECTION_LOST":
|
||||||
payload:
|
return t("state.connection_lost").toString()
|
||||||
this.$t("error.something_went_wrong") +
|
case "CONNECTION_FAILED":
|
||||||
`while publishing msg: ${this.msg} to topic: ${this.pub_topic}`,
|
return t("state.connection_failed").toString()
|
||||||
source: "info",
|
default:
|
||||||
color: "#ff5555",
|
return t("state.disconnected_from", { name: url.value }).toString()
|
||||||
ts: new Date().toLocaleTimeString(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
toggleSubscription() {
|
|
||||||
if (this.subscriptionState) {
|
|
||||||
this.unsubscribe()
|
|
||||||
} else {
|
|
||||||
this.subscribe()
|
|
||||||
}
|
}
|
||||||
},
|
const clearLogEntries = () => {
|
||||||
subscribe() {
|
log.value = []
|
||||||
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>
|
</script>
|
||||||
|
|||||||
@@ -23,13 +23,16 @@
|
|||||||
class="flex px-4 py-2 font-semibold border rounded-l cursor-pointer bg-primaryLight border-divider text-secondaryDark w-26"
|
class="flex px-4 py-2 font-semibold border rounded-l cursor-pointer bg-primaryLight border-divider text-secondaryDark w-26"
|
||||||
:value="`Client ${clientVersion}`"
|
:value="`Client ${clientVersion}`"
|
||||||
readonly
|
readonly
|
||||||
:disabled="connectionState"
|
:disabled="
|
||||||
|
connectionState === 'CONNECTED' ||
|
||||||
|
connectionState === 'CONNECTING'
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col" role="menu">
|
<div class="flex flex-col" role="menu">
|
||||||
<SmartItem
|
<SmartItem
|
||||||
v-for="(_, version) in socketIoClients"
|
v-for="version in SIOVersions"
|
||||||
:key="`client-${version}`"
|
:key="`client-${version}`"
|
||||||
:label="`Client ${version}`"
|
:label="`Client ${version}`"
|
||||||
@click.native="onSelectVersion(version)"
|
@click.native="onSelectVersion(version)"
|
||||||
@@ -43,40 +46,64 @@
|
|||||||
type="url"
|
type="url"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
:class="{ error: !urlValid }"
|
:class="{ error: !isUrlValid }"
|
||||||
class="flex flex-1 w-full px-4 py-2 border bg-primaryLight border-divider text-secondaryDark"
|
class="flex flex-1 w-full px-4 py-2 border bg-primaryLight border-divider text-secondaryDark"
|
||||||
:placeholder="$t('socketio.url')"
|
:placeholder="`${t('socketio.url')}`"
|
||||||
:disabled="connectionState"
|
:disabled="
|
||||||
@keyup.enter="urlValid ? toggleConnection() : null"
|
connectionState === 'CONNECTED' ||
|
||||||
|
connectionState === 'CONNECTING'
|
||||||
|
"
|
||||||
|
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
id="socketio-path"
|
id="socketio-path"
|
||||||
v-model="path"
|
v-model="path"
|
||||||
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
|
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
:disabled="connectionState"
|
:disabled="
|
||||||
@keyup.enter="urlValid ? toggleConnection() : null"
|
connectionState === 'CONNECTED' ||
|
||||||
|
connectionState === 'CONNECTING'
|
||||||
|
"
|
||||||
|
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
id="connect"
|
id="connect"
|
||||||
:disabled="!urlValid"
|
:disabled="!isUrlValid"
|
||||||
name="connect"
|
name="connect"
|
||||||
class="w-32"
|
class="w-32"
|
||||||
:label="
|
:label="
|
||||||
!connectionState ? $t('action.connect') : $t('action.disconnect')
|
connectionState === 'DISCONNECTED'
|
||||||
|
? t('action.connect')
|
||||||
|
: t('action.disconnect')
|
||||||
"
|
"
|
||||||
:loading="connectingState"
|
:loading="connectionState === 'CONNECTING'"
|
||||||
@click.native="toggleConnection"
|
@click.native="toggleConnection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SmartTabs
|
||||||
|
v-model="selectedTab"
|
||||||
|
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||||
|
>
|
||||||
|
<SmartTab
|
||||||
|
:id="'communication'"
|
||||||
|
:label="`${t('websocket.communication')}`"
|
||||||
|
>
|
||||||
|
<RealtimeCommunication
|
||||||
|
:show-event-field="true"
|
||||||
|
:is-connected="connectionState === 'CONNECTED'"
|
||||||
|
@send-message="sendMessage($event)"
|
||||||
|
></RealtimeCommunication>
|
||||||
|
</SmartTab>
|
||||||
|
<SmartTab :id="'protocols'" :label="`${t('request.authorization')}`">
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
||||||
>
|
>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<label class="font-semibold text-secondaryLight">
|
<label class="font-semibold text-secondaryLight">
|
||||||
{{ $t("authorization.type") }}
|
{{ t("authorization.type") }}
|
||||||
</label>
|
</label>
|
||||||
<tippy
|
<tippy
|
||||||
ref="authTypeOptions"
|
ref="authTypeOptions"
|
||||||
@@ -133,18 +160,18 @@
|
|||||||
class="px-2"
|
class="px-2"
|
||||||
@change="authActive = !authActive"
|
@change="authActive = !authActive"
|
||||||
>
|
>
|
||||||
{{ $t("state.enabled") }}
|
{{ t("state.enabled") }}
|
||||||
</SmartCheckbox>
|
</SmartCheckbox>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
to="https://docs.hoppscotch.io/features/authorization"
|
to="https://docs.hoppscotch.io/features/authorization"
|
||||||
blank
|
blank
|
||||||
:title="$t('app.wiki')"
|
:title="t('app.wiki')"
|
||||||
svg="help-circle"
|
svg="help-circle"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="$t('action.clear')"
|
:title="t('action.clear')"
|
||||||
svg="trash-2"
|
svg="trash-2"
|
||||||
@click.native="clearContent"
|
@click.native="clearContent"
|
||||||
/>
|
/>
|
||||||
@@ -158,14 +185,14 @@
|
|||||||
:src="`/images/states/${$colorMode.value}/login.svg`"
|
:src="`/images/states/${$colorMode.value}/login.svg`"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
:alt="$t('empty.authorization')"
|
:alt="`${t('empty.authorization')}`"
|
||||||
/>
|
/>
|
||||||
<span class="pb-4 text-center">
|
<span class="pb-4 text-center">
|
||||||
This SocketIO connection does not use any authentication.
|
{{ t("socketio.connection_not_authorized") }}
|
||||||
</span>
|
</span>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
outline
|
outline
|
||||||
:label="$t('app.documentation')"
|
:label="t('app.documentation')"
|
||||||
to="https://docs.hoppscotch.io/features/authorization"
|
to="https://docs.hoppscotch.io/features/authorization"
|
||||||
blank
|
blank
|
||||||
svg="external-link"
|
svg="external-link"
|
||||||
@@ -187,335 +214,222 @@
|
|||||||
>
|
>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<div class="pb-2 text-secondaryLight">
|
<div class="pb-2 text-secondaryLight">
|
||||||
{{ $t("helpers.authorization") }}
|
{{ t("helpers.authorization") }}
|
||||||
</div>
|
</div>
|
||||||
<SmartAnchor
|
<SmartAnchor
|
||||||
class="link"
|
class="link"
|
||||||
:label="`${$t('authorization.learn')} \xA0 →`"
|
:label="`${t('authorization.learn')} \xA0 →`"
|
||||||
to="https://docs.hoppscotch.io/features/authorization"
|
to="https://docs.hoppscotch.io/features/authorization"
|
||||||
blank
|
blank
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</SmartTab>
|
||||||
|
</SmartTabs>
|
||||||
</template>
|
</template>
|
||||||
<template #secondary>
|
<template #secondary>
|
||||||
<RealtimeLog :title="$t('socketio.log')" :log="log" />
|
<RealtimeLog
|
||||||
</template>
|
:title="t('socketio.log')"
|
||||||
<template #sidebar>
|
:log="log"
|
||||||
<div class="flex items-center justify-between p-4">
|
@delete="clearLogEntries()"
|
||||||
<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>
|
</template>
|
||||||
</AppPaneLayout>
|
</AppPaneLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent, ref } from "@nuxtjs/composition-api"
|
import { onMounted, onUnmounted, ref, watch } 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 debounce from "lodash/debounce"
|
||||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
|
||||||
import {
|
import {
|
||||||
SIOEndpoint$,
|
SIOConnection,
|
||||||
setSIOEndpoint,
|
SIOError,
|
||||||
SIOVersion$,
|
SIOMessage,
|
||||||
setSIOVersion,
|
SOCKET_CLIENTS,
|
||||||
SIOPath$,
|
} from "~/helpers/realtime/SIOConnection"
|
||||||
setSIOPath,
|
import {
|
||||||
SIOConnectionState$,
|
useI18n,
|
||||||
SIOConnectingState$,
|
useNuxt,
|
||||||
setSIOConnectionState,
|
useReadonlyStream,
|
||||||
setSIOConnectingState,
|
useStream,
|
||||||
SIOSocket$,
|
useStreamSubscriber,
|
||||||
setSIOSocket,
|
useToast,
|
||||||
SIOLog$,
|
} from "~/helpers/utils/composables"
|
||||||
setSIOLog,
|
import {
|
||||||
addSIOLogLine,
|
addSIOLogLine,
|
||||||
|
setSIOEndpoint,
|
||||||
|
setSIOLog,
|
||||||
|
setSIOPath,
|
||||||
|
setSIOVersion,
|
||||||
|
SIOClientVersion,
|
||||||
|
SIOEndpoint$,
|
||||||
|
SIOLog$,
|
||||||
|
SIOPath$,
|
||||||
|
SIOVersion$,
|
||||||
} from "~/newstore/SocketIOSession"
|
} from "~/newstore/SocketIOSession"
|
||||||
import { useStream } from "~/helpers/utils/composables"
|
|
||||||
|
|
||||||
const socketIoClients = {
|
const t = useI18n()
|
||||||
v4: ClientV4,
|
const toast = useToast()
|
||||||
v3: ClientV3,
|
const nuxt = useNuxt()
|
||||||
v2: ClientV2,
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
|
|
||||||
|
type SIOTab = "communication" | "protocols"
|
||||||
|
const selectedTab = ref<SIOTab>("communication")
|
||||||
|
|
||||||
|
const SIOVersions = Object.keys(SOCKET_CLIENTS)
|
||||||
|
const url = useStream(SIOEndpoint$, "", setSIOEndpoint)
|
||||||
|
const clientVersion = useStream(SIOVersion$, "v4", setSIOVersion)
|
||||||
|
const path = useStream(SIOPath$, "", setSIOPath)
|
||||||
|
const socket = new SIOConnection()
|
||||||
|
const connectionState = useReadonlyStream(
|
||||||
|
socket.connectionState$,
|
||||||
|
"DISCONNECTED"
|
||||||
|
)
|
||||||
|
const log = useStream(SIOLog$, [], setSIOLog)
|
||||||
|
const authTypeOptions = ref<any>(null)
|
||||||
|
const versionOptions = ref<any | null>(null)
|
||||||
|
|
||||||
|
const isUrlValid = ref(true)
|
||||||
|
const authType = ref<"None" | "Bearer">("None")
|
||||||
|
const bearerToken = ref("")
|
||||||
|
const authActive = ref(true)
|
||||||
|
|
||||||
|
let worker: Worker
|
||||||
|
|
||||||
|
const workerResponseHandler = ({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: { url: string; result: boolean }
|
||||||
|
}) => {
|
||||||
|
if (data.url === url.value) isUrlValid.value = data.result
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
const getMessagePayload = (data: SIOMessage): string =>
|
||||||
setup() {
|
typeof data.value === "object" ? JSON.stringify(data.value) : `${data.value}`
|
||||||
return {
|
|
||||||
socketIoClients,
|
const getErrorPayload = (error: SIOError): string => {
|
||||||
url: useStream(SIOEndpoint$, "", setSIOEndpoint),
|
switch (error.type) {
|
||||||
clientVersion: useStream(SIOVersion$, "", setSIOVersion),
|
case "CONNECTION":
|
||||||
path: useStream(SIOPath$, "", setSIOPath),
|
return t("state.connection_error").toString()
|
||||||
connectingState: useStream(
|
case "RECONNECT_ERROR":
|
||||||
SIOConnectingState$,
|
return t("state.reconnection_error").toString()
|
||||||
false,
|
default:
|
||||||
setSIOConnectingState
|
return t("state.disconnected_from", { name: url.value }).toString()
|
||||||
),
|
|
||||||
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: {
|
onMounted(() => {
|
||||||
urlValid() {
|
worker = nuxt.value.$worker.createRejexWorker()
|
||||||
return this.isUrlValid
|
worker.addEventListener("message", workerResponseHandler)
|
||||||
},
|
|
||||||
},
|
subscribeToStream(socket.event$, (event) => {
|
||||||
watch: {
|
switch (event?.type) {
|
||||||
url() {
|
case "CONNECTING":
|
||||||
this.debouncer()
|
log.value = [
|
||||||
},
|
|
||||||
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 }),
|
payload: `${t("state.connecting_to", { name: url.value })}`,
|
||||||
source: "info",
|
source: "info",
|
||||||
color: "var(--accent-color)",
|
color: "var(--accent-color)",
|
||||||
|
ts: undefined,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
break
|
||||||
|
|
||||||
try {
|
case "CONNECTED":
|
||||||
if (!this.path) {
|
log.value = [
|
||||||
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 }),
|
payload: `${t("state.connected_to", { name: url.value })}`,
|
||||||
source: "info",
|
source: "info",
|
||||||
color: "var(--accent-color)",
|
color: "var(--accent-color)",
|
||||||
ts: new Date().toLocaleTimeString(),
|
ts: event.time,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.$toast.success(this.$t("state.connected"))
|
toast.success(`${t("state.connected")}`)
|
||||||
})
|
break
|
||||||
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({
|
case "MESSAGE_SENT":
|
||||||
platform: "socketio",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
disconnect() {
|
|
||||||
this.io.close()
|
|
||||||
},
|
|
||||||
handleError(error) {
|
|
||||||
this.disconnect()
|
|
||||||
this.connectingState = false
|
|
||||||
this.connectionState = false
|
|
||||||
addSIOLogLine({
|
addSIOLogLine({
|
||||||
payload: this.$t("error.something_went_wrong"),
|
prefix: `[${event.message.eventName}]`,
|
||||||
source: "info",
|
payload: getMessagePayload(event.message),
|
||||||
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",
|
source: "client",
|
||||||
ts: new Date().toLocaleTimeString(),
|
ts: event.time,
|
||||||
})
|
})
|
||||||
this.communication.inputs = [""]
|
break
|
||||||
|
|
||||||
|
case "MESSAGE_RECEIVED":
|
||||||
|
addSIOLogLine({
|
||||||
|
prefix: `[${event.message.eventName}]`,
|
||||||
|
payload: getMessagePayload(event.message),
|
||||||
|
source: "server",
|
||||||
|
ts: event.time,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case "ERROR":
|
||||||
|
addSIOLogLine({
|
||||||
|
payload: getErrorPayload(event.error),
|
||||||
|
source: "info",
|
||||||
|
color: "#ff5555",
|
||||||
|
ts: event.time,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case "DISCONNECTED":
|
||||||
|
addSIOLogLine({
|
||||||
|
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
||||||
|
source: "info",
|
||||||
|
color: "#ff5555",
|
||||||
|
ts: event.time,
|
||||||
|
})
|
||||||
|
toast.error(`${t("state.disconnected")}`)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onSelectVersion(version) {
|
|
||||||
this.clientVersion = version
|
|
||||||
this.$refs.versionOptions.tippy().hide()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(url, (newUrl) => {
|
||||||
|
if (newUrl) debouncer()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(connectionState, (connected) => {
|
||||||
|
if (connected) versionOptions.value.tippy().disable()
|
||||||
|
else versionOptions.value.tippy().enable()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
worker.terminate()
|
||||||
|
})
|
||||||
|
|
||||||
|
const debouncer = debounce(function () {
|
||||||
|
worker.postMessage({ type: "socketio", url: url.value })
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
const toggleConnection = () => {
|
||||||
|
// If it is connecting:
|
||||||
|
if (connectionState.value === "DISCONNECTED") {
|
||||||
|
return socket.connect({
|
||||||
|
url: url.value,
|
||||||
|
path: path.value || "/socket.io",
|
||||||
|
clientVersion: clientVersion.value,
|
||||||
|
auth: authActive.value
|
||||||
|
? {
|
||||||
|
type: authType.value,
|
||||||
|
token: bearerToken.value,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Otherwise, it's disconnecting.
|
||||||
|
socket.disconnect()
|
||||||
|
}
|
||||||
|
const sendMessage = (event: { message: string; eventName: string }) => {
|
||||||
|
socket.sendMessage(event)
|
||||||
|
}
|
||||||
|
const onSelectVersion = (version: SIOClientVersion) => {
|
||||||
|
clientVersion.value = version
|
||||||
|
versionOptions.value.tippy().hide()
|
||||||
|
}
|
||||||
|
const clearLogEntries = () => {
|
||||||
|
log.value = []
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,11 +11,13 @@
|
|||||||
v-model="server"
|
v-model="server"
|
||||||
type="url"
|
type="url"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:class="{ error: !serverValid }"
|
:class="{ error: !isUrlValid }"
|
||||||
class="flex flex-1 w-full px-4 py-2 border rounded-l bg-primaryLight border-divider text-secondaryDark"
|
class="flex flex-1 w-full px-4 py-2 border rounded-l bg-primaryLight border-divider text-secondaryDark"
|
||||||
:placeholder="$t('sse.url')"
|
:placeholder="$t('sse.url')"
|
||||||
:disabled="connectionSSEState"
|
:disabled="
|
||||||
@keyup.enter="serverValid ? toggleSSEConnection() : null"
|
connectionState === 'STARTED' || connectionState === 'STARTING'
|
||||||
|
"
|
||||||
|
@keyup.enter="isUrlValid ? toggleSSEConnection() : null"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
for="event-type"
|
for="event-type"
|
||||||
@@ -28,35 +30,42 @@
|
|||||||
v-model="eventType"
|
v-model="eventType"
|
||||||
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
|
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
:disabled="connectionSSEState"
|
:disabled="
|
||||||
@keyup.enter="serverValid ? toggleSSEConnection() : null"
|
connectionState === 'STARTED' || connectionState === 'STARTING'
|
||||||
|
"
|
||||||
|
@keyup.enter="isUrlValid ? toggleSSEConnection() : null"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
id="start"
|
id="start"
|
||||||
:disabled="!serverValid"
|
:disabled="!isUrlValid"
|
||||||
name="start"
|
name="start"
|
||||||
class="w-32"
|
class="w-32"
|
||||||
:label="
|
:label="
|
||||||
!connectionSSEState ? $t('action.start') : $t('action.stop')
|
connectionState === 'STOPPED'
|
||||||
|
? t('action.start')
|
||||||
|
: t('action.stop')
|
||||||
"
|
"
|
||||||
:loading="connectingState"
|
:loading="connectionState === 'STARTING'"
|
||||||
@click.native="toggleSSEConnection"
|
@click.native="toggleSSEConnection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #secondary>
|
<template #secondary>
|
||||||
<RealtimeLog :title="$t('sse.log')" :log="log" />
|
<RealtimeLog
|
||||||
|
:title="$t('sse.log')"
|
||||||
|
:log="log"
|
||||||
|
@delete="clearLogEntries()"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</AppPaneLayout>
|
</AppPaneLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { ref, watch, onUnmounted, onMounted } from "@nuxtjs/composition-api"
|
||||||
import "splitpanes/dist/splitpanes.css"
|
import "splitpanes/dist/splitpanes.css"
|
||||||
import debounce from "lodash/debounce"
|
import debounce from "lodash/debounce"
|
||||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
|
||||||
import {
|
import {
|
||||||
SSEEndpoint$,
|
SSEEndpoint$,
|
||||||
setSSEEndpoint,
|
setSSEEndpoint,
|
||||||
@@ -64,159 +73,127 @@ import {
|
|||||||
setSSEEventType,
|
setSSEEventType,
|
||||||
SSESocket$,
|
SSESocket$,
|
||||||
setSSESocket,
|
setSSESocket,
|
||||||
SSEConnectingState$,
|
|
||||||
SSEConnectionState$,
|
|
||||||
setSSEConnectionState,
|
|
||||||
setSSEConnectingState,
|
|
||||||
SSELog$,
|
SSELog$,
|
||||||
setSSELog,
|
setSSELog,
|
||||||
addSSELogLine,
|
addSSELogLine,
|
||||||
} from "~/newstore/SSESession"
|
} from "~/newstore/SSESession"
|
||||||
import { useStream } from "~/helpers/utils/composables"
|
import {
|
||||||
|
useNuxt,
|
||||||
|
useStream,
|
||||||
|
useToast,
|
||||||
|
useI18n,
|
||||||
|
useStreamSubscriber,
|
||||||
|
useReadonlyStream,
|
||||||
|
} from "~/helpers/utils/composables"
|
||||||
|
import { SSEConnection } from "~/helpers/realtime/SSEConnection"
|
||||||
|
|
||||||
export default defineComponent({
|
const t = useI18n()
|
||||||
setup() {
|
const nuxt = useNuxt()
|
||||||
return {
|
const toast = useToast()
|
||||||
connectionSSEState: useStream(
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
SSEConnectionState$,
|
|
||||||
false,
|
const sse = useStream(SSESocket$, new SSEConnection(), setSSESocket)
|
||||||
setSSEConnectionState
|
const connectionState = useReadonlyStream(sse.value.connectionState$, "STOPPED")
|
||||||
),
|
const server = useStream(SSEEndpoint$, "", setSSEEndpoint)
|
||||||
connectingState: useStream(
|
const eventType = useStream(SSEEventType$, "", setSSEEventType)
|
||||||
SSEConnectingState$,
|
const log = useStream(SSELog$, [], setSSELog)
|
||||||
false,
|
|
||||||
setSSEConnectingState
|
const isUrlValid = ref(true)
|
||||||
),
|
|
||||||
server: useStream(SSEEndpoint$, "", setSSEEndpoint),
|
let worker: Worker
|
||||||
eventType: useStream(SSEEventType$, "", setSSEEventType),
|
|
||||||
sse: useStream(SSESocket$, null, setSSESocket),
|
const debouncer = debounce(function () {
|
||||||
log: useStream(SSELog$, [], setSSELog),
|
worker.postMessage({ type: "sse", url: server.value })
|
||||||
}
|
}, 1000)
|
||||||
},
|
|
||||||
data() {
|
watch(server, (url) => {
|
||||||
return {
|
if (url) debouncer()
|
||||||
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"))
|
|
||||||
|
const workerResponseHandler = ({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: { url: string; result: boolean }
|
||||||
|
}) => {
|
||||||
|
if (data.url === server.value) isUrlValid.value = data.result
|
||||||
}
|
}
|
||||||
this.sse.addEventListener(this.eventType, ({ data }) => {
|
|
||||||
|
onMounted(() => {
|
||||||
|
worker = nuxt.value.$worker.createRejexWorker()
|
||||||
|
worker.addEventListener("message", workerResponseHandler)
|
||||||
|
|
||||||
|
subscribeToStream(sse.value.event$, (event) => {
|
||||||
|
switch (event?.type) {
|
||||||
|
case "STARTING":
|
||||||
|
log.value = [
|
||||||
|
{
|
||||||
|
payload: `${t("state.connecting_to", { name: server.value })}`,
|
||||||
|
source: "info",
|
||||||
|
color: "var(--accent-color)",
|
||||||
|
ts: undefined,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
break
|
||||||
|
|
||||||
|
case "STARTED":
|
||||||
|
log.value = [
|
||||||
|
{
|
||||||
|
payload: `${t("state.connected_to", { name: server.value })}`,
|
||||||
|
source: "info",
|
||||||
|
color: "var(--accent-color)",
|
||||||
|
ts: Date.now(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
toast.success(`${t("state.connected")}`)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "MESSAGE_RECEIVED":
|
||||||
addSSELogLine({
|
addSSELogLine({
|
||||||
payload: data,
|
payload: event.message,
|
||||||
source: "server",
|
source: "server",
|
||||||
ts: new Date().toLocaleTimeString(),
|
ts: event.time,
|
||||||
})
|
})
|
||||||
})
|
break
|
||||||
} catch (e) {
|
|
||||||
this.handleSSEError(e)
|
case "ERROR":
|
||||||
this.$toast.error(this.$t("error.something_went_wrong"))
|
addSSELogLine({
|
||||||
}
|
payload: t("error.browser_support_sse").toString(),
|
||||||
} else {
|
|
||||||
this.log = [
|
|
||||||
{
|
|
||||||
payload: this.$t("error.browser_support_sse"),
|
|
||||||
source: "info",
|
source: "info",
|
||||||
color: "#ff5555",
|
color: "#ff5555",
|
||||||
ts: new Date().toLocaleTimeString(),
|
ts: event.time,
|
||||||
},
|
})
|
||||||
]
|
break
|
||||||
|
|
||||||
|
case "STOPPED":
|
||||||
|
addSSELogLine({
|
||||||
|
payload: t("state.disconnected_from", {
|
||||||
|
name: server.value,
|
||||||
|
}).toString(),
|
||||||
|
source: "info",
|
||||||
|
color: "#ff5555",
|
||||||
|
ts: event.time,
|
||||||
|
})
|
||||||
|
toast.error(`${t("state.disconnected")}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// METHODS
|
||||||
|
|
||||||
|
const toggleSSEConnection = () => {
|
||||||
|
// If it is connecting:
|
||||||
|
if (connectionState.value === "STOPPED") {
|
||||||
|
return sse.value.start(server.value, eventType.value)
|
||||||
|
}
|
||||||
|
// Otherwise, it's disconnecting.
|
||||||
|
sse.value.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
logHoppRequestRunToAnalytics({
|
onUnmounted(() => {
|
||||||
platform: "sse",
|
worker.terminate()
|
||||||
})
|
|
||||||
},
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
const clearLogEntries = () => {
|
||||||
|
log.value = []
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,40 +12,59 @@
|
|||||||
type="url"
|
type="url"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
:class="{ error: !urlValid }"
|
:class="{ error: !isUrlValid }"
|
||||||
:placeholder="$t('websocket.url')"
|
:placeholder="`${t('websocket.url')}`"
|
||||||
:disabled="connectionState"
|
:disabled="
|
||||||
@keyup.enter="urlValid ? toggleConnection() : null"
|
connectionState === 'CONNECTED' ||
|
||||||
|
connectionState === 'CONNECTING'
|
||||||
|
"
|
||||||
|
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||||
/>
|
/>
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
id="connect"
|
id="connect"
|
||||||
:disabled="!urlValid"
|
:disabled="!isUrlValid"
|
||||||
class="w-32"
|
class="w-32"
|
||||||
name="connect"
|
name="connect"
|
||||||
:label="
|
:label="
|
||||||
!connectionState ? $t('action.connect') : $t('action.disconnect')
|
connectionState === 'DISCONNECTED'
|
||||||
|
? t('action.connect')
|
||||||
|
: t('action.disconnect')
|
||||||
"
|
"
|
||||||
:loading="connectingState"
|
:loading="connectionState === 'CONNECTING'"
|
||||||
@click.native="toggleConnection"
|
@click.native="toggleConnection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<SmartTabs
|
||||||
|
v-model="selectedTab"
|
||||||
|
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||||
|
>
|
||||||
|
<SmartTab
|
||||||
|
:id="'communication'"
|
||||||
|
:label="`${$t('websocket.communication')}`"
|
||||||
|
>
|
||||||
|
<RealtimeCommunication
|
||||||
|
:is-connected="connectionState === 'CONNECTED'"
|
||||||
|
@send-message="sendMessage($event)"
|
||||||
|
></RealtimeCommunication>
|
||||||
|
</SmartTab>
|
||||||
|
<SmartTab :id="'protocols'" :label="`${$t('websocket.protocols')}`">
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
||||||
>
|
>
|
||||||
<label class="font-semibold text-secondaryLight">
|
<label class="font-semibold text-secondaryLight">
|
||||||
{{ $t("websocket.protocols") }}
|
{{ t("websocket.protocols") }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="$t('action.clear_all')"
|
:title="t('action.clear_all')"
|
||||||
svg="trash-2"
|
svg="trash-2"
|
||||||
@click.native="clearContent"
|
@click.native="clearContent"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="$t('add.new')"
|
:title="t('add.new')"
|
||||||
svg="plus"
|
svg="plus"
|
||||||
@click.native="addProtocol"
|
@click.native="addProtocol"
|
||||||
/>
|
/>
|
||||||
@@ -79,7 +98,7 @@
|
|||||||
<input
|
<input
|
||||||
v-model="protocol.value"
|
v-model="protocol.value"
|
||||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
class="flex flex-1 px-4 py-2 bg-transparent"
|
||||||
:placeholder="$t('count.protocol', { count: index + 1 })"
|
:placeholder="`${t('count.protocol', { count: index + 1 })}`"
|
||||||
name="message"
|
name="message"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@@ -96,9 +115,9 @@
|
|||||||
:title="
|
:title="
|
||||||
protocol.hasOwnProperty('active')
|
protocol.hasOwnProperty('active')
|
||||||
? protocol.active
|
? protocol.active
|
||||||
? $t('action.turn_off')
|
? t('action.turn_off')
|
||||||
: $t('action.turn_on')
|
: t('action.turn_on')
|
||||||
: $t('action.turn_off')
|
: t('action.turn_off')
|
||||||
"
|
"
|
||||||
:svg="
|
:svg="
|
||||||
protocol.hasOwnProperty('active')
|
protocol.hasOwnProperty('active')
|
||||||
@@ -119,10 +138,10 @@
|
|||||||
<span>
|
<span>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="$t('action.remove')"
|
:title="t('action.remove')"
|
||||||
svg="trash"
|
svg="trash"
|
||||||
color="red"
|
color="red"
|
||||||
@click.native="deleteProtocol({ index })"
|
@click.native="deleteProtocol(index)"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,56 +154,28 @@
|
|||||||
:src="`/images/states/${$colorMode.value}/add_category.svg`"
|
:src="`/images/states/${$colorMode.value}/add_category.svg`"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
:alt="$t('empty.protocols')"
|
:alt="`${t('empty.protocols')}`"
|
||||||
/>
|
/>
|
||||||
<span class="mb-4 text-center">
|
<span class="mb-4 text-center">
|
||||||
{{ $t("empty.protocols") }}
|
{{ t("empty.protocols") }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</SmartTab>
|
||||||
|
</SmartTabs>
|
||||||
</template>
|
</template>
|
||||||
<template #secondary>
|
<template #secondary>
|
||||||
<RealtimeLog :title="$t('websocket.log')" :log="log" />
|
<RealtimeLog
|
||||||
</template>
|
:title="$t('websocket.log')"
|
||||||
<template #sidebar>
|
:log="log"
|
||||||
<div class="flex items-center justify-between p-4">
|
@delete="clearLogEntries()"
|
||||||
<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>
|
</template>
|
||||||
</AppPaneLayout>
|
</AppPaneLayout>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
<script>
|
import { ref, watch, onUnmounted, onMounted } from "@nuxtjs/composition-api"
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
|
||||||
import debounce from "lodash/debounce"
|
import debounce from "lodash/debounce"
|
||||||
import draggable from "vuedraggable"
|
import draggable from "vuedraggable"
|
||||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
|
||||||
import {
|
import {
|
||||||
setWSEndpoint,
|
setWSEndpoint,
|
||||||
WSEndpoint$,
|
WSEndpoint$,
|
||||||
@@ -194,62 +185,51 @@ import {
|
|||||||
deleteWSProtocol,
|
deleteWSProtocol,
|
||||||
updateWSProtocol,
|
updateWSProtocol,
|
||||||
deleteAllWSProtocols,
|
deleteAllWSProtocols,
|
||||||
WSSocket$,
|
|
||||||
setWSSocket,
|
|
||||||
setWSConnectionState,
|
|
||||||
setWSConnectingState,
|
|
||||||
WSConnectionState$,
|
|
||||||
WSConnectingState$,
|
|
||||||
addWSLogLine,
|
addWSLogLine,
|
||||||
WSLog$,
|
WSLog$,
|
||||||
setWSLog,
|
setWSLog,
|
||||||
|
HoppWSProtocol,
|
||||||
|
setWSSocket,
|
||||||
|
WSSocket$,
|
||||||
} from "~/newstore/WebSocketSession"
|
} from "~/newstore/WebSocketSession"
|
||||||
import { useStream } from "~/helpers/utils/composables"
|
import {
|
||||||
|
useI18n,
|
||||||
|
useStream,
|
||||||
|
useToast,
|
||||||
|
useNuxt,
|
||||||
|
useStreamSubscriber,
|
||||||
|
useReadonlyStream,
|
||||||
|
} from "~/helpers/utils/composables"
|
||||||
|
import { WSConnection, WSErrorMessage } from "~/helpers/realtime/WSConnection"
|
||||||
|
|
||||||
export default defineComponent({
|
const nuxt = useNuxt()
|
||||||
components: {
|
const t = useI18n()
|
||||||
draggable,
|
const toast = useToast()
|
||||||
},
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
setup() {
|
|
||||||
return {
|
const selectedTab = ref<"communication" | "protocols">("communication")
|
||||||
url: useStream(WSEndpoint$, "", setWSEndpoint),
|
const url = useStream(WSEndpoint$, "", setWSEndpoint)
|
||||||
protocols: useStream(WSProtocols$, [], setWSProtocols),
|
const protocols = useStream(WSProtocols$, [], setWSProtocols)
|
||||||
connectionState: useStream(
|
|
||||||
WSConnectionState$,
|
const socket = useStream(WSSocket$, new WSConnection(), setWSSocket)
|
||||||
false,
|
|
||||||
setWSConnectionState
|
const connectionState = useReadonlyStream(
|
||||||
),
|
socket.value.connectionState$,
|
||||||
connectingState: useStream(
|
"DISCONNECTED"
|
||||||
WSConnectingState$,
|
)
|
||||||
false,
|
|
||||||
setWSConnectingState
|
const log = useStream(WSLog$, [], setWSLog)
|
||||||
),
|
// DATA
|
||||||
socket: useStream(WSSocket$, null, setWSSocket),
|
const isUrlValid = ref(true)
|
||||||
log: useStream(WSLog$, [], setWSLog),
|
const activeProtocols = ref<string[]>([])
|
||||||
}
|
let worker: Worker
|
||||||
},
|
watch(url, (newUrl) => {
|
||||||
data() {
|
if (newUrl) debouncer()
|
||||||
return {
|
})
|
||||||
isUrlValid: true,
|
watch(
|
||||||
communication: {
|
protocols,
|
||||||
input: "",
|
(newProtocols) => {
|
||||||
},
|
activeProtocols.value = newProtocols
|
||||||
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) =>
|
.filter((item) =>
|
||||||
Object.prototype.hasOwnProperty.call(item, "active")
|
Object.prototype.hasOwnProperty.call(item, "active")
|
||||||
? item.active === true
|
? item.active === true
|
||||||
@@ -257,177 +237,133 @@ export default defineComponent({
|
|||||||
)
|
)
|
||||||
.map(({ value }) => value)
|
.map(({ value }) => value)
|
||||||
},
|
},
|
||||||
deep: true,
|
{ deep: true }
|
||||||
},
|
)
|
||||||
},
|
const workerResponseHandler = ({
|
||||||
created() {
|
data,
|
||||||
if (process.browser) {
|
}: {
|
||||||
this.worker = this.$worker.createRejexWorker()
|
data: { url: string; result: boolean }
|
||||||
this.worker.addEventListener("message", this.workerResponseHandler)
|
}) => {
|
||||||
}
|
if (data.url === url.value) isUrlValid.value = data.result
|
||||||
},
|
|
||||||
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({
|
const getErrorPayload = (error: WSErrorMessage): string => {
|
||||||
platform: "wss",
|
if (error instanceof SyntaxError) {
|
||||||
})
|
return error.message
|
||||||
},
|
|
||||||
disconnect() {
|
|
||||||
if (this.socket) {
|
|
||||||
this.socket.close()
|
|
||||||
this.connectionState = false
|
|
||||||
this.connectingState = false
|
|
||||||
}
|
}
|
||||||
},
|
return t("error.something_went_wrong").toString()
|
||||||
handleError(error) {
|
}
|
||||||
this.disconnect()
|
|
||||||
this.connectionState = false
|
onMounted(() => {
|
||||||
addWSLogLine({
|
worker = nuxt.value.$worker.createRejexWorker()
|
||||||
payload: this.$t("error.something_went_wrong"),
|
worker.addEventListener("message", workerResponseHandler)
|
||||||
|
|
||||||
|
subscribeToStream(socket.value.event$, (event) => {
|
||||||
|
switch (event?.type) {
|
||||||
|
case "CONNECTING":
|
||||||
|
log.value = [
|
||||||
|
{
|
||||||
|
payload: `${t("state.connecting_to", { name: url.value })}`,
|
||||||
source: "info",
|
source: "info",
|
||||||
color: "#ff5555",
|
color: "var(--accent-color)",
|
||||||
ts: new Date().toLocaleTimeString(),
|
ts: undefined,
|
||||||
})
|
|
||||||
if (error !== null)
|
|
||||||
addWSLogLine({
|
|
||||||
payload: error,
|
|
||||||
source: "info",
|
|
||||||
color: "#ff5555",
|
|
||||||
ts: new Date().toLocaleTimeString(),
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
sendMessage() {
|
]
|
||||||
const message = this.communication.input
|
break
|
||||||
this.socket.send(message)
|
|
||||||
|
case "CONNECTED":
|
||||||
|
log.value = [
|
||||||
|
{
|
||||||
|
payload: `${t("state.connected_to", { name: url.value })}`,
|
||||||
|
source: "info",
|
||||||
|
color: "var(--accent-color)",
|
||||||
|
ts: Date.now(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
toast.success(`${t("state.connected")}`)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "MESSAGE_SENT":
|
||||||
addWSLogLine({
|
addWSLogLine({
|
||||||
payload: message,
|
payload: event.message,
|
||||||
source: "client",
|
source: "client",
|
||||||
ts: new Date().toLocaleTimeString(),
|
ts: Date.now(),
|
||||||
})
|
})
|
||||||
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
|
break
|
||||||
case "down":
|
|
||||||
if (length > 0 && this.currentIndex > -1) {
|
case "MESSAGE_RECEIVED":
|
||||||
if (this.currentIndex === length - 1) {
|
addWSLogLine({
|
||||||
this.currentIndex = -1
|
payload: event.message,
|
||||||
this.communication.input = ""
|
source: "server",
|
||||||
} else if (this.currentIndex < length - 1) {
|
ts: event.time,
|
||||||
this.currentIndex = this.currentIndex + 1
|
})
|
||||||
this.communication.input =
|
break
|
||||||
clientMessages[this.currentIndex].payload
|
|
||||||
}
|
case "ERROR":
|
||||||
}
|
addWSLogLine({
|
||||||
|
payload: getErrorPayload(event.error),
|
||||||
|
source: "info",
|
||||||
|
color: "#ff5555",
|
||||||
|
ts: event.time,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case "DISCONNECTED":
|
||||||
|
addWSLogLine({
|
||||||
|
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
||||||
|
source: "info",
|
||||||
|
color: "#ff5555",
|
||||||
|
ts: event.time,
|
||||||
|
})
|
||||||
|
toast.error(`${t("state.disconnected")}`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
addProtocol() {
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (worker) worker.terminate()
|
||||||
|
})
|
||||||
|
const clearContent = () => {
|
||||||
|
deleteAllWSProtocols()
|
||||||
|
}
|
||||||
|
const debouncer = debounce(function () {
|
||||||
|
worker.postMessage({ type: "ws", url: url.value })
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
const toggleConnection = () => {
|
||||||
|
// If it is connecting:
|
||||||
|
if (connectionState.value === "DISCONNECTED") {
|
||||||
|
return socket.value.connect(url.value, activeProtocols.value)
|
||||||
|
}
|
||||||
|
// Otherwise, it's disconnecting.
|
||||||
|
socket.value.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendMessage = (event: { message: string; eventName: string }) => {
|
||||||
|
socket.value.sendMessage(event)
|
||||||
|
}
|
||||||
|
const addProtocol = () => {
|
||||||
addWSProtocol({ value: "", active: true })
|
addWSProtocol({ value: "", active: true })
|
||||||
},
|
}
|
||||||
deleteProtocol({ index }) {
|
const deleteProtocol = (index: number) => {
|
||||||
const oldProtocols = this.protocols.slice()
|
const oldProtocols = protocols.value.slice()
|
||||||
deleteWSProtocol(index)
|
deleteWSProtocol(index)
|
||||||
this.$toast.success(this.$t("state.deleted"), {
|
toast.success(`${t("state.deleted")}`, {
|
||||||
action: {
|
|
||||||
text: this.$t("action.undo"),
|
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
|
action: {
|
||||||
|
text: `${t("action.undo")}`,
|
||||||
onClick: (_, toastObject) => {
|
onClick: (_, toastObject) => {
|
||||||
this.protocols = oldProtocols
|
protocols.value = oldProtocols
|
||||||
toastObject.remove()
|
toastObject.goAway()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
updateProtocol(index, updated) {
|
const updateProtocol = (index: number, updated: HoppWSProtocol) => {
|
||||||
updateWSProtocol(index, updated)
|
updateWSProtocol(index, updated)
|
||||||
},
|
}
|
||||||
},
|
const clearLogEntries = () => {
|
||||||
})
|
log.value = []
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,7 +16,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span>
|
<span>
|
||||||
<ButtonPrimary v-focus :label="yes" @click.native="resolve" />
|
<ButtonPrimary
|
||||||
|
v-focus
|
||||||
|
:label="yes"
|
||||||
|
:loading="!!loadingState"
|
||||||
|
@click.native="resolve"
|
||||||
|
/>
|
||||||
<ButtonSecondary :label="no" @click.native="hideModal" />
|
<ButtonSecondary :label="no" @click.native="hideModal" />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -42,14 +47,15 @@ export default defineComponent({
|
|||||||
return this.$t("action.no")
|
return this.$t("action.no")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
loadingState: { type: Boolean, default: null },
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hideModal() {
|
hideModal() {
|
||||||
this.$emit("hide-modal")
|
this.$emit("hide-modal")
|
||||||
},
|
},
|
||||||
resolve() {
|
resolve() {
|
||||||
this.$emit("resolve")
|
this.$emit("resolve", this.title)
|
||||||
this.$emit("hide-modal")
|
if (this.loadingState === null) this.$emit("hide-modal")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ import {
|
|||||||
placeholder as placeholderExt,
|
placeholder as placeholderExt,
|
||||||
ViewPlugin,
|
ViewPlugin,
|
||||||
ViewUpdate,
|
ViewUpdate,
|
||||||
|
keymap,
|
||||||
} from "@codemirror/view"
|
} from "@codemirror/view"
|
||||||
import { EditorState, Extension } from "@codemirror/state"
|
import { EditorState, Extension } from "@codemirror/state"
|
||||||
import clone from "lodash/clone"
|
import clone from "lodash/clone"
|
||||||
import { tooltips } from "@codemirror/tooltip"
|
import { tooltips } from "@codemirror/tooltip"
|
||||||
|
import { history, historyKeymap } from "@codemirror/history"
|
||||||
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
||||||
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
||||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||||
@@ -45,6 +47,7 @@ const props = withDefaults(
|
|||||||
styles: string
|
styles: string
|
||||||
envs: { key: string; value: string; source: string }[] | null
|
envs: { key: string; value: string; source: string }[] | null
|
||||||
focus: boolean
|
focus: boolean
|
||||||
|
readonly: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
value: "",
|
value: "",
|
||||||
@@ -52,6 +55,7 @@ const props = withDefaults(
|
|||||||
styles: "",
|
styles: "",
|
||||||
envs: null,
|
envs: null,
|
||||||
focus: false,
|
focus: false,
|
||||||
|
readonly: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -120,7 +124,24 @@ const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
|||||||
|
|
||||||
const initView = (el: any) => {
|
const initView = (el: any) => {
|
||||||
const extensions: Extension = [
|
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,
|
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({
|
tooltips({
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
}),
|
}),
|
||||||
@@ -138,6 +159,8 @@ const initView = (el: any) => {
|
|||||||
ViewPlugin.fromClass(
|
ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
|
if (props.readonly) return
|
||||||
|
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
const prevValue = clone(cachedValue.value)
|
const prevValue = clone(cachedValue.value)
|
||||||
|
|
||||||
@@ -172,6 +195,8 @@ const initView = (el: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
history(),
|
||||||
|
keymap.of([...historyKeymap]),
|
||||||
]
|
]
|
||||||
|
|
||||||
view.value = new EditorView({
|
view.value = new EditorView({
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<SmartItem
|
<SmartItem
|
||||||
:label="label"
|
:label="label"
|
||||||
:icon="
|
:icon="selected ? 'radio_button_checked' : 'radio_button_unchecked'"
|
||||||
value === selected ? 'radio_button_checked' : 'radio_button_unchecked'
|
:active="selected"
|
||||||
"
|
|
||||||
:active="value === selected"
|
|
||||||
role="radio"
|
role="radio"
|
||||||
:aria-checked="value === selected"
|
:aria-checked="selected"
|
||||||
@click.native="$emit('change', value)"
|
@click.native="emit('change', value)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
const emit = defineEmits<{
|
||||||
|
(e: "change", value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
export default defineComponent({
|
defineProps({
|
||||||
props: {
|
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
@@ -25,9 +24,8 @@ export default defineComponent({
|
|||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
selected: {
|
selected: {
|
||||||
type: String,
|
type: Boolean,
|
||||||
default: "",
|
default: false,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,18 +5,22 @@
|
|||||||
:key="`radio-${index}`"
|
:key="`radio-${index}`"
|
||||||
:value="radio.value"
|
:value="radio.value"
|
||||||
:label="radio.label"
|
:label="radio.label"
|
||||||
:selected="selected"
|
:selected="value === radio.value"
|
||||||
@change="$emit('change', radio.value)"
|
@change="emit('input', radio.value)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "input", value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
radios: Array<{
|
radios: Array<{
|
||||||
value: string
|
value: string // The key of the radio option
|
||||||
label: string
|
label: string
|
||||||
}>
|
}>
|
||||||
selected: string
|
value: string // Should be a radio key given in the radios array
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export class GQLConnection {
|
|||||||
.forEach((x) => (finalHeaders[x.key] = x.value))
|
.forEach((x) => (finalHeaders[x.key] = x.value))
|
||||||
|
|
||||||
const reqOptions = {
|
const reqOptions = {
|
||||||
method: "post",
|
method: "POST",
|
||||||
url,
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
...finalHeaders,
|
...finalHeaders,
|
||||||
@@ -213,7 +213,7 @@ export class GQLConnection {
|
|||||||
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
||||||
|
|
||||||
const reqOptions = {
|
const reqOptions = {
|
||||||
method: "post",
|
method: "POST",
|
||||||
url,
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
...finalHeaders,
|
...finalHeaders,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mutation DeleteShortcode($code: ID!) {
|
||||||
|
revokeShortcode(code: $code)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
query GetUserShortcodes($cursor: ID) {
|
||||||
|
myShortcodes(cursor: $cursor) {
|
||||||
|
id
|
||||||
|
request
|
||||||
|
createdOn
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
subscription ShortcodeCreated {
|
||||||
|
myShortcodesCreated {
|
||||||
|
id
|
||||||
|
request
|
||||||
|
createdOn
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
subscription ShortcodeDeleted {
|
||||||
|
myShortcodesRevoked {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
GetCollectionTitleDocument,
|
GetCollectionTitleDocument,
|
||||||
} from "./graphql"
|
} from "./graphql"
|
||||||
|
|
||||||
const BACKEND_PAGE_SIZE = 10
|
export const BACKEND_PAGE_SIZE = 10
|
||||||
|
|
||||||
const getCollectionChildrenIDs = async (collID: string) => {
|
const getCollectionChildrenIDs = async (collID: string) => {
|
||||||
const collsList: string[] = []
|
const collsList: string[] = []
|
||||||
|
|||||||
@@ -4,8 +4,13 @@ import {
|
|||||||
CreateShortcodeDocument,
|
CreateShortcodeDocument,
|
||||||
CreateShortcodeMutation,
|
CreateShortcodeMutation,
|
||||||
CreateShortcodeMutationVariables,
|
CreateShortcodeMutationVariables,
|
||||||
|
DeleteShortcodeDocument,
|
||||||
|
DeleteShortcodeMutation,
|
||||||
|
DeleteShortcodeMutationVariables,
|
||||||
} from "../graphql"
|
} from "../graphql"
|
||||||
|
|
||||||
|
type DeleteShortcodeErrors = "shortcode/not_found"
|
||||||
|
|
||||||
export const createShortcode = (request: HoppRESTRequest) =>
|
export const createShortcode = (request: HoppRESTRequest) =>
|
||||||
runMutation<CreateShortcodeMutation, CreateShortcodeMutationVariables, "">(
|
runMutation<CreateShortcodeMutation, CreateShortcodeMutationVariables, "">(
|
||||||
CreateShortcodeDocument,
|
CreateShortcodeDocument,
|
||||||
@@ -13,3 +18,12 @@ export const createShortcode = (request: HoppRESTRequest) =>
|
|||||||
request: JSON.stringify(request),
|
request: JSON.stringify(request),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const deleteShortcode = (code: string) =>
|
||||||
|
runMutation<
|
||||||
|
DeleteShortcodeMutation,
|
||||||
|
DeleteShortcodeMutationVariables,
|
||||||
|
DeleteShortcodeErrors
|
||||||
|
>(DeleteShortcodeDocument, {
|
||||||
|
code,
|
||||||
|
})
|
||||||
|
|||||||
@@ -768,11 +768,52 @@ const samples = [
|
|||||||
testScript: "",
|
testScript: "",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
command: `curl \`
|
||||||
|
google.com -H "content-type: application/json"`,
|
||||||
|
response: makeRESTRequest({
|
||||||
|
method: "GET",
|
||||||
|
name: "Untitled request",
|
||||||
|
endpoint: "https://google.com/",
|
||||||
|
auth: {
|
||||||
|
authType: "none",
|
||||||
|
authActive: true,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
contentType: null,
|
||||||
|
body: null,
|
||||||
|
},
|
||||||
|
params: [],
|
||||||
|
headers: [],
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: `curl 192.168.0.24:8080/ping`,
|
||||||
|
response: makeRESTRequest({
|
||||||
|
method: "GET",
|
||||||
|
name: "Untitled request",
|
||||||
|
endpoint: "http://192.168.0.24:8080/ping",
|
||||||
|
auth: {
|
||||||
|
authType: "none",
|
||||||
|
authActive: true,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
contentType: null,
|
||||||
|
body: null,
|
||||||
|
},
|
||||||
|
params: [],
|
||||||
|
headers: [],
|
||||||
|
preRequestScript: "",
|
||||||
|
testScript: "",
|
||||||
|
}),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
describe("parseCurlToHoppRESTReq", () => {
|
describe("Parse curl command to Hopp REST Request", () => {
|
||||||
for (const [i, { command, response }] of samples.entries()) {
|
for (const [i, { command, response }] of samples.entries()) {
|
||||||
test(`matches expectation for sample #${i + 1}`, () => {
|
test(`for sample #${i + 1}:\n\n${command}`, () => {
|
||||||
expect(parseCurlToHoppRESTReq(command)).toEqual(response)
|
expect(parseCurlToHoppRESTReq(command)).toEqual(response)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getHeaders, recordToHoppHeaders } from "./sub_helpers/headers"
|
|||||||
// import { getCookies } from "./sub_helpers/cookies"
|
// import { getCookies } from "./sub_helpers/cookies"
|
||||||
import { getQueries } from "./sub_helpers/queries"
|
import { getQueries } from "./sub_helpers/queries"
|
||||||
import { getMethod } from "./sub_helpers/method"
|
import { getMethod } from "./sub_helpers/method"
|
||||||
import { concatParams, parseURL } from "./sub_helpers/url"
|
import { concatParams, getURLObject } from "./sub_helpers/url"
|
||||||
import { preProcessCurlCommand } from "./sub_helpers/preproc"
|
import { preProcessCurlCommand } from "./sub_helpers/preproc"
|
||||||
import { getBody, getFArgumentMultipartData } from "./sub_helpers/body"
|
import { getBody, getFArgumentMultipartData } from "./sub_helpers/body"
|
||||||
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
|
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
|
||||||
@@ -42,7 +42,7 @@ export const parseCurlCommand = (curlCommand: string) => {
|
|||||||
|
|
||||||
const method = getMethod(parsedArguments)
|
const method = getMethod(parsedArguments)
|
||||||
// const cookies = getCookies(parsedArguments)
|
// const cookies = getCookies(parsedArguments)
|
||||||
const urlObject = parseURL(parsedArguments)
|
const urlObject = getURLObject(parsedArguments)
|
||||||
const auth = getAuthObject(parsedArguments, headers, urlObject)
|
const auth = getAuthObject(parsedArguments, headers, urlObject)
|
||||||
|
|
||||||
let rawData: string | string[] = pipe(
|
let rawData: string | string[] = pipe(
|
||||||
|
|||||||
@@ -158,7 +158,9 @@ const getXMLBody = (rawData: string) =>
|
|||||||
O.alt(() => O.some(rawData))
|
O.alt(() => O.some(rawData))
|
||||||
)
|
)
|
||||||
|
|
||||||
const getFormattedJSON = flow(
|
const getFormattedJSON = (jsonString: string) =>
|
||||||
|
pipe(
|
||||||
|
jsonString.replaceAll('\\"', '"'),
|
||||||
safeParseJSON,
|
safeParseJSON,
|
||||||
O.map((parsedJSON) => JSON.stringify(parsedJSON, null, 2)),
|
O.map((parsedJSON) => JSON.stringify(parsedJSON, null, 2)),
|
||||||
O.getOrElse(() => "{ }"),
|
O.getOrElse(() => "{ }"),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const getMethodByDeduction = (parsedArguments: parser.Arguments) => {
|
|||||||
R.or(objHasArrayProperty("F", "string"))
|
R.or(objHasArrayProperty("F", "string"))
|
||||||
)(parsedArguments)
|
)(parsedArguments)
|
||||||
)
|
)
|
||||||
return O.some("post")
|
return O.some("POST")
|
||||||
else return O.none
|
else return O.none
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ const paperCuts = flow(
|
|||||||
S.replace(/\n/g, " "),
|
S.replace(/\n/g, " "),
|
||||||
// remove all $ symbols from start of argument values
|
// remove all $ symbols from start of argument values
|
||||||
S.replace(/\$'/g, "'"),
|
S.replace(/\$'/g, "'"),
|
||||||
S.replace(/\$"/g, '"')
|
S.replace(/\$"/g, '"'),
|
||||||
|
S.trim
|
||||||
)
|
)
|
||||||
|
|
||||||
// replace --zargs option with -Z
|
// replace --zargs option with -Z
|
||||||
|
|||||||
@@ -1,48 +1,80 @@
|
|||||||
import parser from "yargs-parser"
|
import parser from "yargs-parser"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
|
import * as A from "fp-ts/Array"
|
||||||
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
|
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
|
||||||
import { stringArrayJoin } from "~/helpers/functional/array"
|
import { stringArrayJoin } from "~/helpers/functional/array"
|
||||||
|
|
||||||
const defaultRESTReq = getDefaultRESTRequest()
|
const defaultRESTReq = getDefaultRESTRequest()
|
||||||
|
|
||||||
const getProtocolForBaseURL = (baseURL: string) =>
|
const getProtocolFromURL = (url: string) =>
|
||||||
pipe(
|
pipe(
|
||||||
// get the base URL
|
// get the base URL
|
||||||
/^([^\s:@]+:[^\s:@]+@)?([^:/\s]+)([:]*)/.exec(baseURL),
|
/^([^\s:@]+:[^\s:@]+@)?([^:/\s]+)([:]*)/.exec(url),
|
||||||
O.fromNullable,
|
O.fromNullable,
|
||||||
O.filter((burl) => burl.length > 1),
|
O.filter((burl) => burl.length > 1),
|
||||||
O.map((burl) => burl[2]),
|
O.map((burl) => burl[2]),
|
||||||
// set protocol to http for local URLs
|
// set protocol to http for local URLs
|
||||||
O.map((burl) =>
|
O.map((burl) =>
|
||||||
burl === "localhost" || burl === "127.0.0.1"
|
burl === "localhost" ||
|
||||||
? "http://" + baseURL
|
burl === "2130706433" ||
|
||||||
: "https://" + baseURL
|
/127(\.0){0,2}\.1/.test(burl) ||
|
||||||
|
/0177(\.0){0,2}\.1/.test(burl) ||
|
||||||
|
/0x7f(\.0){0,2}\.1/.test(burl) ||
|
||||||
|
/192\.168(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){2}/.test(burl) ||
|
||||||
|
/10(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/.test(burl)
|
||||||
|
? "http://" + url
|
||||||
|
: "https://" + url
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the URL is valid using the URL constructor
|
||||||
|
* @param urlString URL string (with protocol)
|
||||||
|
* @returns boolean whether the URL is valid using the inbuilt URL class
|
||||||
|
*/
|
||||||
|
const isURLValid = (urlString: string) =>
|
||||||
|
pipe(
|
||||||
|
O.tryCatch(() => new URL(urlString)),
|
||||||
|
O.isSome
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks and returns URL object for the valid URL
|
||||||
|
* @param urlText Raw URL string provided by argument parser
|
||||||
|
* @returns Option of URL object
|
||||||
|
*/
|
||||||
|
const parseURL = (urlText: string | number) =>
|
||||||
|
pipe(
|
||||||
|
urlText,
|
||||||
|
O.fromNullable,
|
||||||
|
// preprocess url string
|
||||||
|
O.map((u) => u.toString().replaceAll(/[^a-zA-Z0-9_\-./?&=:@%+#,;\s]/g, "")),
|
||||||
|
O.filter((u) => u.length > 0),
|
||||||
|
O.chain((u) =>
|
||||||
|
pipe(
|
||||||
|
u,
|
||||||
|
// check if protocol is available
|
||||||
|
O.fromPredicate(
|
||||||
|
(url: string) => /^[^:\s]+(?=:\/\/)/.exec(url) !== null
|
||||||
|
),
|
||||||
|
O.alt(() => getProtocolFromURL(u))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
O.filter(isURLValid),
|
||||||
|
O.map((u) => new URL(u))
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes URL string and returns the URL object
|
* Processes URL string and returns the URL object
|
||||||
* @param parsedArguments Parsed Arguments object
|
* @param parsedArguments Parsed Arguments object
|
||||||
* @returns URL object
|
* @returns URL object
|
||||||
*/
|
*/
|
||||||
export function parseURL(parsedArguments: parser.Arguments) {
|
export function getURLObject(parsedArguments: parser.Arguments) {
|
||||||
return pipe(
|
return pipe(
|
||||||
// contains raw url string
|
// contains raw url strings
|
||||||
parsedArguments._[1],
|
parsedArguments._.slice(1),
|
||||||
O.fromNullable,
|
A.findFirstMap(parseURL),
|
||||||
// preprocess url string
|
|
||||||
O.map((u) => u.toString().replace(/["']/g, "").trim()),
|
|
||||||
O.chain((u) =>
|
|
||||||
pipe(
|
|
||||||
// check if protocol is available
|
|
||||||
/^[^:\s]+(?=:\/\/)/.exec(u),
|
|
||||||
O.fromNullable,
|
|
||||||
O.map((_) => u),
|
|
||||||
O.alt(() => getProtocolForBaseURL(u))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
O.map((u) => new URL(u)),
|
|
||||||
// no url found
|
// no url found
|
||||||
O.getOrElse(() => new URL(defaultRESTReq.endpoint))
|
O.getOrElse(() => new URL(defaultRESTReq.endpoint))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import { javascriptLanguage } from "@codemirror/lang-javascript"
|
|||||||
import { xmlLanguage } from "@codemirror/lang-xml"
|
import { xmlLanguage } from "@codemirror/lang-xml"
|
||||||
import { jsonLanguage } from "@codemirror/lang-json"
|
import { jsonLanguage } from "@codemirror/lang-json"
|
||||||
import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql"
|
import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql"
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import * as O from "fp-ts/Option"
|
|
||||||
import { StreamLanguage } from "@codemirror/stream-parser"
|
import { StreamLanguage } from "@codemirror/stream-parser"
|
||||||
import { html } from "@codemirror/legacy-modes/mode/xml"
|
import { html } from "@codemirror/legacy-modes/mode/xml"
|
||||||
import { shell } from "@codemirror/legacy-modes/mode/shell"
|
import { shell } from "@codemirror/legacy-modes/mode/shell"
|
||||||
@@ -96,8 +94,10 @@ const hoppCompleterExt = (completer: Completer): Extension => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoppLinterExt = (hoppLinter: LinterDefinition): Extension => {
|
const hoppLinterExt = (hoppLinter: LinterDefinition | undefined): Extension => {
|
||||||
return linter(async (view) => {
|
return linter(async (view) => {
|
||||||
|
if (!hoppLinter) return []
|
||||||
|
|
||||||
// Requires full document scan, hence expensive on big files, force disable on big files ?
|
// Requires full document scan, hence expensive on big files, force disable on big files ?
|
||||||
const linterResult = await hoppLinter(
|
const linterResult = await hoppLinter(
|
||||||
view.state.doc.toJSON().join(view.state.lineBreak)
|
view.state.doc.toJSON().join(view.state.lineBreak)
|
||||||
@@ -119,16 +119,16 @@ const hoppLinterExt = (hoppLinter: LinterDefinition): Extension => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hoppLang = (
|
const hoppLang = (
|
||||||
language: Language,
|
language: Language | undefined,
|
||||||
linter?: LinterDefinition | undefined,
|
linter?: LinterDefinition | undefined,
|
||||||
completer?: Completer | undefined
|
completer?: Completer | undefined
|
||||||
) => {
|
): Extension | LanguageSupport => {
|
||||||
const exts: Extension[] = []
|
const exts: Extension[] = []
|
||||||
|
|
||||||
if (linter) exts.push(hoppLinterExt(linter))
|
exts.push(hoppLinterExt(linter))
|
||||||
if (completer) exts.push(hoppCompleterExt(completer))
|
if (completer) exts.push(hoppCompleterExt(completer))
|
||||||
|
|
||||||
return new LanguageSupport(language, exts)
|
return language ? new LanguageSupport(language, exts) : exts
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLanguage = (langMime: string): Language | null => {
|
const getLanguage = (langMime: string): Language | null => {
|
||||||
@@ -156,12 +156,7 @@ const getEditorLanguage = (
|
|||||||
langMime: string,
|
langMime: string,
|
||||||
linter: LinterDefinition | undefined,
|
linter: LinterDefinition | undefined,
|
||||||
completer: Completer | undefined
|
completer: Completer | undefined
|
||||||
): Extension =>
|
): Extension => hoppLang(getLanguage(langMime) ?? undefined, linter, completer)
|
||||||
pipe(
|
|
||||||
O.fromNullable(getLanguage(langMime)),
|
|
||||||
O.map((lang) => hoppLang(lang, linter, completer)),
|
|
||||||
O.getOrElseW(() => [])
|
|
||||||
)
|
|
||||||
|
|
||||||
export function useCodemirror(
|
export function useCodemirror(
|
||||||
el: Ref<any | null>,
|
el: Ref<any | null>,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
|
import { flow } from "fp-ts/function"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks and Parses JSON string
|
* Checks and Parses JSON string
|
||||||
@@ -7,3 +8,10 @@ import * as O from "fp-ts/Option"
|
|||||||
*/
|
*/
|
||||||
export const safeParseJSON = (str: string): O.Option<object> =>
|
export const safeParseJSON = (str: string): O.Option<object> =>
|
||||||
O.tryCatch(() => JSON.parse(str))
|
O.tryCatch(() => JSON.parse(str))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given string is a JSON string
|
||||||
|
* @param str Raw string to be checked
|
||||||
|
* @returns If string is a JSON string
|
||||||
|
*/
|
||||||
|
export const isJSON = flow(safeParseJSON, O.isSome)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import cloneDeep from "lodash/cloneDeep"
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual"
|
||||||
|
import { JSPrimitive, TypeFromPrimitive } from "./primtive"
|
||||||
|
|
||||||
export const objRemoveKey =
|
export const objRemoveKey =
|
||||||
<T, K extends keyof T>(key: K) =>
|
<T, K extends keyof T>(key: K) =>
|
||||||
@@ -19,34 +20,15 @@ export const objFieldMatches =
|
|||||||
(obj: T): obj is T & { [_ in K]: V } =>
|
(obj: T): obj is T & { [_ in K]: V } =>
|
||||||
matches.findIndex((x) => isEqual(obj[fieldName], x)) !== -1
|
matches.findIndex((x) => isEqual(obj[fieldName], x)) !== -1
|
||||||
|
|
||||||
type JSPrimitive =
|
export const objHasProperty =
|
||||||
| "undefined"
|
<O extends object, K extends string, P extends JSPrimitive | undefined>(
|
||||||
| "object"
|
prop: K,
|
||||||
| "boolean"
|
type?: P
|
||||||
| "number"
|
) =>
|
||||||
| "bigint"
|
// eslint-disable-next-line
|
||||||
| "string"
|
(obj: O): obj is O & { [_ in K]: TypeFromPrimitive<P> } =>
|
||||||
| "symbol"
|
// eslint-disable-next-line
|
||||||
| "function"
|
prop in obj && (type === undefined || typeof (obj as any)[prop] === type)
|
||||||
|
|
||||||
type TypeFromPrimitive<P extends JSPrimitive | undefined> =
|
|
||||||
P extends "undefined"
|
|
||||||
? undefined
|
|
||||||
: P extends "object"
|
|
||||||
? object | null // typeof null === "object"
|
|
||||||
: P extends "boolean"
|
|
||||||
? boolean
|
|
||||||
: P extends "number"
|
|
||||||
? number
|
|
||||||
: P extends "bigint"
|
|
||||||
? BigInt
|
|
||||||
: P extends "string"
|
|
||||||
? string
|
|
||||||
: P extends "symbol"
|
|
||||||
? Symbol
|
|
||||||
: P extends "function"
|
|
||||||
? Function
|
|
||||||
: unknown
|
|
||||||
|
|
||||||
type TypeFromPrimitiveArray<P extends JSPrimitive | undefined> =
|
type TypeFromPrimitiveArray<P extends JSPrimitive | undefined> =
|
||||||
P extends "undefined"
|
P extends "undefined"
|
||||||
@@ -67,16 +49,6 @@ type TypeFromPrimitiveArray<P extends JSPrimitive | undefined> =
|
|||||||
? Function[]
|
? Function[]
|
||||||
: unknown[]
|
: unknown[]
|
||||||
|
|
||||||
export const objHasProperty =
|
|
||||||
<O extends object, K extends string, P extends JSPrimitive | undefined>(
|
|
||||||
prop: K,
|
|
||||||
type?: P
|
|
||||||
) =>
|
|
||||||
// eslint-disable-next-line
|
|
||||||
(obj: O): obj is O & { [_ in K]: TypeFromPrimitive<P> } =>
|
|
||||||
// eslint-disable-next-line
|
|
||||||
prop in obj && (type === undefined || typeof (obj as any)[prop] === type)
|
|
||||||
|
|
||||||
export const objHasArrayProperty =
|
export const objHasArrayProperty =
|
||||||
<O extends object, K extends string, P extends JSPrimitive>(
|
<O extends object, K extends string, P extends JSPrimitive>(
|
||||||
prop: K,
|
prop: K,
|
||||||
|
|||||||
34
packages/hoppscotch-app/helpers/functional/primtive.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export type JSPrimitive =
|
||||||
|
| "undefined"
|
||||||
|
| "object"
|
||||||
|
| "boolean"
|
||||||
|
| "number"
|
||||||
|
| "bigint"
|
||||||
|
| "string"
|
||||||
|
| "symbol"
|
||||||
|
| "function"
|
||||||
|
|
||||||
|
export type TypeFromPrimitive<P extends JSPrimitive | undefined> =
|
||||||
|
P extends "undefined"
|
||||||
|
? undefined
|
||||||
|
: P extends "object"
|
||||||
|
? object | null // typeof null === "object"
|
||||||
|
: P extends "boolean"
|
||||||
|
? boolean
|
||||||
|
: P extends "number"
|
||||||
|
? number
|
||||||
|
: P extends "bigint"
|
||||||
|
? BigInt
|
||||||
|
: P extends "string"
|
||||||
|
? string
|
||||||
|
: P extends "symbol"
|
||||||
|
? Symbol
|
||||||
|
: P extends "function"
|
||||||
|
? Function
|
||||||
|
: unknown
|
||||||
|
|
||||||
|
export const isOfType =
|
||||||
|
<T extends JSPrimitive>(type: T) =>
|
||||||
|
(value: unknown): value is T =>
|
||||||
|
// eslint-disable-next-line valid-typeof
|
||||||
|
typeof value === type
|
||||||
33
packages/hoppscotch-app/helpers/functional/regex.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Escapes special regex characters in a string.
|
||||||
|
* @param text The string to transform
|
||||||
|
* @returns Escaped string
|
||||||
|
*/
|
||||||
|
export const regexEscape = (text: string) =>
|
||||||
|
text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")
|
||||||
|
|
||||||
|
export type RegexMatch = {
|
||||||
|
matchString: string
|
||||||
|
startIndex: number
|
||||||
|
endIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the regex match ranges for a given input
|
||||||
|
* @param regex The Regular Expression to find from
|
||||||
|
* @param input The input string to get match ranges from
|
||||||
|
* @returns An array of `RegexMatch` objects giving info about the matches
|
||||||
|
*/
|
||||||
|
export const regexFindAllMatches = (regex: RegExp) => (input: string) => {
|
||||||
|
const matches: RegexMatch[] = []
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-cond-assign, prettier/prettier
|
||||||
|
for (let match; match = regex.exec(input); match !== null)
|
||||||
|
matches.push({
|
||||||
|
matchString: match[0],
|
||||||
|
startIndex: match.index,
|
||||||
|
endIndex: match.index + match[0].length - 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
13
packages/hoppscotch-app/helpers/functional/taskEither.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility type which gives you the type of the left value of a TaskEither
|
||||||
|
*/
|
||||||
|
export type TELeftType<T extends TE.TaskEither<any, any>> =
|
||||||
|
T extends TE.TaskEither<
|
||||||
|
infer U,
|
||||||
|
// eslint-disable-next-line
|
||||||
|
infer _
|
||||||
|
>
|
||||||
|
? U
|
||||||
|
: never
|
||||||
@@ -10,7 +10,7 @@ import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
|||||||
// TODO: Add validation to output
|
// TODO: Add validation to output
|
||||||
const fetchGist = (
|
const fetchGist = (
|
||||||
url: string
|
url: string
|
||||||
): TO.TaskOption<HoppCollection<HoppRESTRequest>> =>
|
): TO.TaskOption<HoppCollection<HoppRESTRequest>[]> =>
|
||||||
pipe(
|
pipe(
|
||||||
TO.tryCatch(() =>
|
TO.tryCatch(() =>
|
||||||
axios.get(`https://api.github.com/gists/${url.split("/").pop()}`, {
|
axios.get(`https://api.github.com/gists/${url.split("/").pop()}`, {
|
||||||
@@ -30,8 +30,10 @@ const fetchGist = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
export default defineImporter({
|
export default defineImporter({
|
||||||
|
id: "gist",
|
||||||
name: "import.from_gist",
|
name: "import.from_gist",
|
||||||
icon: "github",
|
icon: "github",
|
||||||
|
applicableTo: ["my-collections", "team-collections"],
|
||||||
steps: [
|
steps: [
|
||||||
step({
|
step({
|
||||||
stepName: "URL_IMPORT",
|
stepName: "URL_IMPORT",
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import { pipe } from "fp-ts/function"
|
import { pipe, flow } from "fp-ts/function"
|
||||||
import * as TE from "fp-ts/TaskEither"
|
import * as TE from "fp-ts/TaskEither"
|
||||||
import * as E from "fp-ts/Either"
|
import * as O from "fp-ts/Option"
|
||||||
import { translateToNewRESTCollection } from "@hoppscotch/data"
|
import * as RA from "fp-ts/ReadonlyArray"
|
||||||
|
import {
|
||||||
|
translateToNewRESTCollection,
|
||||||
|
HoppCollection,
|
||||||
|
HoppRESTRequest,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
import _isPlainObject from "lodash/isPlainObject"
|
||||||
import { step } from "../steps"
|
import { step } from "../steps"
|
||||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||||
|
import { safeParseJSON } from "~/helpers/functional/json"
|
||||||
|
|
||||||
export default defineImporter({
|
export default defineImporter({
|
||||||
|
id: "hoppscotch",
|
||||||
name: "import.from_json",
|
name: "import.from_json",
|
||||||
icon: "folder-plus",
|
icon: "folder-plus",
|
||||||
|
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||||
steps: [
|
steps: [
|
||||||
step({
|
step({
|
||||||
stepName: "FILE_IMPORT",
|
stepName: "FILE_IMPORT",
|
||||||
@@ -19,16 +28,40 @@ export default defineImporter({
|
|||||||
] as const,
|
] as const,
|
||||||
importer: ([content]) =>
|
importer: ([content]) =>
|
||||||
pipe(
|
pipe(
|
||||||
E.tryCatch(
|
safeParseJSON(content),
|
||||||
() => {
|
O.chain(
|
||||||
const x = JSON.parse(content)
|
flow(
|
||||||
|
makeCollectionsArray,
|
||||||
return Array.isArray(x)
|
RA.map(
|
||||||
? x.map((coll: any) => translateToNewRESTCollection(coll))
|
flow(
|
||||||
: [translateToNewRESTCollection(x)]
|
O.fromPredicate(isValidCollection),
|
||||||
},
|
O.map(translateToNewRESTCollection)
|
||||||
() => IMPORTER_INVALID_FILE_FORMAT
|
)
|
||||||
),
|
),
|
||||||
TE.fromEither
|
O.sequenceArray,
|
||||||
|
O.map(RA.toArray)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if a value is a plain object
|
||||||
|
*/
|
||||||
|
const isPlainObject = (value: any): value is object => _isPlainObject(value)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if a collection matches the schema for a hoppscotch collection.
|
||||||
|
* as of now we are only checking if the collection has a "v" key in it.
|
||||||
|
*/
|
||||||
|
const isValidCollection = (
|
||||||
|
collection: unknown
|
||||||
|
): collection is HoppCollection<HoppRESTRequest> =>
|
||||||
|
isPlainObject(collection) && "v" in collection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert single collection object into an array so it can be handled the same as multiple collections
|
||||||
|
*/
|
||||||
|
const makeCollectionsArray = (collections: unknown | unknown[]): unknown[] =>
|
||||||
|
Array.isArray(collections) ? collections : [collections]
|
||||||
|
|||||||
@@ -13,3 +13,10 @@ export const RESTCollectionImporters = [
|
|||||||
GistImporter,
|
GistImporter,
|
||||||
MyCollectionsImporter,
|
MyCollectionsImporter,
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
export const URLImporters = [
|
||||||
|
HoppRESTCollImporter,
|
||||||
|
OpenAPIImporter,
|
||||||
|
PostmanImporter,
|
||||||
|
InsomniaImporter,
|
||||||
|
] as const
|
||||||
|
|||||||
@@ -13,10 +13,18 @@ type HoppImporter<T, StepsType, Errors> = (
|
|||||||
stepValues: StepsOutputList<StepsType>
|
stepValues: StepsOutputList<StepsType>
|
||||||
) => TE.TaskEither<Errors, T>
|
) => TE.TaskEither<Errors, T>
|
||||||
|
|
||||||
|
type HoppImporterApplicableTo = Array<
|
||||||
|
"team-collections" | "my-collections" | "url-import"
|
||||||
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Definition for importers
|
* Definition for importers
|
||||||
*/
|
*/
|
||||||
type HoppImporterDefinition<T, Y, E> = {
|
type HoppImporterDefinition<T, Y, E> = {
|
||||||
|
/**
|
||||||
|
* the id
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
/**
|
/**
|
||||||
* Name of the importer, shown on the Select Importer dropdown
|
* Name of the importer, shown on the Select Importer dropdown
|
||||||
*/
|
*/
|
||||||
@@ -30,7 +38,7 @@ type HoppImporterDefinition<T, Y, E> = {
|
|||||||
/**
|
/**
|
||||||
* Identifier for the importer
|
* Identifier for the importer
|
||||||
*/
|
*/
|
||||||
applicableTo?: Array<"team-collections" | "my-collections">
|
applicableTo: HoppImporterApplicableTo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The importer function, It is a Promise because its supposed to be loaded in lazily (dynamic imports ?)
|
* The importer function, It is a Promise because its supposed to be loaded in lazily (dynamic imports ?)
|
||||||
@@ -47,10 +55,11 @@ type HoppImporterDefinition<T, Y, E> = {
|
|||||||
* Defines a Hoppscotch importer
|
* Defines a Hoppscotch importer
|
||||||
*/
|
*/
|
||||||
export const defineImporter = <ReturnType, StepType, Errors>(input: {
|
export const defineImporter = <ReturnType, StepType, Errors>(input: {
|
||||||
|
id: string
|
||||||
name: string
|
name: string
|
||||||
icon: string
|
icon: string
|
||||||
importer: HoppImporter<ReturnType, StepType, Errors>
|
importer: HoppImporter<ReturnType, StepType, Errors>
|
||||||
applicableTo?: Array<"team-collections" | "my-collections">
|
applicableTo: HoppImporterApplicableTo
|
||||||
steps: StepType
|
steps: StepType
|
||||||
}) => {
|
}) => {
|
||||||
return <HoppImporterDefinition<ReturnType, StepType, Errors>>{
|
return <HoppImporterDefinition<ReturnType, StepType, Errors>>{
|
||||||
|
|||||||
@@ -210,7 +210,9 @@ const getHoppCollections = (doc: InsomniaDoc) =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
export default defineImporter({
|
export default defineImporter({
|
||||||
|
id: "insomnia",
|
||||||
name: "import.from_insomnia",
|
name: "import.from_insomnia",
|
||||||
|
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||||
icon: "insomnia",
|
icon: "insomnia",
|
||||||
steps: [
|
steps: [
|
||||||
step({
|
step({
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { defineImporter } from "."
|
|||||||
import { getRESTCollection } from "~/newstore/collections"
|
import { getRESTCollection } from "~/newstore/collections"
|
||||||
|
|
||||||
export default defineImporter({
|
export default defineImporter({
|
||||||
|
id: "myCollections",
|
||||||
name: "import.from_my_collections",
|
name: "import.from_my_collections",
|
||||||
icon: "user",
|
icon: "user",
|
||||||
applicableTo: ["team-collections"],
|
applicableTo: ["team-collections"],
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import * as RA from "fp-ts/ReadonlyArray"
|
|||||||
import { step } from "../steps"
|
import { step } from "../steps"
|
||||||
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||||
|
|
||||||
const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const
|
export const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const
|
||||||
|
|
||||||
// TODO: URL Import Support
|
// TODO: URL Import Support
|
||||||
|
|
||||||
@@ -586,7 +586,9 @@ const parseOpenAPIDocContent = (str: string) =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
export default defineImporter({
|
export default defineImporter({
|
||||||
|
id: "openapi",
|
||||||
name: "import.from_openapi",
|
name: "import.from_openapi",
|
||||||
|
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||||
icon: "file",
|
icon: "file",
|
||||||
steps: [
|
steps: [
|
||||||
step({
|
step({
|
||||||
@@ -603,7 +605,6 @@ export default defineImporter({
|
|||||||
fileContent,
|
fileContent,
|
||||||
parseOpenAPIDocContent,
|
parseOpenAPIDocContent,
|
||||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT),
|
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT),
|
||||||
|
|
||||||
// Try validating, else the importer is invalid file format
|
// Try validating, else the importer is invalid file format
|
||||||
TE.chainW((obj) =>
|
TE.chainW((obj) =>
|
||||||
pipe(
|
pipe(
|
||||||
@@ -613,7 +614,6 @@ export default defineImporter({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
// Deference the references
|
// Deference the references
|
||||||
TE.chainW((obj) =>
|
TE.chainW((obj) =>
|
||||||
pipe(
|
pipe(
|
||||||
@@ -623,7 +623,6 @@ export default defineImporter({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
TE.chainW(convertOpenApiDocToHopp)
|
TE.chainW(convertOpenApiDocToHopp)
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -297,7 +297,9 @@ const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection<HoppRESTRequest> =>
|
|||||||
export const getHoppCollection = (coll: PMCollection) => getHoppFolder(coll)
|
export const getHoppCollection = (coll: PMCollection) => getHoppFolder(coll)
|
||||||
|
|
||||||
export default defineImporter({
|
export default defineImporter({
|
||||||
|
id: "postman",
|
||||||
name: "import.from_postman",
|
name: "import.from_postman",
|
||||||
|
applicableTo: ["my-collections", "team-collections", "url-import"],
|
||||||
icon: "postman",
|
icon: "postman",
|
||||||
steps: [
|
steps: [
|
||||||
step({
|
step({
|
||||||
|
|||||||
@@ -132,13 +132,13 @@ export function createRESTNetworkRequestStream(
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Assembling params object
|
// Assembling params object
|
||||||
TE.bind("params", ({ req }) =>
|
TE.bind("params", ({ req }) => {
|
||||||
TE.of(
|
const params = new URLSearchParams()
|
||||||
req.effectiveFinalParams.reduce((acc, { key, value }) => {
|
req.effectiveFinalParams.forEach((x) => {
|
||||||
return Object.assign(acc, { [key]: value })
|
params.append(x.key, x.value)
|
||||||
}, {})
|
})
|
||||||
)
|
return TE.of(params)
|
||||||
),
|
}),
|
||||||
|
|
||||||
// Keeping the backup start time
|
// Keeping the backup start time
|
||||||
TE.bind("backupTimeStart", () => TE.of(Date.now())),
|
TE.bind("backupTimeStart", () => TE.of(Date.now())),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const sendPostRequest = async (url, params) => {
|
|||||||
.map((key) => `${key}=${params[key]}`)
|
.map((key) => `${key}=${params[key]}`)
|
||||||
.join("&")
|
.join("&")
|
||||||
const options = {
|
const options = {
|
||||||
method: "post",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
},
|
},
|
||||||
|
|||||||
223
packages/hoppscotch-app/helpers/realtime/MQTTConnection.ts
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import Paho, { ConnectionOptions } from "paho-mqtt"
|
||||||
|
import { BehaviorSubject, Subject } from "rxjs"
|
||||||
|
import { logHoppRequestRunToAnalytics } from "../fb/analytics"
|
||||||
|
|
||||||
|
export type MQTTMessage = { topic: string; message: string }
|
||||||
|
export type MQTTError =
|
||||||
|
| { type: "CONNECTION_NOT_ESTABLISHED"; value: unknown }
|
||||||
|
| { type: "CONNECTION_LOST" }
|
||||||
|
| { type: "CONNECTION_FAILED" }
|
||||||
|
| { type: "SUBSCRIPTION_FAILED"; topic: string }
|
||||||
|
| { type: "PUBLISH_ERROR"; topic: string; message: string }
|
||||||
|
|
||||||
|
export type MQTTEvent = { time: number } & (
|
||||||
|
| { type: "CONNECTING" }
|
||||||
|
| { type: "CONNECTED" }
|
||||||
|
| { type: "MESSAGE_SENT"; message: MQTTMessage }
|
||||||
|
| { type: "SUBSCRIBED"; topic: string }
|
||||||
|
| { type: "SUBSCRIPTION_FAILED"; topic: string }
|
||||||
|
| { type: "MESSAGE_RECEIVED"; message: MQTTMessage }
|
||||||
|
| { type: "DISCONNECTED"; manual: boolean }
|
||||||
|
| { type: "ERROR"; error: MQTTError }
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
||||||
|
|
||||||
|
export class MQTTConnection {
|
||||||
|
subscriptionState$ = new BehaviorSubject<boolean>(false)
|
||||||
|
connectionState$ = new BehaviorSubject<ConnectionState>("DISCONNECTED")
|
||||||
|
event$: Subject<MQTTEvent> = new Subject()
|
||||||
|
|
||||||
|
private mqttClient: Paho.Client | undefined
|
||||||
|
private manualDisconnect = false
|
||||||
|
|
||||||
|
private addEvent(event: MQTTEvent) {
|
||||||
|
this.event$.next(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(url: string, username: string, password: string) {
|
||||||
|
try {
|
||||||
|
this.connectionState$.next("CONNECTING")
|
||||||
|
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "CONNECTING",
|
||||||
|
})
|
||||||
|
|
||||||
|
const parseUrl = new URL(url)
|
||||||
|
const { hostname, pathname, port } = parseUrl
|
||||||
|
this.mqttClient = new Paho.Client(
|
||||||
|
`${hostname + (pathname !== "/" ? pathname : "")}`,
|
||||||
|
port !== "" ? Number(port) : 8081,
|
||||||
|
"hoppscotch"
|
||||||
|
)
|
||||||
|
const connectOptions: ConnectionOptions = {
|
||||||
|
onSuccess: this.onConnectionSuccess.bind(this),
|
||||||
|
onFailure: this.onConnectionFailure.bind(this),
|
||||||
|
useSSL: parseUrl.protocol !== "ws:",
|
||||||
|
}
|
||||||
|
if (username !== "") {
|
||||||
|
connectOptions.userName = username
|
||||||
|
}
|
||||||
|
if (password !== "") {
|
||||||
|
connectOptions.password = password
|
||||||
|
}
|
||||||
|
this.mqttClient.connect(connectOptions)
|
||||||
|
this.mqttClient.onConnectionLost = this.onConnectionLost.bind(this)
|
||||||
|
this.mqttClient.onMessageArrived = this.onMessageArrived.bind(this)
|
||||||
|
} catch (e) {
|
||||||
|
this.handleError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
logHoppRequestRunToAnalytics({
|
||||||
|
platform: "mqtt",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnectionFailure() {
|
||||||
|
this.connectionState$.next("DISCONNECTED")
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "ERROR",
|
||||||
|
error: {
|
||||||
|
type: "CONNECTION_FAILED",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnectionSuccess() {
|
||||||
|
this.connectionState$.next("CONNECTED")
|
||||||
|
this.addEvent({
|
||||||
|
type: "CONNECTED",
|
||||||
|
time: Date.now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnectionLost() {
|
||||||
|
this.connectionState$.next("DISCONNECTED")
|
||||||
|
if (this.manualDisconnect) {
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "DISCONNECTED",
|
||||||
|
manual: this.manualDisconnect,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "ERROR",
|
||||||
|
error: {
|
||||||
|
type: "CONNECTION_LOST",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.manualDisconnect = false
|
||||||
|
this.subscriptionState$.next(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessageArrived({
|
||||||
|
payloadString: message,
|
||||||
|
destinationName: topic,
|
||||||
|
}: {
|
||||||
|
payloadString: string
|
||||||
|
destinationName: string
|
||||||
|
}) {
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "MESSAGE_RECEIVED",
|
||||||
|
message: {
|
||||||
|
topic,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: unknown) {
|
||||||
|
this.disconnect()
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "ERROR",
|
||||||
|
error: {
|
||||||
|
type: "CONNECTION_NOT_ESTABLISHED",
|
||||||
|
value: error,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
publish(topic: string, message: string) {
|
||||||
|
if (this.connectionState$.value === "DISCONNECTED") return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// it was publish
|
||||||
|
this.mqttClient?.send(topic, message, 0, false)
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "MESSAGE_SENT",
|
||||||
|
message: {
|
||||||
|
topic,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "ERROR",
|
||||||
|
error: {
|
||||||
|
type: "PUBLISH_ERROR",
|
||||||
|
topic,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(topic: string) {
|
||||||
|
try {
|
||||||
|
this.mqttClient?.subscribe(topic, {
|
||||||
|
onSuccess: this.usubSuccess.bind(this, topic),
|
||||||
|
onFailure: this.usubFailure.bind(this, topic),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "ERROR",
|
||||||
|
error: {
|
||||||
|
type: "SUBSCRIPTION_FAILED",
|
||||||
|
topic,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usubSuccess(topic: string) {
|
||||||
|
this.subscriptionState$.next(!this.subscriptionState$.value)
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "SUBSCRIBED",
|
||||||
|
topic,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
usubFailure(topic: string) {
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "ERROR",
|
||||||
|
error: {
|
||||||
|
type: "SUBSCRIPTION_FAILED",
|
||||||
|
topic,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(topic: string) {
|
||||||
|
this.mqttClient?.unsubscribe(topic, {
|
||||||
|
onSuccess: this.usubSuccess.bind(this, topic),
|
||||||
|
onFailure: this.usubFailure.bind(this, topic),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.manualDisconnect = true
|
||||||
|
this.mqttClient?.disconnect()
|
||||||
|
this.connectionState$.next("DISCONNECTED")
|
||||||
|
}
|
||||||
|
}
|
||||||
84
packages/hoppscotch-app/helpers/realtime/SIOClients.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import wildcard from "socketio-wildcard"
|
||||||
|
import ClientV2 from "socket.io-client-v2"
|
||||||
|
import { io as ClientV4, Socket as SocketV4 } from "socket.io-client-v4"
|
||||||
|
import { io as ClientV3, Socket as SocketV3 } from "socket.io-client-v3"
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
path: string
|
||||||
|
auth: {
|
||||||
|
token: string | undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PossibleEvent =
|
||||||
|
| "connect"
|
||||||
|
| "connect_error"
|
||||||
|
| "reconnect_error"
|
||||||
|
| "error"
|
||||||
|
| "disconnect"
|
||||||
|
| "*"
|
||||||
|
|
||||||
|
export interface SIOClient {
|
||||||
|
connect(url: string, opts?: Options): void
|
||||||
|
on(event: PossibleEvent, cb: (data: any) => void): void
|
||||||
|
emit(event: string, data: any, cb: (data: any) => void): void
|
||||||
|
close(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SIOClientV4 implements SIOClient {
|
||||||
|
private client: SocketV4 | undefined
|
||||||
|
connect(url: string, opts?: Options) {
|
||||||
|
this.client = ClientV4(url, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event: PossibleEvent, cb: (data: any) => void) {
|
||||||
|
this.client?.on(event, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: string, data: any, cb: (data: any) => void): void {
|
||||||
|
this.client?.emit(event, data, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.client?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SIOClientV3 implements SIOClient {
|
||||||
|
private client: SocketV3 | undefined
|
||||||
|
connect(url: string, opts?: Options) {
|
||||||
|
this.client = ClientV3(url, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event: PossibleEvent, cb: (data: any) => void): void {
|
||||||
|
this.client?.on(event, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: string, data: any, cb: (data: any) => void): void {
|
||||||
|
this.client?.emit(event, data, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.client?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SIOClientV2 implements SIOClient {
|
||||||
|
private client: any | undefined
|
||||||
|
connect(url: string, opts?: Options) {
|
||||||
|
this.client = new ClientV2(url, opts)
|
||||||
|
wildcard(ClientV2.Manager)(this.client)
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event: PossibleEvent, cb: (data: any) => void): void {
|
||||||
|
this.client?.on(event, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: string, data: any, cb: (data: any) => void): void {
|
||||||
|
this.client?.emit(event, data, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.client?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
163
packages/hoppscotch-app/helpers/realtime/SIOConnection.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { BehaviorSubject, Subject } from "rxjs"
|
||||||
|
import { logHoppRequestRunToAnalytics } from "../fb/analytics"
|
||||||
|
import { SIOClientV2, SIOClientV3, SIOClientV4, SIOClient } from "./SIOClients"
|
||||||
|
import { SIOClientVersion } from "~/newstore/SocketIOSession"
|
||||||
|
|
||||||
|
export const SOCKET_CLIENTS = {
|
||||||
|
v2: SIOClientV2,
|
||||||
|
v3: SIOClientV3,
|
||||||
|
v4: SIOClientV4,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type SIOAuth = { type: "None" } | { type: "Bearer"; token: string }
|
||||||
|
|
||||||
|
export type ConnectionOption = {
|
||||||
|
url: string
|
||||||
|
path: string
|
||||||
|
clientVersion: SIOClientVersion
|
||||||
|
auth: SIOAuth | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SIOMessage = {
|
||||||
|
eventName: string
|
||||||
|
value: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type SIOErrorType = "CONNECTION" | "RECONNECT_ERROR" | "UNKNOWN"
|
||||||
|
export type SIOError = {
|
||||||
|
type: SIOErrorType
|
||||||
|
value: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SIOEvent = { time: number } & (
|
||||||
|
| { type: "CONNECTING" }
|
||||||
|
| { type: "CONNECTED" }
|
||||||
|
| { type: "MESSAGE_SENT"; message: SIOMessage }
|
||||||
|
| { type: "MESSAGE_RECEIVED"; message: SIOMessage }
|
||||||
|
| { type: "DISCONNECTED"; manual: boolean }
|
||||||
|
| { type: "ERROR"; error: SIOError }
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
||||||
|
|
||||||
|
export class SIOConnection {
|
||||||
|
connectionState$: BehaviorSubject<ConnectionState>
|
||||||
|
event$: Subject<SIOEvent> = new Subject()
|
||||||
|
socket: SIOClient | undefined
|
||||||
|
constructor() {
|
||||||
|
this.connectionState$ = new BehaviorSubject<ConnectionState>("DISCONNECTED")
|
||||||
|
}
|
||||||
|
|
||||||
|
private addEvent(event: SIOEvent) {
|
||||||
|
this.event$.next(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
connect({ url, path, clientVersion, auth }: ConnectionOption) {
|
||||||
|
this.connectionState$.next("CONNECTING")
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "CONNECTING",
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
this.socket = new SOCKET_CLIENTS[clientVersion]()
|
||||||
|
|
||||||
|
if (auth?.type === "Bearer") {
|
||||||
|
this.socket.connect(url, {
|
||||||
|
path,
|
||||||
|
auth: {
|
||||||
|
token: auth.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.socket.connect(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.on("connect", () => {
|
||||||
|
this.connectionState$.next("CONNECTED")
|
||||||
|
this.addEvent({
|
||||||
|
type: "CONNECTED",
|
||||||
|
time: Date.now(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.socket.on("*", ({ data }: { data: string[] }) => {
|
||||||
|
const [eventName, message] = data
|
||||||
|
this.addEvent({
|
||||||
|
message: { eventName, value: message },
|
||||||
|
type: "MESSAGE_RECEIVED",
|
||||||
|
time: Date.now(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.socket.on("connect_error", (error: unknown) => {
|
||||||
|
this.handleError(error, "CONNECTION")
|
||||||
|
})
|
||||||
|
|
||||||
|
this.socket.on("reconnect_error", (error: unknown) => {
|
||||||
|
this.handleError(error, "RECONNECT_ERROR")
|
||||||
|
})
|
||||||
|
|
||||||
|
this.socket.on("error", (error: unknown) => {
|
||||||
|
this.handleError(error, "UNKNOWN")
|
||||||
|
})
|
||||||
|
|
||||||
|
this.socket.on("disconnect", () => {
|
||||||
|
this.connectionState$.next("DISCONNECTED")
|
||||||
|
this.addEvent({
|
||||||
|
type: "DISCONNECTED",
|
||||||
|
time: Date.now(),
|
||||||
|
manual: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(error, "CONNECTION")
|
||||||
|
}
|
||||||
|
|
||||||
|
logHoppRequestRunToAnalytics({
|
||||||
|
platform: "socketio",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: unknown, type: SIOErrorType) {
|
||||||
|
this.disconnect()
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "ERROR",
|
||||||
|
error: {
|
||||||
|
type,
|
||||||
|
value: error,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(event: { message: string; eventName: string }) {
|
||||||
|
if (this.connectionState$.value === "DISCONNECTED") return
|
||||||
|
const { message, eventName } = event
|
||||||
|
|
||||||
|
this.socket?.emit(eventName, message, (data) => {
|
||||||
|
// receive response from server
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "MESSAGE_RECEIVED",
|
||||||
|
message: {
|
||||||
|
eventName,
|
||||||
|
value: data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "MESSAGE_SENT",
|
||||||
|
message: {
|
||||||
|
eventName,
|
||||||
|
value: message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.socket?.close()
|
||||||
|
this.connectionState$.next("DISCONNECTED")
|
||||||
|
}
|
||||||
|
}
|
||||||
86
packages/hoppscotch-app/helpers/realtime/SSEConnection.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { BehaviorSubject, Subject } from "rxjs"
|
||||||
|
import { logHoppRequestRunToAnalytics } from "../fb/analytics"
|
||||||
|
|
||||||
|
export type SSEEvent = { time: number } & (
|
||||||
|
| { type: "STARTING" }
|
||||||
|
| { type: "STARTED" }
|
||||||
|
| { type: "MESSAGE_RECEIVED"; message: string }
|
||||||
|
| { type: "STOPPED"; manual: boolean }
|
||||||
|
| { type: "ERROR"; error: Event | null }
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ConnectionState = "STARTING" | "STARTED" | "STOPPED"
|
||||||
|
|
||||||
|
export class SSEConnection {
|
||||||
|
connectionState$: BehaviorSubject<ConnectionState>
|
||||||
|
event$: Subject<SSEEvent> = new Subject()
|
||||||
|
sse: EventSource | undefined
|
||||||
|
constructor() {
|
||||||
|
this.connectionState$ = new BehaviorSubject<ConnectionState>("STOPPED")
|
||||||
|
}
|
||||||
|
|
||||||
|
private addEvent(event: SSEEvent) {
|
||||||
|
this.event$.next(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
start(url: string, eventType: string) {
|
||||||
|
this.connectionState$.next("STARTING")
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "STARTING",
|
||||||
|
})
|
||||||
|
if (typeof EventSource !== "undefined") {
|
||||||
|
try {
|
||||||
|
this.sse = new EventSource(url)
|
||||||
|
this.sse.onopen = () => {
|
||||||
|
this.connectionState$.next("STARTED")
|
||||||
|
this.addEvent({
|
||||||
|
type: "STARTED",
|
||||||
|
time: Date.now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.sse.onerror = this.handleError
|
||||||
|
this.sse.addEventListener(eventType, ({ data }) => {
|
||||||
|
this.addEvent({
|
||||||
|
type: "MESSAGE_RECEIVED",
|
||||||
|
message: data,
|
||||||
|
time: Date.now(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// A generic event type returned if anything goes wrong or browser doesn't support SSE
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventSource/error_event#event_type
|
||||||
|
this.handleError(error as Event)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.addEvent({
|
||||||
|
type: "ERROR",
|
||||||
|
time: Date.now(),
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logHoppRequestRunToAnalytics({
|
||||||
|
platform: "sse",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: Event) {
|
||||||
|
this.stop()
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "ERROR",
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.sse?.close()
|
||||||
|
this.connectionState$.next("STOPPED")
|
||||||
|
this.addEvent({
|
||||||
|
type: "STOPPED",
|
||||||
|
time: Date.now(),
|
||||||
|
manual: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
102
packages/hoppscotch-app/helpers/realtime/WSConnection.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { BehaviorSubject, Subject } from "rxjs"
|
||||||
|
import { logHoppRequestRunToAnalytics } from "../fb/analytics"
|
||||||
|
|
||||||
|
export type WSErrorMessage = SyntaxError | Event
|
||||||
|
|
||||||
|
export type WSEvent = { time: number } & (
|
||||||
|
| { type: "CONNECTING" }
|
||||||
|
| { type: "CONNECTED" }
|
||||||
|
| { type: "MESSAGE_SENT"; message: string }
|
||||||
|
| { type: "MESSAGE_RECEIVED"; message: string }
|
||||||
|
| { type: "DISCONNECTED"; manual: boolean }
|
||||||
|
| { type: "ERROR"; error: WSErrorMessage }
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
||||||
|
|
||||||
|
export class WSConnection {
|
||||||
|
connectionState$: BehaviorSubject<ConnectionState>
|
||||||
|
event$: Subject<WSEvent> = new Subject()
|
||||||
|
socket: WebSocket | undefined
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.connectionState$ = new BehaviorSubject<ConnectionState>("DISCONNECTED")
|
||||||
|
}
|
||||||
|
|
||||||
|
private addEvent(event: WSEvent) {
|
||||||
|
this.event$.next(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(url: string, protocols: string[]) {
|
||||||
|
try {
|
||||||
|
this.connectionState$.next("CONNECTING")
|
||||||
|
this.socket = new WebSocket(url, protocols)
|
||||||
|
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "CONNECTING",
|
||||||
|
})
|
||||||
|
|
||||||
|
this.socket.onopen = () => {
|
||||||
|
this.connectionState$.next("CONNECTED")
|
||||||
|
this.addEvent({
|
||||||
|
type: "CONNECTED",
|
||||||
|
time: Date.now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.onerror = (error) => {
|
||||||
|
this.handleError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.onclose = () => {
|
||||||
|
this.connectionState$.next("DISCONNECTED")
|
||||||
|
this.addEvent({
|
||||||
|
type: "DISCONNECTED",
|
||||||
|
time: Date.now(),
|
||||||
|
manual: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.onmessage = ({ data }) => {
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "MESSAGE_RECEIVED",
|
||||||
|
message: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// We will have SyntaxError if anything goes wrong with WebSocket constructor
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#exceptions
|
||||||
|
this.handleError(error as SyntaxError)
|
||||||
|
}
|
||||||
|
|
||||||
|
logHoppRequestRunToAnalytics({
|
||||||
|
platform: "wss",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: WSErrorMessage) {
|
||||||
|
this.disconnect()
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "ERROR",
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(event: { message: string; eventName: string }) {
|
||||||
|
if (this.connectionState$.value === "DISCONNECTED") return
|
||||||
|
const { message } = event
|
||||||
|
this.socket?.send(message)
|
||||||
|
this.addEvent({
|
||||||
|
time: Date.now(),
|
||||||
|
type: "MESSAGE_SENT",
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.socket?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
8
packages/hoppscotch-app/helpers/shortcodes/Shortcode.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Defines how a Shortcode is represented in the ShortcodeListAdapter
|
||||||
|
*/
|
||||||
|
export interface Shortcode {
|
||||||
|
id: string
|
||||||
|
request: string
|
||||||
|
createdOn: Date
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
import { BehaviorSubject, Subscription } from "rxjs"
|
||||||
|
import { GQLError, runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
|
||||||
|
import {
|
||||||
|
GetUserShortcodesQuery,
|
||||||
|
GetUserShortcodesDocument,
|
||||||
|
ShortcodeCreatedDocument,
|
||||||
|
ShortcodeDeletedDocument,
|
||||||
|
} from "../backend/graphql"
|
||||||
|
import { BACKEND_PAGE_SIZE } from "../backend/helpers"
|
||||||
|
import { Shortcode } from "./Shortcode"
|
||||||
|
|
||||||
|
export default class ShortcodeListAdapter {
|
||||||
|
error$: BehaviorSubject<GQLError<string> | null>
|
||||||
|
loading$: BehaviorSubject<boolean>
|
||||||
|
shortcodes$: BehaviorSubject<GetUserShortcodesQuery["myShortcodes"]>
|
||||||
|
hasMoreShortcodes$: BehaviorSubject<boolean>
|
||||||
|
|
||||||
|
private timeoutHandle: ReturnType<typeof setTimeout> | null
|
||||||
|
private isDispose: boolean
|
||||||
|
|
||||||
|
private myShortcodesCreated: Subscription | null
|
||||||
|
private myShortcodesRevoked: Subscription | null
|
||||||
|
|
||||||
|
constructor(deferInit: boolean = false) {
|
||||||
|
this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
|
||||||
|
this.loading$ = new BehaviorSubject<boolean>(false)
|
||||||
|
this.shortcodes$ = new BehaviorSubject<
|
||||||
|
GetUserShortcodesQuery["myShortcodes"]
|
||||||
|
>([])
|
||||||
|
this.hasMoreShortcodes$ = new BehaviorSubject<boolean>(true)
|
||||||
|
this.timeoutHandle = null
|
||||||
|
this.isDispose = false
|
||||||
|
this.myShortcodesCreated = null
|
||||||
|
this.myShortcodesRevoked = null
|
||||||
|
|
||||||
|
if (!deferInit) this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribeSubscriptions() {
|
||||||
|
this.myShortcodesCreated?.unsubscribe()
|
||||||
|
this.myShortcodesRevoked?.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
if (this.timeoutHandle) throw new Error(`Adapter already initialized`)
|
||||||
|
if (this.isDispose) throw new Error(`Adapter has been disposed`)
|
||||||
|
|
||||||
|
this.fetchList()
|
||||||
|
this.registerSubscriptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
if (!this.timeoutHandle) throw new Error(`Adapter has not been initialized`)
|
||||||
|
if (!this.isDispose) throw new Error(`Adapter has been disposed`)
|
||||||
|
|
||||||
|
this.isDispose = true
|
||||||
|
clearTimeout(this.timeoutHandle)
|
||||||
|
this.timeoutHandle = null
|
||||||
|
this.unsubscribeSubscriptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchList() {
|
||||||
|
this.loadMore(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMore(forcedAttempt = false) {
|
||||||
|
if (!this.hasMoreShortcodes$.value && !forcedAttempt) return
|
||||||
|
|
||||||
|
this.loading$.next(true)
|
||||||
|
|
||||||
|
const lastCodeID =
|
||||||
|
this.shortcodes$.value.length > 0
|
||||||
|
? this.shortcodes$.value[this.shortcodes$.value.length - 1].id
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const result = await runGQLQuery({
|
||||||
|
query: GetUserShortcodesDocument,
|
||||||
|
variables: {
|
||||||
|
cursor: lastCodeID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (E.isLeft(result)) {
|
||||||
|
this.error$.next(result.left)
|
||||||
|
console.error(result.left)
|
||||||
|
this.loading$.next(false)
|
||||||
|
|
||||||
|
throw new Error(`Failed fetching short codes list: ${result.left}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchedResult = result.right.myShortcodes
|
||||||
|
|
||||||
|
this.pushNewShortcodes(fetchedResult)
|
||||||
|
|
||||||
|
if (fetchedResult.length !== BACKEND_PAGE_SIZE) {
|
||||||
|
this.hasMoreShortcodes$.next(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading$.next(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private pushNewShortcodes(results: Shortcode[]) {
|
||||||
|
const userShortcodes = this.shortcodes$.value
|
||||||
|
|
||||||
|
userShortcodes.push(...results)
|
||||||
|
|
||||||
|
this.shortcodes$.next(userShortcodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private createShortcode(shortcode: Shortcode) {
|
||||||
|
const userShortcodes = this.shortcodes$.value
|
||||||
|
|
||||||
|
userShortcodes.unshift(shortcode)
|
||||||
|
|
||||||
|
this.shortcodes$.next(userShortcodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteShortcode(codeId: string) {
|
||||||
|
const newShortcodes = this.shortcodes$.value.filter(
|
||||||
|
({ id }) => id !== codeId
|
||||||
|
)
|
||||||
|
|
||||||
|
this.shortcodes$.next(newShortcodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerSubscriptions() {
|
||||||
|
this.myShortcodesCreated = runGQLSubscription({
|
||||||
|
query: ShortcodeCreatedDocument,
|
||||||
|
}).subscribe((result) => {
|
||||||
|
if (E.isLeft(result)) {
|
||||||
|
console.error(result.left)
|
||||||
|
throw new Error(`Shortcode Create Error ${result.left}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createShortcode(result.right.myShortcodesCreated)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.myShortcodesRevoked = runGQLSubscription({
|
||||||
|
query: ShortcodeDeletedDocument,
|
||||||
|
}).subscribe((result) => {
|
||||||
|
if (E.isLeft(result)) {
|
||||||
|
console.error(result.left)
|
||||||
|
throw new Error(`Shortcode Delete Error ${result.left}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deleteShortcode(result.right.myShortcodesRevoked.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import axios, { AxiosRequestConfig } from "axios"
|
|||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as TE from "fp-ts/TaskEither"
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
import { NetworkResponse, NetworkStrategy } from "../network"
|
import { NetworkResponse, NetworkStrategy } from "../network"
|
||||||
import { decodeB64StringToArrayBuffer } from "../utils/b64"
|
import { decodeB64StringToArrayBuffer } from "../utils/b64"
|
||||||
import { settingsStore } from "~/newstore/settings"
|
import { settingsStore } from "~/newstore/settings"
|
||||||
@@ -40,19 +41,43 @@ const getProxyPayload = (
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preProcessRequest = (req: AxiosRequestConfig): AxiosRequestConfig => {
|
||||||
|
const reqClone = cloneDeep(req)
|
||||||
|
|
||||||
|
// If the parameters are URLSearchParams, inject them to URL instead
|
||||||
|
// This prevents issues of marshalling the URLSearchParams to the proxy
|
||||||
|
if (reqClone.params instanceof URLSearchParams) {
|
||||||
|
try {
|
||||||
|
const url = new URL(reqClone.url ?? "")
|
||||||
|
|
||||||
|
for (const [key, value] of reqClone.params.entries()) {
|
||||||
|
url.searchParams.append(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqClone.url = url.toString()
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
reqClone.params = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reqClone
|
||||||
|
}
|
||||||
|
|
||||||
const axiosWithProxy: NetworkStrategy = (req) =>
|
const axiosWithProxy: NetworkStrategy = (req) =>
|
||||||
pipe(
|
pipe(
|
||||||
TE.Do,
|
TE.Do,
|
||||||
|
|
||||||
|
TE.bind("processedReq", () => TE.of(preProcessRequest(req))),
|
||||||
|
|
||||||
// If the request has FormData, the proxy needs a key
|
// If the request has FormData, the proxy needs a key
|
||||||
TE.bind("multipartKey", () =>
|
TE.bind("multipartKey", ({ processedReq }) =>
|
||||||
TE.of(req.data instanceof FormData ? v4() : null)
|
TE.of(processedReq.data instanceof FormData ? v4() : null)
|
||||||
),
|
),
|
||||||
|
|
||||||
// Build headers to send
|
// Build headers to send
|
||||||
TE.bind("headers", ({ multipartKey }) =>
|
TE.bind("headers", ({ processedReq, multipartKey }) =>
|
||||||
TE.of(
|
TE.of(
|
||||||
req.data instanceof FormData
|
processedReq.data instanceof FormData
|
||||||
? <ProxyHeaders>{
|
? <ProxyHeaders>{
|
||||||
"multipart-part-key": `proxyRequestData-${multipartKey}`,
|
"multipart-part-key": `proxyRequestData-${multipartKey}`,
|
||||||
}
|
}
|
||||||
@@ -61,8 +86,8 @@ const axiosWithProxy: NetworkStrategy = (req) =>
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Create payload
|
// Create payload
|
||||||
TE.bind("payload", ({ multipartKey }) =>
|
TE.bind("payload", ({ processedReq, multipartKey }) =>
|
||||||
TE.of(getProxyPayload(req, multipartKey))
|
TE.of(getProxyPayload(processedReq, multipartKey))
|
||||||
),
|
),
|
||||||
|
|
||||||
// Run the proxy request
|
// Run the proxy request
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import * as TE from "fp-ts/TaskEither"
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import * as O from "fp-ts/Option"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
|
import { AxiosRequestConfig } from "axios"
|
||||||
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
import { NetworkResponse, NetworkStrategy } from "../network"
|
import { NetworkResponse, NetworkStrategy } from "../network"
|
||||||
import { browserIsChrome, browserIsFirefox } from "../utils/userAgent"
|
import { browserIsChrome, browserIsFirefox } from "../utils/userAgent"
|
||||||
|
|
||||||
@@ -13,31 +16,92 @@ export const hasFirefoxExtensionInstalled = () =>
|
|||||||
hasExtensionInstalled() && browserIsFirefox()
|
hasExtensionInstalled() && browserIsFirefox()
|
||||||
|
|
||||||
export const cancelRunningExtensionRequest = () => {
|
export const cancelRunningExtensionRequest = () => {
|
||||||
if (
|
window.__POSTWOMAN_EXTENSION_HOOK__?.cancelRunningRequest()
|
||||||
hasExtensionInstalled() &&
|
|
||||||
window.__POSTWOMAN_EXTENSION_HOOK__.cancelRunningRequest
|
|
||||||
) {
|
|
||||||
window.__POSTWOMAN_EXTENSION_HOOK__.cancelRunningRequest()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defineSubscribableObject = <T extends object>(obj: T) => {
|
||||||
|
const proxyObject = {
|
||||||
|
...obj,
|
||||||
|
_subscribers: {} as {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
[key in keyof T]?: ((...args: any[]) => any)[]
|
||||||
|
},
|
||||||
|
subscribe(prop: keyof T, func: (...args: any[]) => any): void {
|
||||||
|
if (Array.isArray(this._subscribers[prop])) {
|
||||||
|
this._subscribers[prop]?.push(func)
|
||||||
|
} else {
|
||||||
|
this._subscribers[prop] = [func]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscribableProxyObject = typeof proxyObject
|
||||||
|
|
||||||
|
return new Proxy(proxyObject, {
|
||||||
|
set(obj, prop, newVal) {
|
||||||
|
obj[prop as keyof SubscribableProxyObject] = newVal
|
||||||
|
|
||||||
|
const currentSubscribers = obj._subscribers[prop as keyof T]
|
||||||
|
|
||||||
|
if (Array.isArray(currentSubscribers)) {
|
||||||
|
for (const subscriber of currentSubscribers) {
|
||||||
|
subscriber(newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const preProcessRequest = (req: AxiosRequestConfig): AxiosRequestConfig => {
|
||||||
|
const reqClone = cloneDeep(req)
|
||||||
|
|
||||||
|
// If the parameters are URLSearchParams, inject them to URL instead
|
||||||
|
// This prevents marshalling issues with structured cloning of URLSearchParams
|
||||||
|
if (reqClone.params instanceof URLSearchParams) {
|
||||||
|
try {
|
||||||
|
const url = new URL(reqClone.url ?? "")
|
||||||
|
|
||||||
|
for (const [key, value] of reqClone.params.entries()) {
|
||||||
|
url.searchParams.append(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqClone.url = url.toString()
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
reqClone.params = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reqClone
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensionStrategy: NetworkStrategy = (req) =>
|
const extensionStrategy: NetworkStrategy = (req) =>
|
||||||
pipe(
|
pipe(
|
||||||
TE.Do,
|
TE.Do,
|
||||||
|
|
||||||
|
TE.bind("processedReq", () => TE.of(preProcessRequest(req))),
|
||||||
|
|
||||||
// Storeing backup timing data in case the extension does not have that info
|
// Storeing backup timing data in case the extension does not have that info
|
||||||
TE.bind("backupTimeDataStart", () => TE.of(new Date().getTime())),
|
TE.bind("backupTimeDataStart", () => TE.of(new Date().getTime())),
|
||||||
|
|
||||||
// Run the request
|
// Run the request
|
||||||
TE.bind("response", () =>
|
TE.bind("response", ({ processedReq }) =>
|
||||||
|
pipe(
|
||||||
|
window.__POSTWOMAN_EXTENSION_HOOK__,
|
||||||
|
O.fromNullable,
|
||||||
|
TE.fromOption(() => "NO_PW_EXT_HOOK" as const),
|
||||||
|
TE.chain((extensionHook) =>
|
||||||
TE.tryCatch(
|
TE.tryCatch(
|
||||||
() =>
|
() =>
|
||||||
window.__POSTWOMAN_EXTENSION_HOOK__.sendRequest({
|
extensionHook.sendRequest({
|
||||||
...req,
|
...processedReq,
|
||||||
wantsBinary: true,
|
wantsBinary: true,
|
||||||
}) as Promise<NetworkResponse>,
|
}),
|
||||||
(err) => err as any
|
(err) => err as any
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
// Inject backup time data if not present
|
// Inject backup time data if not present
|
||||||
|
|||||||
@@ -122,13 +122,6 @@ describe("cancelRunningExtensionRequest", () => {
|
|||||||
cancelRunningExtensionRequest()
|
cancelRunningExtensionRequest()
|
||||||
expect(cancelFunc).not.toHaveBeenCalled()
|
expect(cancelFunc).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("does not cancel request if extension installed but function not present", () => {
|
|
||||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
|
||||||
|
|
||||||
cancelRunningExtensionRequest()
|
|
||||||
expect(cancelFunc).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("extensionStrategy", () => {
|
describe("extensionStrategy", () => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
export type HoppRealtimeLogLine = {
|
export type HoppRealtimeLogLine = {
|
||||||
|
prefix?: string
|
||||||
payload: string
|
payload: string
|
||||||
source: string
|
source: string
|
||||||
color?: string
|
color?: string
|
||||||
ts: string
|
ts: number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HoppRealtimeLog = HoppRealtimeLogLine[]
|
export type HoppRealtimeLog = HoppRealtimeLogLine[]
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
parseBodyEnvVariables,
|
parseBodyEnvVariables,
|
||||||
parseRawKeyValueEntries,
|
parseRawKeyValueEntries,
|
||||||
Environment,
|
Environment,
|
||||||
|
HoppRESTHeader,
|
||||||
|
HoppRESTParam,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import { arrayFlatMap, arraySort } from "../functional/array"
|
import { arrayFlatMap, arraySort } from "../functional/array"
|
||||||
import { toFormData } from "../functional/formData"
|
import { toFormData } from "../functional/formData"
|
||||||
@@ -29,6 +31,146 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
|||||||
effectiveFinalBody: FormData | string | null
|
effectiveFinalBody: FormData | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get headers that can be generated by authorization config of the request
|
||||||
|
* @param req Request to check
|
||||||
|
* @param envVars Currently active environment variables
|
||||||
|
* @returns The list of headers
|
||||||
|
*/
|
||||||
|
const getComputedAuthHeaders = (
|
||||||
|
req: HoppRESTRequest,
|
||||||
|
envVars: Environment["variables"]
|
||||||
|
) => {
|
||||||
|
// If Authorization header is also being user-defined, that takes priority
|
||||||
|
if (req.headers.find((h) => h.key.toLowerCase() === "authorization"))
|
||||||
|
return []
|
||||||
|
|
||||||
|
if (!req.auth.authActive) return []
|
||||||
|
|
||||||
|
const headers: HoppRESTHeader[] = []
|
||||||
|
|
||||||
|
// TODO: Support a better b64 implementation than btoa ?
|
||||||
|
if (req.auth.authType === "basic") {
|
||||||
|
const username = parseTemplateString(req.auth.username, envVars)
|
||||||
|
const password = parseTemplateString(req.auth.password, envVars)
|
||||||
|
|
||||||
|
headers.push({
|
||||||
|
active: true,
|
||||||
|
key: "Authorization",
|
||||||
|
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||||
|
})
|
||||||
|
} else if (
|
||||||
|
req.auth.authType === "bearer" ||
|
||||||
|
req.auth.authType === "oauth-2"
|
||||||
|
) {
|
||||||
|
headers.push({
|
||||||
|
active: true,
|
||||||
|
key: "Authorization",
|
||||||
|
value: `Bearer ${parseTemplateString(req.auth.token, envVars)}`,
|
||||||
|
})
|
||||||
|
} else if (req.auth.authType === "api-key") {
|
||||||
|
const { key, value, addTo } = req.auth
|
||||||
|
|
||||||
|
if (addTo === "Headers") {
|
||||||
|
headers.push({
|
||||||
|
active: true,
|
||||||
|
key: parseTemplateString(key, envVars),
|
||||||
|
value: parseTemplateString(value, envVars),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get headers that can be generated by body config of the request
|
||||||
|
* @param req Request to check
|
||||||
|
* @returns The list of headers
|
||||||
|
*/
|
||||||
|
export const getComputedBodyHeaders = (
|
||||||
|
req: HoppRESTRequest
|
||||||
|
): HoppRESTHeader[] => {
|
||||||
|
// If a content-type is already defined, that will override this
|
||||||
|
if (
|
||||||
|
req.headers.find(
|
||||||
|
(req) => req.active && req.key.toLowerCase() === "content-type"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
// Body should have a non-null content-type
|
||||||
|
if (req.body.contentType === null) return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
key: "content-type",
|
||||||
|
value: req.body.contentType,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComputedHeader = {
|
||||||
|
source: "auth" | "body"
|
||||||
|
header: HoppRESTHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of headers that will be added during execution of the request
|
||||||
|
* For e.g, Authorization headers maybe added if an Auth Mode is defined on REST
|
||||||
|
* @param req The request to check
|
||||||
|
* @param envVars The environment variables active
|
||||||
|
* @returns The headers that are generated along with the source of that header
|
||||||
|
*/
|
||||||
|
export const getComputedHeaders = (
|
||||||
|
req: HoppRESTRequest,
|
||||||
|
envVars: Environment["variables"]
|
||||||
|
): ComputedHeader[] => [
|
||||||
|
...getComputedAuthHeaders(req, envVars).map((header) => ({
|
||||||
|
source: "auth" as const,
|
||||||
|
header,
|
||||||
|
})),
|
||||||
|
...getComputedBodyHeaders(req).map((header) => ({
|
||||||
|
source: "body" as const,
|
||||||
|
header,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
|
||||||
|
export type ComputedParam = {
|
||||||
|
source: "auth"
|
||||||
|
param: HoppRESTParam
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of params that will be added during execution of the request
|
||||||
|
* For e.g, Authorization params (like API-key) maybe added if an Auth Mode is defined on REST
|
||||||
|
* @param req The request to check
|
||||||
|
* @param envVars The environment variables active
|
||||||
|
* @returns The params that are generated along with the source of that header
|
||||||
|
*/
|
||||||
|
export const getComputedParams = (
|
||||||
|
req: HoppRESTRequest,
|
||||||
|
envVars: Environment["variables"]
|
||||||
|
): ComputedParam[] => {
|
||||||
|
// When this gets complex, its best to split this function off (like with getComputedHeaders)
|
||||||
|
// API-key auth can be added to query params
|
||||||
|
if (!req.auth.authActive) return []
|
||||||
|
if (req.auth.authType !== "api-key") return []
|
||||||
|
if (req.auth.addTo !== "Query params") return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "auth",
|
||||||
|
param: {
|
||||||
|
active: true,
|
||||||
|
key: parseTemplateString(req.auth.key, envVars),
|
||||||
|
value: parseTemplateString(req.auth.value, envVars),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
// Resolves environment variables in the body
|
// Resolves environment variables in the body
|
||||||
export const resolvesEnvsInBody = (
|
export const resolvesEnvsInBody = (
|
||||||
body: HoppRESTReqBody,
|
body: HoppRESTReqBody,
|
||||||
@@ -135,83 +277,29 @@ export function getEffectiveRESTRequest(
|
|||||||
): EffectiveHoppRESTRequest {
|
): EffectiveHoppRESTRequest {
|
||||||
const envVariables = [...environment.variables, ...getGlobalVariables()]
|
const envVariables = [...environment.variables, ...getGlobalVariables()]
|
||||||
|
|
||||||
const effectiveFinalHeaders = request.headers
|
const effectiveFinalHeaders = pipe(
|
||||||
.filter(
|
getComputedHeaders(request, envVariables).map((h) => h.header),
|
||||||
(x) =>
|
A.concat(request.headers),
|
||||||
x.key !== "" && // Remove empty keys
|
A.filter((x) => x.active && x.key !== ""),
|
||||||
x.active // Only active
|
A.map((x) => ({
|
||||||
)
|
|
||||||
.map((x) => ({
|
|
||||||
// Parse out environment template strings
|
|
||||||
active: true,
|
active: true,
|
||||||
key: parseTemplateString(x.key, envVariables),
|
key: parseTemplateString(x.key, envVariables),
|
||||||
value: parseTemplateString(x.value, envVariables),
|
value: parseTemplateString(x.value, envVariables),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const effectiveFinalParams = request.params
|
|
||||||
.filter(
|
|
||||||
(x) =>
|
|
||||||
x.key !== "" && // Remove empty keys
|
|
||||||
x.active // Only active
|
|
||||||
)
|
)
|
||||||
.map((x) => ({
|
|
||||||
|
const effectiveFinalParams = pipe(
|
||||||
|
getComputedParams(request, envVariables).map((p) => p.param),
|
||||||
|
A.concat(request.params),
|
||||||
|
A.filter((x) => x.active && x.key !== ""),
|
||||||
|
A.map((x) => ({
|
||||||
active: true,
|
active: true,
|
||||||
key: parseTemplateString(x.key, envVariables),
|
key: parseTemplateString(x.key, envVariables),
|
||||||
value: parseTemplateString(x.value, envVariables),
|
value: parseTemplateString(x.value, envVariables),
|
||||||
}))
|
}))
|
||||||
|
)
|
||||||
// Authentication
|
|
||||||
if (request.auth.authActive) {
|
|
||||||
// TODO: Support a better b64 implementation than btoa ?
|
|
||||||
if (request.auth.authType === "basic") {
|
|
||||||
const username = parseTemplateString(request.auth.username, envVariables)
|
|
||||||
const password = parseTemplateString(request.auth.password, envVariables)
|
|
||||||
|
|
||||||
effectiveFinalHeaders.push({
|
|
||||||
active: true,
|
|
||||||
key: "Authorization",
|
|
||||||
value: `Basic ${btoa(`${username}:${password}`)}`,
|
|
||||||
})
|
|
||||||
} else if (
|
|
||||||
request.auth.authType === "bearer" ||
|
|
||||||
request.auth.authType === "oauth-2"
|
|
||||||
) {
|
|
||||||
effectiveFinalHeaders.push({
|
|
||||||
active: true,
|
|
||||||
key: "Authorization",
|
|
||||||
value: `Bearer ${parseTemplateString(
|
|
||||||
request.auth.token,
|
|
||||||
envVariables
|
|
||||||
)}`,
|
|
||||||
})
|
|
||||||
} else if (request.auth.authType === "api-key") {
|
|
||||||
const { key, value, addTo } = request.auth
|
|
||||||
if (addTo === "Headers") {
|
|
||||||
effectiveFinalHeaders.push({
|
|
||||||
active: true,
|
|
||||||
key: parseTemplateString(key, envVariables),
|
|
||||||
value: parseTemplateString(value, envVariables),
|
|
||||||
})
|
|
||||||
} else if (addTo === "Query params") {
|
|
||||||
effectiveFinalParams.push({
|
|
||||||
active: true,
|
|
||||||
key: parseTemplateString(key, envVariables),
|
|
||||||
value: parseTemplateString(value, envVariables),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
|
const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
|
||||||
const contentTypeInHeader = effectiveFinalHeaders.find(
|
|
||||||
(x) => x.key.toLowerCase() === "content-type"
|
|
||||||
)
|
|
||||||
if (request.body.contentType && !contentTypeInHeader?.value)
|
|
||||||
effectiveFinalHeaders.push({
|
|
||||||
active: true,
|
|
||||||
key: "content-type",
|
|
||||||
value: request.body.contentType,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...request,
|
...request,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ describe("wsValid", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("returns true for valid URL with Hostname", () => {
|
test("returns true for valid URL with Hostname", () => {
|
||||||
expect(wsValid("wss://hoppscotch-websocket.herokuapp.com/")).toBe(true)
|
expect(wsValid("wss://echo-websocket.hoppscotch.io/")).toBe(true)
|
||||||
expect(wsValid("wss://hoppscotch-websocket.herokuapp.com")).toBe(true)
|
expect(wsValid("wss://echo-websocket.hoppscotch.io")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns false for invalid URL with IP address", () => {
|
test("returns false for invalid URL with IP address", () => {
|
||||||
@@ -29,8 +29,8 @@ describe("wsValid", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("returns true for wss protocol URLs", () => {
|
test("returns true for wss protocol URLs", () => {
|
||||||
expect(wsValid("wss://hoppscotch-websocket.herokuapp.com/")).toBe(true)
|
expect(wsValid("wss://echo-websocket.hoppscotch.io/")).toBe(true)
|
||||||
expect(wsValid("wss://hoppscotch-websocket.herokuapp.com")).toBe(true)
|
expect(wsValid("wss://echo-websocket.hoppscotch.io")).toBe(true)
|
||||||
expect(wsValid("wss://174.129.224.73/")).toBe(true)
|
expect(wsValid("wss://174.129.224.73/")).toBe(true)
|
||||||
expect(wsValid("wss://174.129.224.73")).toBe(true)
|
expect(wsValid("wss://174.129.224.73")).toBe(true)
|
||||||
})
|
})
|
||||||
@@ -65,8 +65,8 @@ describe("httpValid", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("returns false for non-http(s) protocol URLs", () => {
|
test("returns false for non-http(s) protocol URLs", () => {
|
||||||
expect(httpValid("wss://hoppscotch-websocket.herokuapp.com/")).toBe(false)
|
expect(httpValid("wss://echo-websocket.hoppscotch.io/")).toBe(false)
|
||||||
expect(httpValid("wss://hoppscotch-websocket.herokuapp.com")).toBe(false)
|
expect(httpValid("wss://echo-websocket.hoppscotch.io")).toBe(false)
|
||||||
expect(httpValid("wss://174.129.224.73/")).toBe(false)
|
expect(httpValid("wss://174.129.224.73/")).toBe(false)
|
||||||
expect(httpValid("wss://174.129.224.73")).toBe(false)
|
expect(httpValid("wss://174.129.224.73")).toBe(false)
|
||||||
})
|
})
|
||||||
@@ -129,10 +129,8 @@ describe("socketioValid", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("returns true for wss protocol URLs", () => {
|
test("returns true for wss protocol URLs", () => {
|
||||||
expect(socketioValid("wss://hoppscotch-websocket.herokuapp.com/")).toBe(
|
expect(socketioValid("wss://echo-websocket.hoppscotch.io/")).toBe(true)
|
||||||
true
|
expect(socketioValid("wss://echo-websocket.hoppscotch.io")).toBe(true)
|
||||||
)
|
|
||||||
expect(socketioValid("wss://hoppscotch-websocket.herokuapp.com")).toBe(true)
|
|
||||||
expect(socketioValid("wss://174.129.224.73/")).toBe(true)
|
expect(socketioValid("wss://174.129.224.73/")).toBe(true)
|
||||||
expect(socketioValid("wss://174.129.224.73")).toBe(true)
|
expect(socketioValid("wss://174.129.224.73")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
const sourceEmojis = {
|
|
||||||
// Source used for info messages.
|
|
||||||
info: "\tℹ️ [INFO]:\t",
|
|
||||||
// Source used for client to server messages.
|
|
||||||
client: "\t⬅️ [SENT]:\t",
|
|
||||||
// Source used for server to client messages.
|
|
||||||
server: "\t➡️ [RECEIVED]:\t",
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSourcePrefix(source: keyof typeof sourceEmojis) {
|
|
||||||
return sourceEmojis[source]
|
|
||||||
}
|
|
||||||
@@ -64,6 +64,8 @@ import {
|
|||||||
useRouter,
|
useRouter,
|
||||||
watch,
|
watch,
|
||||||
ref,
|
ref,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
} from "@nuxtjs/composition-api"
|
} from "@nuxtjs/composition-api"
|
||||||
import { Splitpanes, Pane } from "splitpanes"
|
import { Splitpanes, Pane } from "splitpanes"
|
||||||
import "splitpanes/dist/splitpanes.css"
|
import "splitpanes/dist/splitpanes.css"
|
||||||
@@ -77,6 +79,12 @@ import { hookKeybindingsListener } from "~/helpers/keybindings"
|
|||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { useSentry } from "~/helpers/sentry"
|
import { useSentry } from "~/helpers/sentry"
|
||||||
import { useColorMode } from "~/helpers/utils/composables"
|
import { useColorMode } from "~/helpers/utils/composables"
|
||||||
|
import {
|
||||||
|
changeExtensionStatus,
|
||||||
|
ExtensionStatus,
|
||||||
|
} from "~/newstore/HoppExtension"
|
||||||
|
|
||||||
|
import { defineSubscribableObject } from "~/helpers/strategies/ExtensionStrategy"
|
||||||
|
|
||||||
function appLayout() {
|
function appLayout() {
|
||||||
const rightSidebar = useSetting("SIDEBAR")
|
const rightSidebar = useSetting("SIDEBAR")
|
||||||
@@ -202,6 +210,62 @@ function defineJumpActions() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupExtensionHooks() {
|
||||||
|
const extensionPollIntervalId = ref<ReturnType<typeof setInterval>>()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (window.__HOPP_EXTENSION_STATUS_PROXY__) {
|
||||||
|
changeExtensionStatus(window.__HOPP_EXTENSION_STATUS_PROXY__.status)
|
||||||
|
|
||||||
|
window.__HOPP_EXTENSION_STATUS_PROXY__.subscribe(
|
||||||
|
"status",
|
||||||
|
(status: ExtensionStatus) => changeExtensionStatus(status)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const statusProxy = defineSubscribableObject({
|
||||||
|
status: "waiting" as ExtensionStatus,
|
||||||
|
})
|
||||||
|
|
||||||
|
window.__HOPP_EXTENSION_STATUS_PROXY__ = statusProxy
|
||||||
|
statusProxy.subscribe("status", (status: ExtensionStatus) =>
|
||||||
|
changeExtensionStatus(status)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeping identifying extension backward compatible
|
||||||
|
* We are assuming the default version is 0.24 or later. So if the extension exists, its identified immediately,
|
||||||
|
* then we use a poll to find the version, this will get the version for 0.24 and any other version
|
||||||
|
* of the extension, but will have a slight lag.
|
||||||
|
* 0.24 users will get the benefits of 0.24, while the extension won't break for the old users
|
||||||
|
*/
|
||||||
|
extensionPollIntervalId.value = setInterval(() => {
|
||||||
|
if (typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined") {
|
||||||
|
if (extensionPollIntervalId.value)
|
||||||
|
clearInterval(extensionPollIntervalId.value)
|
||||||
|
|
||||||
|
const version = window.__POSTWOMAN_EXTENSION_HOOK__.getVersion()
|
||||||
|
|
||||||
|
// When the version is not 0.24 or higher, the extension wont do this. so we have to do it manually
|
||||||
|
if (
|
||||||
|
version.major === 0 &&
|
||||||
|
version.minor <= 23 &&
|
||||||
|
window.__HOPP_EXTENSION_STATUS_PROXY__
|
||||||
|
) {
|
||||||
|
window.__HOPP_EXTENSION_STATUS_PROXY__.status = "available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cleanup timer
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (extensionPollIntervalId.value) {
|
||||||
|
clearInterval(extensionPollIntervalId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { Splitpanes, Pane },
|
components: { Splitpanes, Pane },
|
||||||
setup() {
|
setup() {
|
||||||
@@ -229,6 +293,8 @@ export default defineComponent({
|
|||||||
showSupport.value = !showSupport.value
|
showSupport.value = !showSupport.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setupExtensionHooks()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mdAndLarger,
|
mdAndLarger,
|
||||||
spacerClass,
|
spacerClass,
|
||||||
|
|||||||