PathVariables.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. <template>
  2. <div>
  3. <div
  4. v-if="envExpandError"
  5. class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
  6. >
  7. {{ nestedVars }}
  8. </div>
  9. <div
  10. class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
  11. >
  12. <label class="font-semibold text-secondaryLight"> My Variables </label>
  13. <div class="flex">
  14. <ButtonSecondary
  15. v-tippy="{ theme: 'tooltip' }"
  16. to="https://docs.hoppscotch.io/features/#"
  17. blank
  18. :title="t('app.wiki')"
  19. svg="help-circle"
  20. />
  21. <ButtonSecondary
  22. v-tippy="{ theme: 'tooltip' }"
  23. :title="t('action.clear_all')"
  24. svg="trash-2"
  25. @click.native="clearContent()"
  26. />
  27. <ButtonSecondary
  28. v-tippy="{ theme: 'tooltip' }"
  29. :title="t('add.new')"
  30. svg="plus"
  31. @click.native="addVar"
  32. />
  33. </div>
  34. </div>
  35. <div>
  36. <draggable
  37. v-model="workingVars"
  38. animation="250"
  39. handle=".draggable-handle"
  40. draggable=".draggable-content"
  41. ghost-class="cursor-move"
  42. chosen-class="bg-primaryLight"
  43. drag-class="cursor-grabbing"
  44. >
  45. <div
  46. v-for="(variable, index) in workingVars"
  47. :key="`vari-${variable.id}-${index}`"
  48. class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
  49. >
  50. <span>
  51. <ButtonSecondary
  52. svg="grip-vertical"
  53. class="cursor-auto text-primary hover:text-primary"
  54. :class="{
  55. 'draggable-handle group-hover:text-secondaryLight !cursor-grab':
  56. index !== workingVars?.length - 1,
  57. }"
  58. tabindex="-1"
  59. />
  60. </span>
  61. <SmartEnvInput
  62. v-model="variable.key"
  63. :placeholder="`${t('count.variable', { count: index + 1 })}`"
  64. @change="
  65. updateVar(index, {
  66. id: variable.id,
  67. key: $event,
  68. value: variable.value,
  69. })
  70. "
  71. />
  72. <SmartEnvInput
  73. v-model="variable.value"
  74. :placeholder="`${t('count.value', { count: index + 1 })}`"
  75. @change="
  76. updateVar(index, {
  77. id: variable.id,
  78. key: variable.key,
  79. value: $event,
  80. })
  81. "
  82. />
  83. <span>
  84. <ButtonSecondary
  85. v-tippy="{ theme: 'tooltip' }"
  86. :title="t('action.remove')"
  87. svg="trash"
  88. color="red"
  89. @click.native="deleteVar(index)"
  90. />
  91. </span>
  92. </div>
  93. </draggable>
  94. <div
  95. v-if="workingVars.length === 0"
  96. class="flex flex-col items-center justify-center p-4 text-secondaryLight"
  97. >
  98. <img
  99. :src="`/images/states/${$colorMode.value}/add_files.svg`"
  100. loading="lazy"
  101. class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
  102. :alt="`${t('empty.parameters')}`"
  103. />
  104. <span class="pb-4 text-center">{{ emptyVars }}</span>
  105. <ButtonSecondary
  106. :label="`${t('add.new')}`"
  107. svg="plus"
  108. filled
  109. class="mb-4"
  110. @click.native="addVar"
  111. />
  112. </div>
  113. </div>
  114. </div>
  115. </template>
  116. <script setup lang="ts">
  117. import { computed, Ref, ref, watch } from "@nuxtjs/composition-api"
  118. import { flow, pipe } from "fp-ts/function"
  119. import * as O from "fp-ts/Option"
  120. import * as A from "fp-ts/Array"
  121. import { HoppRESTVar, parseMyVariablesString } from "@hoppscotch/data"
  122. import draggable from "vuedraggable"
  123. import cloneDeep from "lodash/cloneDeep"
  124. import isEqual from "lodash/isEqual"
  125. import * as E from "fp-ts/Either"
  126. import { useI18n, useStream, useToast } from "~/helpers/utils/composables"
  127. import { throwError } from "~/helpers/functional/error"
  128. import { restVars$, setRESTVars } from "~/newstore/RESTSession"
  129. import { objRemoveKey } from "~/helpers/functional/object"
  130. const t = useI18n()
  131. const toast = useToast()
  132. const emptyVars: string = "Add a new variable"
  133. const nestedVars: string = "nested variables greater than 10 levels"
  134. const idTicker = ref(0)
  135. const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
  136. // The functional variables list (the variables actually applied to the session)
  137. const vars = useStream(restVars$, [], setRESTVars) as Ref<HoppRESTVar[]>
  138. // The UI representation of the variables list (has the empty end variable)
  139. const workingVars = ref<Array<HoppRESTVar & { id: number }>>([
  140. {
  141. id: idTicker.value++,
  142. key: "",
  143. value: "",
  144. },
  145. ])
  146. // Rule: Working vars always have last element is always an empty var
  147. watch(workingVars, (varsList) => {
  148. if (varsList.length > 0 && varsList[varsList.length - 1].key !== "") {
  149. workingVars.value.push({
  150. id: idTicker.value++,
  151. key: "",
  152. value: "",
  153. })
  154. }
  155. })
  156. // Sync logic between params and working/bulk params
  157. watch(
  158. vars,
  159. (newVarsList) => {
  160. // Sync should overwrite working params
  161. const filteredWorkingVars: HoppRESTVar[] = pipe(
  162. workingVars.value,
  163. A.filterMap(
  164. flow(
  165. O.fromPredicate((e) => e.key !== ""),
  166. O.map(objRemoveKey("id"))
  167. )
  168. )
  169. )
  170. if (!isEqual(newVarsList, filteredWorkingVars)) {
  171. workingVars.value = pipe(
  172. newVarsList,
  173. A.map((x) => ({ id: idTicker.value++, ...x }))
  174. )
  175. }
  176. },
  177. { immediate: true }
  178. )
  179. watch(workingVars, (newWorkingVars) => {
  180. const fixedVars = pipe(
  181. newWorkingVars,
  182. A.filterMap(
  183. flow(
  184. O.fromPredicate((e) => e.key !== ""),
  185. O.map(objRemoveKey("id"))
  186. )
  187. )
  188. )
  189. if (!isEqual(vars.value, fixedVars)) {
  190. vars.value = cloneDeep(fixedVars)
  191. }
  192. })
  193. const addVar = () => {
  194. workingVars.value.push({
  195. id: idTicker.value++,
  196. key: "",
  197. value: "",
  198. })
  199. }
  200. const updateVar = (index: number, vari: HoppRESTVar & { id: number }) => {
  201. workingVars.value = workingVars.value.map((h, i) => (i === index ? vari : h))
  202. }
  203. const deleteVar = (index: number) => {
  204. const varsBeforeDeletion = cloneDeep(workingVars.value)
  205. if (
  206. !(varsBeforeDeletion.length > 0 && index === varsBeforeDeletion.length - 1)
  207. ) {
  208. if (deletionToast.value) {
  209. deletionToast.value.goAway(0)
  210. deletionToast.value = null
  211. }
  212. deletionToast.value = toast.success(`${t("state.deleted")}`, {
  213. action: [
  214. {
  215. text: `${t("action.undo")}`,
  216. onClick: (_, toastObject) => {
  217. workingVars.value = varsBeforeDeletion
  218. toastObject.goAway(0)
  219. deletionToast.value = null
  220. },
  221. },
  222. ],
  223. onComplete: () => {
  224. deletionToast.value = null
  225. },
  226. })
  227. }
  228. workingVars.value = pipe(
  229. workingVars.value,
  230. A.deleteAt(index),
  231. O.getOrElseW(() => throwError("Working Params Deletion Out of Bounds"))
  232. )
  233. }
  234. const envExpandError = computed(() => {
  235. const variables = pipe(vars.value)
  236. return pipe(
  237. variables,
  238. A.exists(({ value }) => E.isLeft(parseMyVariablesString(value, variables)))
  239. )
  240. })
  241. const clearContent = () => {
  242. // set params list to the initial state
  243. workingVars.value = [
  244. {
  245. id: idTicker.value++,
  246. key: "",
  247. value: "",
  248. },
  249. ]
  250. }
  251. </script>