documentation.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <template>
  2. <Splitpanes
  3. class="smart-splitter"
  4. :rtl="SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768"
  5. :class="{
  6. '!flex-row-reverse': SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768,
  7. }"
  8. :horizontal="!(windowInnerWidth.x.value >= 768)"
  9. >
  10. <Pane size="75" min-size="65" class="hide-scrollbar !overflow-auto">
  11. <Splitpanes class="smart-splitter" :horizontal="COLUMN_LAYOUT">
  12. <Pane class="hide-scrollbar !overflow-auto">
  13. <AppSection label="import">
  14. <div class="flex items-start justify-between p-4">
  15. <label>
  16. {{ $t("documentation.generate_message") }}
  17. </label>
  18. <span
  19. class="
  20. bg-accentDark
  21. text-accentContrast
  22. inline-flex
  23. px-2
  24. py-1
  25. rounded
  26. "
  27. >
  28. BETA
  29. </span>
  30. </div>
  31. <div
  32. class="
  33. bg-primary
  34. border-dividerLight
  35. sticky
  36. top-0
  37. z-10
  38. flex
  39. items-start
  40. justify-between
  41. border-b
  42. "
  43. >
  44. <label for="collectionUpload">
  45. <ButtonSecondary
  46. v-tippy="{ theme: 'tooltip' }"
  47. title="JSON"
  48. svg="folder"
  49. class="!rounded-none"
  50. :label="$t('import.collections')"
  51. @click.native="$refs.collectionUpload.click()"
  52. />
  53. </label>
  54. <input
  55. ref="collectionUpload"
  56. class="input"
  57. name="collectionUpload"
  58. type="file"
  59. @change="uploadCollection"
  60. />
  61. <ButtonSecondary
  62. v-tippy="{ theme: 'tooltip' }"
  63. :title="$t('action.clear')"
  64. svg="trash-2"
  65. @click.native="collectionJSON = '[]'"
  66. />
  67. </div>
  68. <textarea-autosize
  69. id="import-curl"
  70. v-model="collectionJSON"
  71. class="p-4 font-mono bg-primary"
  72. autofocus
  73. rows="8"
  74. />
  75. <div
  76. class="
  77. border-t
  78. bg-primary
  79. border-dividerLight
  80. sticky
  81. bottom-0
  82. z-10
  83. flex
  84. items-start
  85. justify-between
  86. p-4
  87. border-b
  88. "
  89. >
  90. <ButtonPrimary
  91. :label="$t('documentation.generate')"
  92. @click.native="getDoc"
  93. />
  94. </div>
  95. </AppSection>
  96. </Pane>
  97. <Pane class="hide-scrollbar !overflow-auto">
  98. <AppSection label="documentation">
  99. <div class="flex flex-col">
  100. <div
  101. v-if="items.length === 0"
  102. class="
  103. flex
  104. text-secondaryLight
  105. flex-col
  106. items-center
  107. justify-center
  108. p-4
  109. "
  110. >
  111. <i class="pb-2 opacity-75 material-icons">topic</i>
  112. <span class="text-center">
  113. {{ $t("helpers.generate_documentation_first") }}
  114. </span>
  115. </div>
  116. <div
  117. v-else
  118. class="
  119. flex
  120. bg-primary
  121. border-dividerLight
  122. sticky
  123. top-0
  124. z-10
  125. flex-1
  126. p-4
  127. border-b
  128. "
  129. >
  130. <span
  131. v-tippy="{ theme: 'tooltip' }"
  132. :title="
  133. !currentUser
  134. ? $t('export.require_github')
  135. : currentUser.provider !== 'github.com'
  136. ? $t('export.require_github')
  137. : 'Beta'
  138. "
  139. >
  140. <ButtonPrimary
  141. :disabled="
  142. !currentUser
  143. ? true
  144. : currentUser.provider !== 'github.com'
  145. ? true
  146. : false
  147. "
  148. :label="$t('export.create_secret_gist')"
  149. @click.native="createDocsGist"
  150. />
  151. </span>
  152. </div>
  153. <div
  154. v-for="(collection, index) in items"
  155. :key="`collection-${index}`"
  156. >
  157. <DocsCollection :collection="collection" />
  158. </div>
  159. </div>
  160. </AppSection>
  161. </Pane>
  162. </Splitpanes>
  163. </Pane>
  164. <Pane
  165. v-if="SIDEBAR"
  166. size="25"
  167. min-size="20"
  168. class="hide-scrollbar !overflow-auto"
  169. >
  170. <aside>
  171. <Collections
  172. :selected="selected"
  173. :doc="true"
  174. @use-collection="useSelectedCollection($event)"
  175. @remove-collection="removeSelectedCollection($event)"
  176. />
  177. </aside>
  178. </Pane>
  179. </Splitpanes>
  180. </template>
  181. <script>
  182. import { defineComponent } from "@nuxtjs/composition-api"
  183. import { Splitpanes, Pane } from "splitpanes"
  184. import "splitpanes/dist/splitpanes.css"
  185. import Mustache from "mustache"
  186. import { currentUser$ } from "~/helpers/fb/auth"
  187. import DocsTemplate from "~/assets/md/docs.md"
  188. import folderContents from "~/assets/md/folderContents.md"
  189. import folderBody from "~/assets/md/folderBody.md"
  190. import { useSetting } from "~/newstore/settings"
  191. import { useReadonlyStream } from "~/helpers/utils/composables"
  192. import useWindowSize from "~/helpers/utils/useWindowSize"
  193. export default defineComponent({
  194. components: { Splitpanes, Pane },
  195. setup() {
  196. return {
  197. windowInnerWidth: useWindowSize(),
  198. SIDEBAR: useSetting("SIDEBAR"),
  199. COLUMN_LAYOUT: useSetting("COLUMN_LAYOUT"),
  200. currentUser: useReadonlyStream(currentUser$, null),
  201. SIDEBAR_ON_LEFT: useSetting("SIDEBAR_ON_LEFT"),
  202. }
  203. },
  204. data() {
  205. return {
  206. collectionJSON: "[]",
  207. items: [],
  208. docsMarkdown: "",
  209. selected: [],
  210. }
  211. },
  212. head() {
  213. return {
  214. title: `${this.$t("navigation.doc")} • Hoppscotch`,
  215. }
  216. },
  217. methods: {
  218. async createDocsGist() {
  219. await this.$axios
  220. .$post(
  221. "https://api.github.com/gists",
  222. {
  223. files: {
  224. "api-docs.md": {
  225. content: this.docsMarkdown,
  226. },
  227. },
  228. },
  229. {
  230. headers: {
  231. Authorization: `token ${this.currentUser.accessToken}`,
  232. Accept: "application/vnd.github.v3+json",
  233. },
  234. }
  235. )
  236. .then((res) => {
  237. this.$toast.success(this.$t("export.gist_created"))
  238. window.open(res.html_url)
  239. })
  240. .catch((e) => {
  241. this.$toast.error(this.$t("error.something_went_wrong"))
  242. console.error(e)
  243. })
  244. },
  245. uploadCollection() {
  246. const file = this.$refs.collectionUpload.files[0]
  247. if (file !== undefined && file !== null) {
  248. const reader = new FileReader()
  249. reader.onload = ({ target }) => {
  250. this.collectionJSON = target.result
  251. }
  252. reader.readAsText(file)
  253. this.$toast.success(this.$t("state.file_imported"))
  254. } else {
  255. this.$toast.error(this.$t("action.choose_file"))
  256. }
  257. this.$refs.collectionUpload.value = ""
  258. },
  259. assignIDs(items, pref, nestingLevel) {
  260. for (let i = 0; i < items.length; ++i) {
  261. items[i].id = `&emsp;${pref}${i + 1}.`
  262. items[i].ref = `${items[i].name.split(" ").join("-")}`
  263. items[i].nestingLevel = nestingLevel
  264. items[i].folders = this.assignIDs(
  265. items[i].folders,
  266. items[i].id,
  267. nestingLevel + "#"
  268. )
  269. for (let j = 0; j < items[i].requests.length; ++j) {
  270. items[i].requests[j].id = `&emsp;${items[i].id}${i + 1}`
  271. items[i].requests[j].ref = `${items[i].requests[j].name
  272. .split(" ")
  273. .join("-")}`
  274. items[i].requests[j].nestingLevel = nestingLevel + "#"
  275. }
  276. }
  277. return items
  278. },
  279. getDoc() {
  280. try {
  281. this.items = JSON.parse(this.collectionJSON)
  282. this.assignIDs(this.items, "", "#")
  283. this.$toast.clear()
  284. this.$toast.success(this.$t("state.docs_generated"))
  285. const docsMarkdown = Mustache.render(
  286. DocsTemplate,
  287. {
  288. collections: this.items,
  289. isHeaders() {
  290. return this.headers.length
  291. },
  292. isParams() {
  293. return this.params.length
  294. },
  295. isAuth() {
  296. return this.auth !== "None"
  297. },
  298. isAuthBasic() {
  299. return this.httpUser && this.httpPassword
  300. },
  301. isRawParams() {
  302. return this.rawParams && this.rawParams !== ""
  303. },
  304. isPreRequestScript() {
  305. return this.preRequestScript && this.preRequestScript !== ""
  306. },
  307. isTestScript() {
  308. return this.testScript && this.testScript !== ""
  309. },
  310. },
  311. {
  312. folderContents,
  313. folderBody,
  314. }
  315. )
  316. this.docsMarkdown = docsMarkdown.replace(/^\s*[\r\n]/gm, "\n\n")
  317. } catch (e) {
  318. console.error(e)
  319. this.$toast.error(this.$t("error.something_went_wrong"))
  320. }
  321. },
  322. useSelectedCollection(collection) {
  323. if (this.selected.find((coll) => coll === collection)) {
  324. return
  325. }
  326. this.selected.push(collection)
  327. const importCollection = JSON.stringify(this.selected, null, 2)
  328. this.collectionJSON = JSON.stringify(
  329. JSON.parse(importCollection),
  330. null,
  331. 2
  332. )
  333. },
  334. removeSelectedCollection(collection) {
  335. this.selected = this.selected.filter((coll) => coll !== collection)
  336. const importCollection = JSON.stringify(this.selected, null, 2)
  337. this.collectionJSON = JSON.stringify(
  338. JSON.parse(importCollection),
  339. null,
  340. 2
  341. )
  342. },
  343. },
  344. })
  345. </script>