feat: port ace editor to codemirror
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
@@ -467,7 +467,7 @@ input[type="checkbox"] {
|
|||||||
@apply border-b;
|
@apply border-b;
|
||||||
@apply border-dividerLight;
|
@apply border-dividerLight;
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
@apply h-auto;
|
@apply !h-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror * {
|
.CodeMirror * {
|
||||||
|
|||||||
@@ -33,45 +33,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, PropType } from "@nuxtjs/composition-api"
|
|
||||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||||
import { getCurrentStrategyID } from "~/helpers/network"
|
import { getCurrentStrategyID } from "~/helpers/network"
|
||||||
import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
|
import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
|
||||||
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
|
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
conn: GQLConnection
|
||||||
conn: {
|
}>()
|
||||||
type: Object as PropType<GQLConnection>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const connected = useReadonlyStream(props.conn.connected$, false)
|
|
||||||
const headers = useReadonlyStream(gqlHeaders$, [])
|
|
||||||
|
|
||||||
const url = useStream(gqlURL$, "", setGQLURL)
|
const connected = useReadonlyStream(props.conn.connected$, false)
|
||||||
|
const headers = useReadonlyStream(gqlHeaders$, [])
|
||||||
|
|
||||||
const onConnectClick = () => {
|
const url = useStream(gqlURL$, "", setGQLURL)
|
||||||
if (!connected.value) {
|
|
||||||
props.conn.connect(url.value, headers.value as any)
|
|
||||||
|
|
||||||
logHoppRequestRunToAnalytics({
|
const onConnectClick = () => {
|
||||||
platform: "graphql-schema",
|
if (!connected.value) {
|
||||||
strategy: getCurrentStrategyID(),
|
props.conn.connect(url.value, headers.value as any)
|
||||||
})
|
|
||||||
} else {
|
|
||||||
props.conn.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
logHoppRequestRunToAnalytics({
|
||||||
url,
|
platform: "graphql-schema",
|
||||||
connected,
|
strategy: getCurrentStrategyID(),
|
||||||
onConnectClick,
|
})
|
||||||
}
|
} else {
|
||||||
},
|
props.conn.disconnect()
|
||||||
})
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -106,19 +106,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartAceEditor
|
<div ref="variableEditor" class="w-full block"></div>
|
||||||
ref="variableEditor"
|
|
||||||
v-model="variableString"
|
|
||||||
:lang="'json'"
|
|
||||||
:options="{
|
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 16,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
styles="border-b border-dividerLight"
|
|
||||||
/>
|
|
||||||
</AppSection>
|
</AppSection>
|
||||||
</SmartTab>
|
</SmartTab>
|
||||||
|
|
||||||
@@ -323,6 +311,7 @@ import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
|||||||
import { getCurrentStrategyID } from "~/helpers/network"
|
import { getCurrentStrategyID } from "~/helpers/network"
|
||||||
import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
|
import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
|
||||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
|
import "codemirror/mode/javascript/javascript"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
conn: GQLConnection
|
conn: GQLConnection
|
||||||
@@ -370,6 +359,16 @@ useCodemirror(bulkEditor, bulkHeaders, {
|
|||||||
completer: null,
|
completer: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const variableEditor = ref<any | null>(null)
|
||||||
|
|
||||||
|
useCodemirror(variableEditor, variableString, {
|
||||||
|
extendedEditorConfig: {
|
||||||
|
mode: "javascript",
|
||||||
|
},
|
||||||
|
linter: null,
|
||||||
|
completer: null,
|
||||||
|
})
|
||||||
|
|
||||||
const queryEditor = ref<any | null>(null)
|
const queryEditor = ref<any | null>(null)
|
||||||
|
|
||||||
const copyQueryIcon = ref("copy")
|
const copyQueryIcon = ref("copy")
|
||||||
|
|||||||
@@ -59,38 +59,6 @@
|
|||||||
justify-center
|
justify-center
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="flex space-x-2 pb-4">
|
|
||||||
<div class="flex flex-col space-y-4 items-end">
|
|
||||||
<span class="flex flex-1 items-center">
|
|
||||||
{{ $t("shortcut.request.send_request") }}
|
|
||||||
</span>
|
|
||||||
<span class="flex flex-1 items-center">
|
|
||||||
{{ $t("shortcut.general.show_all") }}
|
|
||||||
</span>
|
|
||||||
<!-- <span class="flex flex-1 items-center">
|
|
||||||
{{ $t("shortcut.general.command_menu") }}
|
|
||||||
</span>
|
|
||||||
<span class="flex flex-1 items-center">
|
|
||||||
{{ $t("shortcut.general.help_menu") }}
|
|
||||||
</span> -->
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="flex">
|
|
||||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
|
||||||
<span class="shortcut-key">G</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
|
||||||
<span class="shortcut-key">K</span>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="flex">
|
|
||||||
<span class="shortcut-key">/</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
<span class="shortcut-key">?</span>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="$t('app.documentation')"
|
:label="$t('app.documentation')"
|
||||||
to="https://docs.hoppscotch.io"
|
to="https://docs.hoppscotch.io"
|
||||||
@@ -103,77 +71,56 @@
|
|||||||
</AppSection>
|
</AppSection>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { PropType, ref, useContext } from "@nuxtjs/composition-api"
|
||||||
defineComponent,
|
|
||||||
PropType,
|
|
||||||
ref,
|
|
||||||
useContext,
|
|
||||||
} from "@nuxtjs/composition-api"
|
|
||||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||||
import { gqlResponse$ } from "~/newstore/GQLSession"
|
import { gqlResponse$ } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
export default defineComponent({
|
defineProps({
|
||||||
props: {
|
conn: {
|
||||||
conn: {
|
type: Object as PropType<GQLConnection>,
|
||||||
type: Object as PropType<GQLConnection>,
|
required: true,
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const {
|
|
||||||
$toast,
|
|
||||||
app: { i18n },
|
|
||||||
} = useContext()
|
|
||||||
const t = i18n.t.bind(i18n)
|
|
||||||
|
|
||||||
const responseString = useReadonlyStream(gqlResponse$, "")
|
|
||||||
|
|
||||||
const downloadResponseIcon = ref("download")
|
|
||||||
const copyResponseIcon = ref("copy")
|
|
||||||
|
|
||||||
const copyResponse = () => {
|
|
||||||
copyToClipboard(responseString.value!)
|
|
||||||
copyResponseIcon.value = "check"
|
|
||||||
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadResponse = () => {
|
|
||||||
const dataToWrite = responseString.value
|
|
||||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
|
||||||
const a = document.createElement("a")
|
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
a.href = url
|
|
||||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
downloadResponseIcon.value = "check"
|
|
||||||
$toast.success(t("state.download_started").toString(), {
|
|
||||||
icon: "downloading",
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.removeChild(a)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
downloadResponseIcon.value = "download"
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
responseString,
|
|
||||||
|
|
||||||
downloadResponseIcon,
|
|
||||||
copyResponseIcon,
|
|
||||||
|
|
||||||
downloadResponse,
|
|
||||||
copyResponse,
|
|
||||||
|
|
||||||
getSpecialKey: getPlatformSpecialKey,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
$toast,
|
||||||
|
app: { i18n },
|
||||||
|
} = useContext()
|
||||||
|
const t = i18n.t.bind(i18n)
|
||||||
|
|
||||||
|
const responseString = useReadonlyStream(gqlResponse$, "")
|
||||||
|
|
||||||
|
const downloadResponseIcon = ref("download")
|
||||||
|
const copyResponseIcon = ref("copy")
|
||||||
|
|
||||||
|
const copyResponse = () => {
|
||||||
|
copyToClipboard(responseString.value!)
|
||||||
|
copyResponseIcon.value = "check"
|
||||||
|
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadResponse = () => {
|
||||||
|
const dataToWrite = responseString.value
|
||||||
|
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||||
|
const a = document.createElement("a")
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
|
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
downloadResponseIcon.value = "check"
|
||||||
|
$toast.success(t("state.download_started").toString(), {
|
||||||
|
icon: "downloading",
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
downloadResponseIcon.value = "download"
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -9,38 +9,6 @@
|
|||||||
justify-center
|
justify-center
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="flex space-x-2 pb-4">
|
|
||||||
<div class="flex flex-col space-y-4 items-end">
|
|
||||||
<span class="flex flex-1 items-center">
|
|
||||||
{{ $t("shortcut.request.send_request") }}
|
|
||||||
</span>
|
|
||||||
<span class="flex flex-1 items-center">
|
|
||||||
{{ $t("shortcut.general.show_all") }}
|
|
||||||
</span>
|
|
||||||
<!-- <span class="flex flex-1 items-center">
|
|
||||||
{{ $t("shortcut.general.command_menu") }}
|
|
||||||
</span>
|
|
||||||
<span class="flex flex-1 items-center">
|
|
||||||
{{ $t("shortcut.general.help_menu") }}
|
|
||||||
</span> -->
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="flex">
|
|
||||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
|
||||||
<span class="shortcut-key">G</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
|
||||||
<span class="shortcut-key">K</span>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="flex">
|
|
||||||
<span class="shortcut-key">/</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
<span class="shortcut-key">?</span>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="$t('app.documentation')"
|
:label="$t('app.documentation')"
|
||||||
to="https://docs.hoppscotch.io"
|
to="https://docs.hoppscotch.io"
|
||||||
@@ -102,26 +70,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { computed } from "@nuxtjs/composition-api"
|
||||||
import findStatusGroup from "~/helpers/findStatusGroup"
|
import findStatusGroup from "~/helpers/findStatusGroup"
|
||||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
response: HoppRESTResponse
|
||||||
response: {
|
}>()
|
||||||
type: Object,
|
|
||||||
default: () => null,
|
const statusCategory = computed(() => {
|
||||||
},
|
if (
|
||||||
},
|
props.response.type === "loading" ||
|
||||||
computed: {
|
props.response.type === "network_fail"
|
||||||
statusCategory() {
|
)
|
||||||
return findStatusGroup(this.response.statusCode)
|
return ""
|
||||||
},
|
return findStatusGroup(props.response.statusCode)
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getSpecialKey: getPlatformSpecialKey,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -45,19 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<SmartAceEditor
|
<div ref="htmlResponse" class="w-full block"></div>
|
||||||
:value="responseBodyText"
|
|
||||||
:lang="'html'"
|
|
||||||
:options="{
|
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 16,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
readOnly: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
styles="border-b border-dividerLight"
|
|
||||||
/>
|
|
||||||
<iframe
|
<iframe
|
||||||
ref="previewFrame"
|
ref="previewFrame"
|
||||||
:class="{ hidden: !previewEnabled }"
|
:class="{ hidden: !previewEnabled }"
|
||||||
@@ -68,76 +56,102 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { computed, ref, useContext } from "@nuxtjs/composition-api"
|
||||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import "codemirror/mode/htmlmixed/htmlmixed"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
mixins: [TextContentRendererMixin],
|
response: HoppRESTResponse
|
||||||
props: {
|
}>()
|
||||||
response: { type: Object, default: () => {} },
|
|
||||||
},
|
const {
|
||||||
data() {
|
$toast,
|
||||||
return {
|
app: { i18n },
|
||||||
downloadIcon: "download",
|
} = useContext()
|
||||||
copyIcon: "copy",
|
const t = i18n.t.bind(i18n)
|
||||||
previewEnabled: false,
|
|
||||||
}
|
const responseBodyText = computed(() => {
|
||||||
},
|
if (
|
||||||
methods: {
|
props.response.type === "loading" ||
|
||||||
downloadResponse() {
|
props.response.type === "network_fail"
|
||||||
const dataToWrite = this.responseBodyText
|
)
|
||||||
const file = new Blob([dataToWrite], { type: "text/html" })
|
return ""
|
||||||
const a = document.createElement("a")
|
if (typeof props.response.body === "string") return props.response.body
|
||||||
const url = URL.createObjectURL(file)
|
else {
|
||||||
a.href = url
|
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||||
// TODO get uri from meta
|
// HACK: Temporary trailing null character issue from the extension fix
|
||||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
return res.replace(/\0+$/, "")
|
||||||
document.body.appendChild(a)
|
}
|
||||||
a.click()
|
|
||||||
this.downloadIcon = "check"
|
|
||||||
this.$toast.success(this.$t("state.download_started"), {
|
|
||||||
icon: "downloading",
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.removeChild(a)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
this.downloadIcon = "download"
|
|
||||||
}, 1000)
|
|
||||||
},
|
|
||||||
copyResponse() {
|
|
||||||
copyToClipboard(this.responseBodyText)
|
|
||||||
this.copyIcon = "check"
|
|
||||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
|
||||||
icon: "content_paste",
|
|
||||||
})
|
|
||||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
|
||||||
},
|
|
||||||
togglePreview() {
|
|
||||||
this.previewEnabled = !this.previewEnabled
|
|
||||||
if (this.previewEnabled) {
|
|
||||||
if (
|
|
||||||
this.$refs.previewFrame.getAttribute("data-previewing-url") ===
|
|
||||||
this.url
|
|
||||||
)
|
|
||||||
return
|
|
||||||
// Use DOMParser to parse document HTML.
|
|
||||||
const previewDocument = new DOMParser().parseFromString(
|
|
||||||
this.responseBodyText,
|
|
||||||
"text/html"
|
|
||||||
)
|
|
||||||
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
|
|
||||||
previewDocument.head.innerHTML =
|
|
||||||
`<base href="${this.url}">` + previewDocument.head.innerHTML
|
|
||||||
// Finally, set the iframe source to the resulting HTML.
|
|
||||||
this.$refs.previewFrame.srcdoc =
|
|
||||||
previewDocument.documentElement.outerHTML
|
|
||||||
this.$refs.previewFrame.setAttribute("data-previewing-url", this.url)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const downloadIcon = ref("download")
|
||||||
|
const copyIcon = ref("copy")
|
||||||
|
const previewEnabled = ref(false)
|
||||||
|
const previewFrame = ref<any | null>(null)
|
||||||
|
const url = ref("")
|
||||||
|
|
||||||
|
const htmlResponse = ref<any | null>(null)
|
||||||
|
|
||||||
|
useCodemirror(htmlResponse, responseBodyText, {
|
||||||
|
extendedEditorConfig: {
|
||||||
|
mode: "javascript",
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
linter: null,
|
||||||
|
completer: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadResponse = () => {
|
||||||
|
const dataToWrite = responseBodyText.value
|
||||||
|
const file = new Blob([dataToWrite], { type: "text/html" })
|
||||||
|
const a = document.createElement("a")
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
|
// TODO get uri from meta
|
||||||
|
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
downloadIcon.value = "check"
|
||||||
|
$toast.success(t("state.download_started").toString(), {
|
||||||
|
icon: "downloading",
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
downloadIcon.value = "download"
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyResponse = () => {
|
||||||
|
copyToClipboard(responseBodyText.value)
|
||||||
|
copyIcon.value = "check"
|
||||||
|
$toast.success(t("state.copied_to_clipboard").toString(), {
|
||||||
|
icon: "content_paste",
|
||||||
|
})
|
||||||
|
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePreview = () => {
|
||||||
|
previewEnabled.value = !previewEnabled.value
|
||||||
|
if (previewEnabled.value) {
|
||||||
|
if (previewFrame.value.getAttribute("data-previewing-url") === url.value)
|
||||||
|
return
|
||||||
|
// Use DOMParser to parse document HTML.
|
||||||
|
const previewDocument = new DOMParser().parseFromString(
|
||||||
|
responseBodyText.value,
|
||||||
|
"text/html"
|
||||||
|
)
|
||||||
|
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
|
||||||
|
previewDocument.head.innerHTML =
|
||||||
|
`<base href="${url.value}">` + previewDocument.head.innerHTML
|
||||||
|
// Finally, set the iframe source to the resulting HTML.
|
||||||
|
previewFrame.value.srcdoc = previewDocument.documentElement.outerHTML
|
||||||
|
previewFrame.value.setAttribute("data-previewing-url", url.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -149,5 +163,6 @@ export default defineComponent({
|
|||||||
@apply w-full;
|
@apply w-full;
|
||||||
@apply border;
|
@apply border;
|
||||||
@apply border-dividerLight;
|
@apply border-dividerLight;
|
||||||
|
@apply z-5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -36,88 +36,92 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<SmartAceEditor
|
<div ref="jsonResponse" class="w-full block"></div>
|
||||||
:value="jsonBodyText"
|
|
||||||
:lang="'json'"
|
|
||||||
:provide-outline="true"
|
|
||||||
:options="{
|
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 16,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
readOnly: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
styles="border-b border-dividerLight"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { computed, ref, useContext } from "@nuxtjs/composition-api"
|
||||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import "codemirror/mode/javascript/javascript"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
mixins: [TextContentRendererMixin],
|
response: HoppRESTResponse
|
||||||
props: {
|
}>()
|
||||||
response: { type: Object, default: () => {} },
|
|
||||||
},
|
const {
|
||||||
data() {
|
$toast,
|
||||||
return {
|
app: { i18n },
|
||||||
downloadIcon: "download",
|
} = useContext()
|
||||||
copyIcon: "copy",
|
const t = i18n.t.bind(i18n)
|
||||||
}
|
|
||||||
},
|
const responseBodyText = computed(() => {
|
||||||
computed: {
|
if (
|
||||||
jsonBodyText() {
|
props.response.type === "loading" ||
|
||||||
try {
|
props.response.type === "network_fail"
|
||||||
return JSON.stringify(JSON.parse(this.responseBodyText), null, 2)
|
)
|
||||||
} catch (e) {
|
return ""
|
||||||
// Most probs invalid JSON was returned, so drop prettification (should we warn ?)
|
if (typeof props.response.body === "string") return props.response.body
|
||||||
return this.responseBodyText
|
else {
|
||||||
}
|
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||||
},
|
// HACK: Temporary trailing null character issue from the extension fix
|
||||||
responseType() {
|
return res.replace(/\0+$/, "")
|
||||||
return (
|
}
|
||||||
this.response.headers.find(
|
|
||||||
(h) => h.key.toLowerCase() === "content-type"
|
|
||||||
).value || ""
|
|
||||||
)
|
|
||||||
.split(";")[0]
|
|
||||||
.toLowerCase()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
downloadResponse() {
|
|
||||||
const dataToWrite = this.responseBodyText
|
|
||||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
|
||||||
const a = document.createElement("a")
|
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
a.href = url
|
|
||||||
// TODO get uri from meta
|
|
||||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
this.downloadIcon = "check"
|
|
||||||
this.$toast.success(this.$t("state.download_started"), {
|
|
||||||
icon: "downloading",
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.removeChild(a)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
this.downloadIcon = "download"
|
|
||||||
}, 1000)
|
|
||||||
},
|
|
||||||
copyResponse() {
|
|
||||||
copyToClipboard(this.responseBodyText)
|
|
||||||
this.copyIcon = "check"
|
|
||||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
|
||||||
icon: "content_paste",
|
|
||||||
})
|
|
||||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const downloadIcon = ref("download")
|
||||||
|
const copyIcon = ref("copy")
|
||||||
|
|
||||||
|
const jsonBodyText = computed(() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(responseBodyText.value), null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
// Most probs invalid JSON was returned, so drop prettification (should we warn ?)
|
||||||
|
return responseBodyText.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const jsonResponse = ref<any | null>(null)
|
||||||
|
|
||||||
|
useCodemirror(jsonResponse, jsonBodyText, {
|
||||||
|
extendedEditorConfig: {
|
||||||
|
mode: "javascript",
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
linter: null,
|
||||||
|
completer: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadResponse = () => {
|
||||||
|
const dataToWrite = responseBodyText.value
|
||||||
|
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||||
|
const a = document.createElement("a")
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
|
// TODO get uri from meta
|
||||||
|
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
downloadIcon.value = "check"
|
||||||
|
$toast.success(t("state.download_started").toString(), {
|
||||||
|
icon: "downloading",
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
downloadIcon.value = "download"
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyResponse = () => {
|
||||||
|
copyToClipboard(responseBodyText.value)
|
||||||
|
copyIcon.value = "check"
|
||||||
|
$toast.success(t("state.copied_to_clipboard").toString(), {
|
||||||
|
icon: "content_paste",
|
||||||
|
})
|
||||||
|
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -36,79 +36,92 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<SmartAceEditor
|
<div ref="rawResponse" class="w-full block"></div>
|
||||||
:value="responseBodyText"
|
|
||||||
:lang="'plain_text'"
|
|
||||||
:options="{
|
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 16,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
readOnly: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
styles="border-b border-dividerLight"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { ref, useContext, computed } from "@nuxtjs/composition-api"
|
||||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import "codemirror/mode/yaml/yaml"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
mixins: [TextContentRendererMixin],
|
response: HoppRESTResponse
|
||||||
props: {
|
}>()
|
||||||
response: { type: Object, default: () => {} },
|
|
||||||
},
|
const {
|
||||||
data() {
|
$toast,
|
||||||
return {
|
app: { i18n },
|
||||||
downloadIcon: "download",
|
} = useContext()
|
||||||
copyIcon: "copy",
|
const t = i18n.t.bind(i18n)
|
||||||
}
|
|
||||||
},
|
const responseBodyText = computed(() => {
|
||||||
computed: {
|
if (
|
||||||
responseType() {
|
props.response.type === "loading" ||
|
||||||
return (
|
props.response.type === "network_fail"
|
||||||
this.response.headers.find(
|
)
|
||||||
(h) => h.key.toLowerCase() === "content-type"
|
return ""
|
||||||
).value || ""
|
if (typeof props.response.body === "string") return props.response.body
|
||||||
)
|
else {
|
||||||
.split(";")[0]
|
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||||
.toLowerCase()
|
// HACK: Temporary trailing null character issue from the extension fix
|
||||||
},
|
return res.replace(/\0+$/, "")
|
||||||
},
|
}
|
||||||
methods: {
|
|
||||||
downloadResponse() {
|
|
||||||
const dataToWrite = this.responseBodyText
|
|
||||||
const file = new Blob([dataToWrite], { type: this.responseType })
|
|
||||||
const a = document.createElement("a")
|
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
a.href = url
|
|
||||||
// TODO get uri from meta
|
|
||||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
this.downloadIcon = "check"
|
|
||||||
this.$toast.success(this.$t("state.download_started"), {
|
|
||||||
icon: "downloading",
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.removeChild(a)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
this.downloadIcon = "download"
|
|
||||||
}, 1000)
|
|
||||||
},
|
|
||||||
copyResponse() {
|
|
||||||
copyToClipboard(this.responseBodyText)
|
|
||||||
this.copyIcon = "check"
|
|
||||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
|
||||||
icon: "content_paste",
|
|
||||||
})
|
|
||||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const downloadIcon = ref("download")
|
||||||
|
const copyIcon = ref("copy")
|
||||||
|
|
||||||
|
const responseType = computed(() => {
|
||||||
|
return (
|
||||||
|
props.response.headers.find((h) => h.key.toLowerCase() === "content-type")
|
||||||
|
.value || ""
|
||||||
|
)
|
||||||
|
.split(";")[0]
|
||||||
|
.toLowerCase()
|
||||||
|
})
|
||||||
|
|
||||||
|
const rawResponse = ref<any | null>(null)
|
||||||
|
|
||||||
|
useCodemirror(rawResponse, responseBodyText, {
|
||||||
|
extendedEditorConfig: {
|
||||||
|
mode: "text/x-yaml",
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
linter: null,
|
||||||
|
completer: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadResponse = () => {
|
||||||
|
const dataToWrite = responseBodyText.value
|
||||||
|
const file = new Blob([dataToWrite], { type: responseType.value })
|
||||||
|
const a = document.createElement("a")
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
|
// TODO get uri from meta
|
||||||
|
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
downloadIcon.value = "check"
|
||||||
|
$toast.success(t("state.download_started").toString(), {
|
||||||
|
icon: "downloading",
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
downloadIcon.value = "download"
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyResponse = () => {
|
||||||
|
copyToClipboard(responseBodyText.value)
|
||||||
|
copyIcon.value = "check"
|
||||||
|
$toast.success(t("state.copied_to_clipboard").toString(), {
|
||||||
|
icon: "content_paste",
|
||||||
|
})
|
||||||
|
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -36,79 +36,92 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<SmartAceEditor
|
<div ref="xmlResponse" class="w-full block"></div>
|
||||||
:value="responseBodyText"
|
|
||||||
:lang="'xml'"
|
|
||||||
:options="{
|
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 16,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
readOnly: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
styles="border-b border-dividerLight"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { computed, ref, useContext } from "@nuxtjs/composition-api"
|
||||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import "codemirror/mode/xml/xml"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
mixins: [TextContentRendererMixin],
|
response: HoppRESTResponse
|
||||||
props: {
|
}>()
|
||||||
response: { type: Object, default: () => {} },
|
|
||||||
},
|
const {
|
||||||
data() {
|
$toast,
|
||||||
return {
|
app: { i18n },
|
||||||
copyIcon: "copy",
|
} = useContext()
|
||||||
downloadIcon: "download",
|
const t = i18n.t.bind(i18n)
|
||||||
}
|
|
||||||
},
|
const responseBodyText = computed(() => {
|
||||||
computed: {
|
if (
|
||||||
responseType() {
|
props.response.type === "loading" ||
|
||||||
return (
|
props.response.type === "network_fail"
|
||||||
this.response.headers.find(
|
)
|
||||||
(h) => h.key.toLowerCase() === "content-type"
|
return ""
|
||||||
).value || ""
|
if (typeof props.response.body === "string") return props.response.body
|
||||||
)
|
else {
|
||||||
.split(";")[0]
|
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||||
.toLowerCase()
|
// HACK: Temporary trailing null character issue from the extension fix
|
||||||
},
|
return res.replace(/\0+$/, "")
|
||||||
},
|
}
|
||||||
methods: {
|
|
||||||
downloadResponse() {
|
|
||||||
const dataToWrite = this.responseBodyText
|
|
||||||
const file = new Blob([dataToWrite], { type: this.responseType })
|
|
||||||
const a = document.createElement("a")
|
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
a.href = url
|
|
||||||
// TODO get uri from meta
|
|
||||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
this.downloadIcon = "check"
|
|
||||||
this.$toast.success(this.$t("state.download_started"), {
|
|
||||||
icon: "downloading",
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.removeChild(a)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
this.downloadIcon = "download"
|
|
||||||
}, 1000)
|
|
||||||
},
|
|
||||||
copyResponse() {
|
|
||||||
copyToClipboard(this.responseBodyText)
|
|
||||||
this.copyIcon = "check"
|
|
||||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
|
||||||
icon: "content_paste",
|
|
||||||
})
|
|
||||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const downloadIcon = ref("download")
|
||||||
|
const copyIcon = ref("copy")
|
||||||
|
|
||||||
|
const responseType = computed(() => {
|
||||||
|
return (
|
||||||
|
props.response.headers.find((h) => h.key.toLowerCase() === "content-type")
|
||||||
|
.value || ""
|
||||||
|
)
|
||||||
|
.split(";")[0]
|
||||||
|
.toLowerCase()
|
||||||
|
})
|
||||||
|
|
||||||
|
const xmlResponse = ref<any | null>(null)
|
||||||
|
|
||||||
|
useCodemirror(xmlResponse, responseBodyText, {
|
||||||
|
extendedEditorConfig: {
|
||||||
|
mode: "application/xml",
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
linter: null,
|
||||||
|
completer: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadResponse = () => {
|
||||||
|
const dataToWrite = responseBodyText.value
|
||||||
|
const file = new Blob([dataToWrite], { type: responseType.value })
|
||||||
|
const a = document.createElement("a")
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
|
// TODO get uri from meta
|
||||||
|
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
downloadIcon.value = "check"
|
||||||
|
$toast.success(t("state.download_started").toString(), {
|
||||||
|
icon: "downloading",
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
downloadIcon.value = "download"
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyResponse = () => {
|
||||||
|
copyToClipboard(responseBodyText.value)
|
||||||
|
copyIcon.value = "check"
|
||||||
|
$toast.success(t("state.copied_to_clipboard").toString(), {
|
||||||
|
icon: "content_paste",
|
||||||
|
})
|
||||||
|
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const DEFAULT_EDITOR_CONFIG: CodeMirror.EditorConfiguration = {
|
|||||||
extraKeys: {
|
extraKeys: {
|
||||||
"Ctrl-Space": "autocomplete",
|
"Ctrl-Space": "autocomplete",
|
||||||
},
|
},
|
||||||
|
viewportMargin: Infinity,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,6 +121,8 @@ export function useCodemirror(
|
|||||||
|
|
||||||
cm.value = CodeMirror(el.value!, DEFAULT_EDITOR_CONFIG)
|
cm.value = CodeMirror(el.value!, DEFAULT_EDITOR_CONFIG)
|
||||||
|
|
||||||
|
cm.value.setValue(value.value)
|
||||||
|
|
||||||
setTheme()
|
setTheme()
|
||||||
updateEditorConfig()
|
updateEditorConfig()
|
||||||
updateLinterConfig()
|
updateLinterConfig()
|
||||||
@@ -139,7 +142,14 @@ export function useCodemirror(
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Reinitialize if the target ref updates
|
// Reinitialize if the target ref updates
|
||||||
watch(el, initialize)
|
watch(el, () => {
|
||||||
|
if (cm.value) {
|
||||||
|
const parent = cm.value.getWrapperElement()
|
||||||
|
parent.remove()
|
||||||
|
cm.value = null
|
||||||
|
}
|
||||||
|
initialize()
|
||||||
|
})
|
||||||
|
|
||||||
const setTheme = () => {
|
const setTheme = () => {
|
||||||
if (cm.value) {
|
if (cm.value) {
|
||||||
|
|||||||
Reference in New Issue
Block a user