documentation.vue 10 KB

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