JSONLensRenderer.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <template>
  2. <div>
  3. <div
  4. class="bg-primary border-b border-dividerLight flex flex-1 top-lowerSecondaryStickyFold pl-4 z-10 sticky items-center justify-between"
  5. >
  6. <label class="font-semibold text-secondaryLight">{{
  7. t("response.body")
  8. }}</label>
  9. <div class="flex">
  10. <ButtonSecondary
  11. v-if="response.body"
  12. v-tippy="{ theme: 'tooltip' }"
  13. :title="t('state.linewrap')"
  14. :class="{ '!text-accent': linewrapEnabled }"
  15. svg="wrap-text"
  16. @click.native.prevent="linewrapEnabled = !linewrapEnabled"
  17. />
  18. <ButtonSecondary
  19. v-if="response.body"
  20. ref="downloadResponse"
  21. v-tippy="{ theme: 'tooltip' }"
  22. :title="t('action.download_file')"
  23. :svg="downloadIcon"
  24. @click.native="downloadResponse"
  25. />
  26. <ButtonSecondary
  27. v-if="response.body"
  28. ref="copyResponse"
  29. v-tippy="{ theme: 'tooltip' }"
  30. :title="t('action.copy')"
  31. :svg="copyIcon"
  32. @click.native="copyResponse"
  33. />
  34. </div>
  35. </div>
  36. <div ref="jsonResponse"></div>
  37. <div
  38. v-if="outlinePath"
  39. class="bg-primaryLight border-t border-dividerLight flex flex-nowrap flex-1 px-2 bottom-0 z-10 sticky overflow-auto hide-scrollbar"
  40. >
  41. <div
  42. v-for="(item, index) in outlinePath"
  43. :key="`item-${index}`"
  44. class="flex items-center"
  45. >
  46. <tippy
  47. ref="outlineOptions"
  48. interactive
  49. trigger="click"
  50. theme="popover"
  51. arrow
  52. >
  53. <template #trigger>
  54. <div v-if="item.kind === 'RootObject'" class="outline">{}</div>
  55. <div v-if="item.kind === 'RootArray'" class="outline">[]</div>
  56. <div v-if="item.kind === 'ArrayMember'" class="outline">
  57. {{ item.index }}
  58. </div>
  59. <div v-if="item.kind === 'ObjectMember'" class="outline">
  60. {{ item.name }}
  61. </div>
  62. </template>
  63. <div
  64. v-if="item.kind === 'ArrayMember' || item.kind === 'ObjectMember'"
  65. >
  66. <div v-if="item.kind === 'ArrayMember'" class="flex flex-col">
  67. <SmartItem
  68. v-for="(arrayMember, astIndex) in item.astParent.values"
  69. :key="`ast-${astIndex}`"
  70. :label="`${astIndex}`"
  71. @click.native="
  72. () => {
  73. jumpCursor(arrayMember)
  74. outlineOptions[index].tippy().hide()
  75. }
  76. "
  77. />
  78. </div>
  79. <div v-if="item.kind === 'ObjectMember'" class="flex flex-col">
  80. <SmartItem
  81. v-for="(objectMember, astIndex) in item.astParent.members"
  82. :key="`ast-${astIndex}`"
  83. :label="objectMember.key.value"
  84. @click.native="
  85. () => {
  86. jumpCursor(objectMember)
  87. outlineOptions[index].tippy().hide()
  88. }
  89. "
  90. />
  91. </div>
  92. </div>
  93. <div v-if="item.kind === 'RootObject'" class="flex flex-col">
  94. <SmartItem
  95. label="{}"
  96. @click.native="
  97. () => {
  98. jumpCursor(item.astValue)
  99. outlineOptions[index].tippy().hide()
  100. }
  101. "
  102. />
  103. </div>
  104. <div v-if="item.kind === 'RootArray'" class="flex flex-col">
  105. <SmartItem
  106. label="[]"
  107. @click.native="
  108. () => {
  109. jumpCursor(item.astValue)
  110. outlineOptions[index].tippy().hide()
  111. }
  112. "
  113. />
  114. </div>
  115. </tippy>
  116. <i
  117. v-if="index + 1 !== outlinePath.length"
  118. class="text-secondaryLight opacity-50 material-icons"
  119. >chevron_right</i
  120. >
  121. </div>
  122. </div>
  123. </div>
  124. </template>
  125. <script setup lang="ts">
  126. import { computed, ref, reactive } from "@nuxtjs/composition-api"
  127. import { useCodemirror } from "~/helpers/editor/codemirror"
  128. import { copyToClipboard } from "~/helpers/utils/clipboard"
  129. import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
  130. import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
  131. import { getJSONOutlineAtPos } from "~/helpers/newOutline"
  132. import {
  133. convertIndexToLineCh,
  134. convertLineChToIndex,
  135. } from "~/helpers/editor/utils"
  136. import { useI18n, useToast } from "~/helpers/utils/composables"
  137. const t = useI18n()
  138. const props = defineProps<{
  139. response: HoppRESTResponse
  140. }>()
  141. const toast = useToast()
  142. const responseBodyText = computed(() => {
  143. if (
  144. props.response.type === "loading" ||
  145. props.response.type === "network_fail"
  146. )
  147. return ""
  148. if (typeof props.response.body === "string") return props.response.body
  149. else {
  150. const res = new TextDecoder("utf-8").decode(props.response.body)
  151. // HACK: Temporary trailing null character issue from the extension fix
  152. return res.replace(/\0+$/, "")
  153. }
  154. })
  155. const downloadIcon = ref("download")
  156. const copyIcon = ref("copy")
  157. const jsonBodyText = computed(() => {
  158. try {
  159. return JSON.stringify(JSON.parse(responseBodyText.value), null, 2)
  160. } catch (e) {
  161. // Most probs invalid JSON was returned, so drop prettification (should we warn ?)
  162. return responseBodyText.value
  163. }
  164. })
  165. const ast = computed(() => {
  166. try {
  167. return jsonParse(jsonBodyText.value)
  168. } catch (_: any) {
  169. return null
  170. }
  171. })
  172. const outlineOptions = ref<any | null>(null)
  173. const jsonResponse = ref<any | null>(null)
  174. const linewrapEnabled = ref(true)
  175. const { cursor } = useCodemirror(
  176. jsonResponse,
  177. jsonBodyText,
  178. reactive({
  179. extendedEditorConfig: {
  180. mode: "application/ld+json",
  181. readOnly: true,
  182. lineWrapping: linewrapEnabled,
  183. },
  184. linter: null,
  185. completer: null,
  186. })
  187. )
  188. const jumpCursor = (ast: JSONValue | JSONObjectMember) => {
  189. const pos = convertIndexToLineCh(jsonBodyText.value, ast.start)
  190. pos.line--
  191. cursor.value = pos
  192. }
  193. const downloadResponse = () => {
  194. const dataToWrite = responseBodyText.value
  195. const file = new Blob([dataToWrite], { type: "application/json" })
  196. const a = document.createElement("a")
  197. const url = URL.createObjectURL(file)
  198. a.href = url
  199. // TODO get uri from meta
  200. a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
  201. document.body.appendChild(a)
  202. a.click()
  203. downloadIcon.value = "check"
  204. toast.success(`${t("state.download_started")}`)
  205. setTimeout(() => {
  206. document.body.removeChild(a)
  207. URL.revokeObjectURL(url)
  208. downloadIcon.value = "download"
  209. }, 1000)
  210. }
  211. const outlinePath = computed(() => {
  212. if (ast.value) {
  213. return getJSONOutlineAtPos(
  214. ast.value,
  215. convertLineChToIndex(jsonBodyText.value, cursor.value)
  216. )
  217. } else return null
  218. })
  219. const copyResponse = () => {
  220. copyToClipboard(responseBodyText.value)
  221. copyIcon.value = "check"
  222. toast.success(`${t("state.copied_to_clipboard")}`)
  223. setTimeout(() => (copyIcon.value = "copy"), 1000)
  224. }
  225. </script>
  226. <style lang="scss" scoped>
  227. .outline {
  228. @apply cursor-pointer;
  229. @apply flex-grow-0 flex-shrink-0;
  230. @apply text-secondaryLight;
  231. @apply inline-flex;
  232. @apply items-center;
  233. @apply px-2;
  234. @apply py-1;
  235. @apply transition;
  236. @apply hover:text-secondary;
  237. }
  238. </style>