chore: split app to commons and web (squash commit)

This commit is contained in:
Andrew Bastin
2022-12-02 02:57:46 -05:00
parent fb827e3586
commit 3d004f2322
535 changed files with 1487 additions and 501 deletions

View File

@@ -0,0 +1,54 @@
<template>
<div>
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
{{ t("request.header_list") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="headers"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyIcon"
@click="copyHeaders"
/>
</div>
</div>
<LensesHeadersRendererEntry
v-for="(header, index) in headers"
:key="index"
:header="header"
/>
</div>
</template>
<script setup lang="ts">
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import { HoppRESTHeader } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
headers: Array<HoppRESTHeader>
}>()
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const copyHeaders = () => {
copyToClipboard(JSON.stringify(props.headers))
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
</script>

View File

@@ -0,0 +1,56 @@
<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 justify-between flex-1 min-w-0 py-2 pl-4 transition group-hover:text-secondaryDark"
>
<span class="truncate rounded-sm select-all">
{{ header.value }}
</span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyIcon"
class="hidden group-hover:inline-flex !py-0"
@click="copyHeader(header.value)"
/>
</span>
</div>
</template>
<script setup lang="ts">
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import { refAutoReset } from "@vueuse/core"
import type { HoppRESTHeader } from "@hoppscotch/data"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
const t = useI18n()
const toast = useToast()
defineProps<{
header: HoppRESTHeader
}>()
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const copyHeader = (headerValue: string) => {
copyToClipboard(headerValue)
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
</script>

View File

@@ -0,0 +1,95 @@
<template>
<SmartTabs
v-if="response"
v-model="selectedLensTab"
styles="sticky overflow-x-auto flex-shrink-0 z-10 bg-primary top-lowerPrimaryStickyFold"
>
<SmartTab
v-for="(lens, index) in validLenses"
:id="lens.renderer"
:key="`lens-${index}`"
:label="t(lens.lensName)"
class="flex flex-col flex-1 w-full h-full"
>
<component :is="lens.renderer" :response="response" />
</SmartTab>
<SmartTab
v-if="headerLength"
id="headers"
:label="t('response.headers')"
:info="`${headerLength}`"
class="flex flex-col flex-1"
>
<LensesHeadersRenderer :headers="response.headers" />
</SmartTab>
<SmartTab
id="results"
:label="t('test.results')"
:indicator="
testResults &&
(testResults.expectResults.length ||
testResults.tests.length ||
testResults.envDiff.selected.additions.length ||
testResults.envDiff.selected.updations.length ||
testResults.envDiff.global.updations.length)
? true
: false
"
class="flex flex-col flex-1"
>
<HttpTestResult />
</SmartTab>
</SmartTabs>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import { getSuitableLenses, getLensRenderers } from "~/helpers/lenses/lenses"
import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { restTestResults$ } from "~/newstore/RESTSession"
export default defineComponent({
components: {
// Lens Renderers
...getLensRenderers(),
},
props: {
response: { type: Object, default: () => ({}) },
},
setup() {
const testResults = useReadonlyStream(restTestResults$, null)
return {
testResults,
t: useI18n(),
}
},
data() {
return {
selectedLensTab: "",
}
},
computed: {
headerLength() {
if (!this.response || !this.response.headers) return 0
return Object.keys(this.response.headers).length
},
validLenses() {
if (!this.response) return []
return getSuitableLenses(this.response)
},
},
watch: {
validLenses: {
handler(newValue) {
if (newValue.length === 0) return
this.selectedLensTab = newValue[0].renderer
},
immediate: true,
},
},
})
</script>

View File

@@ -0,0 +1,129 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
{{ t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${
previewEnabled ? t('hide.preview') : t('response.preview_html')
} <kbd>${getSpecialKey()}</kbd><kbd>Shift</kbd><kbd>P</kbd>`"
:icon="!previewEnabled ? IconEye : IconEyeOff"
@click.prevent="togglePreview"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.download_file'
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
:icon="downloadIcon"
@click="downloadResponse"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.copy'
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
:icon="copyIcon"
@click="copyResponse"
/>
</div>
</div>
<div
v-show="!previewEnabled"
ref="htmlResponse"
class="flex flex-col flex-1"
></div>
<iframe
v-show="previewEnabled"
ref="previewFrame"
class="covers-response"
src="about:blank"
loading="lazy"
sandbox=""
></iframe>
</div>
</template>
<script setup lang="ts">
import IconWrapText from "~icons/lucide/wrap-text"
import IconEye from "~icons/lucide/eye"
import IconEyeOff from "~icons/lucide/eye-off"
import { ref, reactive } from "vue"
import {
usePreview,
useResponseBody,
useCopyResponse,
useDownloadResponse,
} from "@composables/lens-actions"
import { useCodemirror } from "@composables/codemirror"
import { useI18n } from "@composables/i18n"
import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
const t = useI18n()
const props = defineProps<{
response: HoppRESTResponse & { type: "success" | "fail" }
}>()
const htmlResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
const { responseBodyText } = useResponseBody(props.response)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"text/html",
responseBodyText
)
const { previewFrame, previewEnabled, togglePreview } = usePreview(
false,
responseBodyText
)
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
useCodemirror(
htmlResponse,
responseBodyText,
reactive({
extendedEditorConfig: {
mode: "htmlmixed",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: true,
})
)
defineActionHandler("response.preview.toggle", () => togglePreview())
defineActionHandler("response.file.download", () => downloadResponse())
defineActionHandler("response.copy", () => copyResponse())
</script>
<style lang="scss" scoped>
.covers-response {
@apply bg-white;
@apply h-full;
@apply w-full;
@apply border;
@apply border-dividerLight;
@apply z-5;
}
</style>

View File

@@ -0,0 +1,102 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
{{ t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.download_file'
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
:icon="downloadIcon"
@click="downloadResponse"
/>
</div>
</div>
<img
class="flex max-w-full border-b border-dividerLight"
:src="imageSource"
loading="lazy"
:alt="imageSource"
/>
</div>
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { computed, onMounted, ref, watch } from "vue"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { useDownloadResponse } from "~/composables/lens-actions"
import { flow, pipe } from "fp-ts/function"
import * as S from "fp-ts/string"
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import { objFieldMatches } from "~/helpers/functional/object"
const t = useI18n()
const props = defineProps<{
response: HoppRESTResponse & { type: "success" | "fail" }
}>()
const imageSource = ref("")
const responseType = computed(() =>
pipe(
props.response,
O.fromPredicate(objFieldMatches("type", ["fail", "success"] as const)),
O.chain(
// Try getting content-type
flow(
(res) => res.headers,
A.findFirst((h) => h.key.toLowerCase() === "content-type"),
O.map(flow((h) => h.value, S.split(";"), RNEA.head, S.toLowerCase))
)
),
O.getOrElse(() => "text/plain")
)
)
const { downloadIcon, downloadResponse } = useDownloadResponse(
responseType.value,
computed(() => props.response.body)
)
watch(props.response, () => {
imageSource.value = ""
const buf = props.response.body
const bytes = new Uint8Array(buf)
const blob = new Blob([bytes.buffer])
const reader = new FileReader()
reader.onload = ({ target }) => {
// target.result will always be string because we're using FileReader.readAsDataURL
imageSource.value = target!.result as string
}
reader.readAsDataURL(blob)
})
onMounted(() => {
imageSource.value = ""
const buf = props.response.body
const bytes = new Uint8Array(buf)
const blob = new Blob([bytes.buffer])
const reader = new FileReader()
reader.onload = ({ target }) => {
// target.result will always be string because we're using FileReader.readAsDataURL
imageSource.value = target!.result as string
}
reader.readAsDataURL(blob)
})
defineActionHandler("response.file.download", () => downloadResponse())
</script>

View File

@@ -0,0 +1,384 @@
<template>
<div
v-if="response.type === 'success' || response.type === 'fail'"
class="flex flex-col flex-1"
>
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
{{ t("response.body") }}
</label>
<div class="flex items-center">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.filter')"
:icon="IconFilter"
:class="{ '!text-accent': toggleFilter }"
@click.prevent="toggleFilterState"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.download_file'
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
:icon="downloadIcon"
@click="downloadResponse"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.copy'
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
:icon="copyIcon"
@click="copyResponse"
/>
</div>
</div>
<div
v-if="toggleFilter"
class="sticky z-10 flex flex-shrink-0 overflow-x-auto border-b bg-primary top-lowerTertiaryStickyFold border-dividerLight"
>
<div
class="inline-flex items-center flex-1 bg-primaryLight border-divider text-secondaryDark"
>
<span class="inline-flex items-center flex-1 px-4">
<icon-lucide-search class="w-4 h-4 text-secondaryLight" />
<input
v-model="filterQueryText"
v-focus
class="input !border-0 !px-2"
:placeholder="`${t('response.filter_response_body')}`"
type="text"
/>
</span>
<div
v-if="filterResponseError"
class="flex items-center justify-center px-2 py-1 rounded text-tiny text-accentContrast"
:class="{
'bg-red-500':
filterResponseError.type === 'JSON_PARSE_FAILED' ||
filterResponseError.type === 'JSON_PATH_QUERY_ERROR',
'bg-amber-500': filterResponseError.type === 'RESPONSE_EMPTY',
}"
>
<icon-lucide-info class="svg-icons mr-1.5" />
<span>{{ filterResponseError.error }}</span>
</div>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="t('app.wiki')"
:icon="IconHelpCircle"
to="https://github.com/JSONPath-Plus/JSONPath"
blank
/>
</div>
</div>
<div ref="jsonResponse" class="flex flex-col flex-1 h-auto h-full"></div>
<div
v-if="outlinePath"
class="sticky bottom-0 z-10 flex flex-shrink-0 px-2 overflow-auto overflow-x-auto border-t bg-primaryLight border-dividerLight flex-nowrap"
>
<div
v-for="(item, index) in outlinePath"
:key="`item-${index}`"
class="flex items-center"
>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions[index].focus()"
>
<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 #content="{ hide }">
<div
v-if="item.kind === 'ArrayMember' || item.kind === 'ObjectMember'"
>
<div
v-if="item.kind === 'ArrayMember'"
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<SmartItem
v-for="(arrayMember, astIndex) in item.astParent.values"
:key="`ast-${astIndex}`"
:label="`${astIndex}`"
@click="
() => {
jumpCursor(arrayMember)
hide()
}
"
/>
</div>
<div
v-if="item.kind === 'ObjectMember'"
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<SmartItem
v-for="(objectMember, astIndex) in item.astParent.members"
:key="`ast-${astIndex}`"
:label="objectMember.key.value"
@click="
() => {
jumpCursor(objectMember)
hide()
}
"
/>
</div>
</div>
<div
v-if="item.kind === 'RootObject'"
ref="tippyActions"
class="flex flex-col"
>
<SmartItem
label="{}"
@click="
() => {
jumpCursor(item.astValue)
hide()
}
"
/>
</div>
<div
v-if="item.kind === 'RootArray'"
ref="tippyActions"
class="flex flex-col"
>
<SmartItem
label="[]"
@click="
() => {
jumpCursor(item.astValue)
hide()
}
"
/>
</div>
</template>
</tippy>
<icon-lucide-chevron-right
v-if="index + 1 !== outlinePath.length"
class="opacity-50 text-secondaryLight svg-icons"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import IconWrapText from "~icons/lucide/wrap-text"
import IconFilter from "~icons/lucide/filter"
import IconHelpCircle from "~icons/lucide/help-circle"
import * as LJSON from "lossless-json"
import * as O from "fp-ts/Option"
import * as E from "fp-ts/Either"
import { pipe } from "fp-ts/function"
import { computed, ref, reactive } from "vue"
import { JSONPath } from "jsonpath-plus"
import { useCodemirror } from "@composables/codemirror"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
import { getJSONOutlineAtPos } from "~/helpers/newOutline"
import {
convertIndexToLineCh,
convertLineChToIndex,
} from "~/helpers/editor/utils"
import { useI18n } from "@composables/i18n"
import {
useCopyResponse,
useResponseBody,
useDownloadResponse,
} from "@composables/lens-actions"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
const t = useI18n()
const props = defineProps<{
response: HoppRESTResponse
}>()
const { responseBodyText } = useResponseBody(props.response)
const toggleFilter = ref(false)
const filterQueryText = ref("")
type BodyParseError =
| { type: "JSON_PARSE_FAILED" }
| { type: "JSON_PATH_QUERY_FAILED"; error: Error }
const responseJsonObject = computed(() =>
pipe(
responseBodyText.value,
E.tryCatchK(
LJSON.parse,
(): BodyParseError => ({ type: "JSON_PARSE_FAILED" })
)
)
)
const jsonResponseBodyText = computed(() => {
if (filterQueryText.value.length > 0) {
return pipe(
responseJsonObject.value,
E.chain((parsedJSON) =>
E.tryCatch(
() =>
JSONPath({
path: filterQueryText.value,
json: parsedJSON,
}) as undefined,
(err): BodyParseError => ({
type: "JSON_PATH_QUERY_FAILED",
error: err as Error,
})
)
),
E.map(JSON.stringify)
)
} else {
return E.right(responseBodyText.value)
}
})
const jsonBodyText = computed(() =>
pipe(
jsonResponseBodyText.value,
E.getOrElse(() => responseBodyText.value),
O.tryCatchK(LJSON.parse),
O.map((val) => LJSON.stringify(val, undefined, 2)),
O.getOrElse(() => responseBodyText.value)
)
)
const ast = computed(() =>
pipe(
jsonBodyText.value,
O.tryCatchK(jsonParse),
O.getOrElseW(() => null)
)
)
const filterResponseError = computed(() =>
pipe(
jsonResponseBodyText.value,
E.match(
(e) => {
switch (e.type) {
case "JSON_PATH_QUERY_FAILED":
return { type: "JSON_PATH_QUERY_ERROR", error: e.error.message }
case "JSON_PARSE_FAILED":
return {
type: "JSON_PARSE_FAILED",
error: t("error.json_parsing_failed").toString(),
}
}
},
(result) =>
result === "[]"
? {
type: "RESPONSE_EMPTY",
error: t("error.no_results_found").toString(),
}
: undefined
)
)
)
const { copyIcon, copyResponse } = useCopyResponse(jsonBodyText)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"application/json",
jsonBodyText
)
// Template refs
const tippyActions = ref<any | null>(null)
const jsonResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
const { cursor } = useCodemirror(
jsonResponse,
jsonBodyText,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: true,
})
)
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)
)
)
const toggleFilterState = () => {
filterQueryText.value = ""
toggleFilter.value = !toggleFilter.value
}
defineActionHandler("response.file.download", () => downloadResponse())
defineActionHandler("response.copy", () => copyResponse())
</script>
<style lang="scss" scoped>
.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;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
{{ t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.download_file'
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
:icon="downloadIcon"
@click="downloadResponse"
/>
</div>
</div>
<vue-pdf-embed
:source="pdfsrc"
class="flex flex-1 overflow-auto border-b border-dividerLight"
type="application/pdf"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue"
import VuePdfEmbed from "vue-pdf-embed"
import { useI18n } from "@composables/i18n"
import { useDownloadResponse } from "@composables/lens-actions"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
const t = useI18n()
const props = defineProps<{
response: HoppRESTResponse & {
type: "success" | "fail"
}
}>()
const pdfsrc = computed(() =>
URL.createObjectURL(
new Blob([props.response.body], {
type: "application/pdf",
})
)
)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"application/pdf",
computed(() => props.response.body)
)
defineActionHandler("response.file.download", () => downloadResponse())
</script>

View File

@@ -0,0 +1,119 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
{{ t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.download_file'
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
:icon="downloadIcon"
@click="downloadResponse"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.copy'
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
:icon="copyIcon"
@click="copyResponse"
/>
</div>
</div>
<div ref="rawResponse" class="flex flex-col flex-1"></div>
</div>
</template>
<script setup lang="ts">
import IconWrapText from "~icons/lucide/wrap-text"
import { ref, computed, reactive } from "vue"
import { flow, pipe } from "fp-ts/function"
import * as S from "fp-ts/string"
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import { useCodemirror } from "@composables/codemirror"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { useI18n } from "@composables/i18n"
import {
useResponseBody,
useDownloadResponse,
useCopyResponse,
} from "@composables/lens-actions"
import { objFieldMatches } from "~/helpers/functional/object"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
const t = useI18n()
const props = defineProps<{
response: HoppRESTResponse & { type: "success" | "fail" }
}>()
const { responseBodyText } = useResponseBody(props.response)
const rawResponseBody = computed(() =>
props.response.type === "fail" || props.response.type === "success"
? props.response.body
: new ArrayBuffer(0)
)
const responseType = computed(() =>
pipe(
props.response,
O.fromPredicate(objFieldMatches("type", ["fail", "success"] as const)),
O.chain(
// Try getting content-type
flow(
(res) => res.headers,
A.findFirst((h) => h.key.toLowerCase() === "content-type"),
O.map(flow((h) => h.value, S.split(";"), RNEA.head, S.toLowerCase))
)
),
O.getOrElse(() => "text/plain")
)
)
const { downloadIcon, downloadResponse } = useDownloadResponse(
responseType.value,
rawResponseBody
)
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
const rawResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
rawResponse,
responseBodyText,
reactive({
extendedEditorConfig: {
mode: "text/plain",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: true,
})
)
defineActionHandler("response.file.download", () => downloadResponse())
defineActionHandler("response.copy", () => copyResponse())
</script>

View File

@@ -0,0 +1,113 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
{{ t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.download_file'
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
:icon="downloadIcon"
@click="downloadResponse"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'action.copy'
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
:icon="copyIcon"
@click="copyResponse"
/>
</div>
</div>
<div ref="xmlResponse" class="flex flex-col flex-1"></div>
</div>
</template>
<script setup lang="ts">
import IconWrapText from "~icons/lucide/wrap-text"
import { computed, ref, reactive } from "vue"
import { flow, pipe } from "fp-ts/function"
import * as S from "fp-ts/string"
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import { useCodemirror } from "@composables/codemirror"
import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { useI18n } from "@composables/i18n"
import {
useResponseBody,
useDownloadResponse,
useCopyResponse,
} from "@composables/lens-actions"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { objFieldMatches } from "~/helpers/functional/object"
const t = useI18n()
const props = defineProps<{
response: HoppRESTResponse & { type: "success" | "fail" }
}>()
const { responseBodyText } = useResponseBody(props.response)
const responseType = computed(() =>
pipe(
props.response,
O.fromPredicate(objFieldMatches("type", ["fail", "success"] as const)),
O.chain(
// Try getting content-type
flow(
(res) => res.headers,
A.findFirst((h) => h.key.toLowerCase() === "content-type"),
O.map(flow((h) => h.value, S.split(";"), RNEA.head, S.toLowerCase))
)
),
O.getOrElse(() => "text/plain")
)
)
const { downloadIcon, downloadResponse } = useDownloadResponse(
responseType.value,
responseBodyText
)
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
const xmlResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
xmlResponse,
responseBodyText,
reactive({
extendedEditorConfig: {
mode: "application/xml",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: true,
})
)
defineActionHandler("response.file.download", () => downloadResponse())
defineActionHandler("response.copy", () => copyResponse())
</script>

View File

@@ -0,0 +1,16 @@
export default {
props: {
response: {},
},
computed: {
responseBodyText() {
if (typeof this.response.body === "string") return this.response.body
else {
const res = new TextDecoder("utf-8").decode(this.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
}
},
},
}