feat: port ace editor to codemirror

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
liyasthomas
2021-09-09 17:47:27 +05:30
parent 8b4535c131
commit 02cf620090
10 changed files with 437 additions and 485 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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