index.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. <template>
  2. <AppSection
  3. label="collections"
  4. :class="{ 'rounded border border-divider': saveRequest }"
  5. >
  6. <div
  7. class="
  8. divide-y divide-dividerLight
  9. bg-primary
  10. border-b border-dividerLight
  11. rounded-t
  12. flex flex-col
  13. top-0
  14. z-10
  15. sticky
  16. "
  17. >
  18. <div v-if="!saveRequest" class="search-wrappe">
  19. <input
  20. v-model="filterText"
  21. type="search"
  22. autocomplete="off"
  23. :placeholder="$t('action.search')"
  24. class="bg-transparent flex w-full py-2 pr-2 pl-4"
  25. />
  26. </div>
  27. <CollectionsChooseType
  28. :collections-type="collectionsType"
  29. :show="showTeamCollections"
  30. :doc="doc"
  31. @update-collection-type="updateCollectionType"
  32. @update-selected-team="updateSelectedTeam"
  33. />
  34. <div class="flex flex-1 justify-between">
  35. <ButtonSecondary
  36. v-if="
  37. collectionsType.type == 'team-collections' &&
  38. (collectionsType.selectedTeam == undefined ||
  39. collectionsType.selectedTeam.myRole == 'VIEWER')
  40. "
  41. v-tippy="{ theme: 'tooltip' }"
  42. disabled
  43. class="!rounded-none"
  44. svg="plus"
  45. :title="$t('team.no_access')"
  46. :label="$t('action.new')"
  47. />
  48. <ButtonSecondary
  49. v-else
  50. svg="plus"
  51. :label="$t('action.new')"
  52. class="!rounded-none"
  53. @click.native="displayModalAdd(true)"
  54. />
  55. <span class="flex">
  56. <ButtonSecondary
  57. v-tippy="{ theme: 'tooltip' }"
  58. to="https://docs.hoppscotch.io/features/collections"
  59. blank
  60. :title="$t('app.wiki')"
  61. svg="help-circle"
  62. />
  63. <ButtonSecondary
  64. v-if="!saveRequest"
  65. v-tippy="{ theme: 'tooltip' }"
  66. :disabled="
  67. collectionsType.type == 'team-collections' &&
  68. collectionsType.selectedTeam == undefined
  69. "
  70. svg="archive"
  71. :title="$t('modal.import_export')"
  72. @click.native="displayModalImportExport(true)"
  73. />
  74. </span>
  75. </div>
  76. </div>
  77. <div class="flex flex-col">
  78. <component
  79. :is="
  80. collectionsType.type == 'my-collections'
  81. ? 'CollectionsMyCollection'
  82. : 'CollectionsTeamsCollection'
  83. "
  84. v-for="(collection, index) in filteredCollections"
  85. :key="`collection-${index}`"
  86. :collection-index="index"
  87. :collection="collection"
  88. :doc="doc"
  89. :is-filtered="filterText.length > 0"
  90. :selected="selected.some((coll) => coll == collection)"
  91. :save-request="saveRequest"
  92. :collections-type="collectionsType"
  93. :picked="picked"
  94. @edit-collection="editCollection(collection, index)"
  95. @add-folder="addFolder($event)"
  96. @edit-folder="editFolder($event)"
  97. @edit-request="editRequest($event)"
  98. @duplicate-request="duplicateRequest($event)"
  99. @update-team-collections="updateTeamCollections"
  100. @select-collection="$emit('use-collection', collection)"
  101. @unselect-collection="$emit('remove-collection', collection)"
  102. @select="$emit('select', $event)"
  103. @expand-collection="expandCollection"
  104. @remove-collection="removeCollection"
  105. @remove-request="removeRequest"
  106. />
  107. </div>
  108. <div
  109. v-if="filteredCollections.length === 0 && filterText.length === 0"
  110. class="flex flex-col text-secondaryLight p-4 items-center justify-center"
  111. >
  112. <img
  113. :src="`/images/states/${$colorMode.value}/pack.svg`"
  114. loading="lazy"
  115. class="flex-col my-4 object-contain object-center h-16 w-16 inline-flex"
  116. :alt="$t('empty.collections')"
  117. />
  118. <span class="text-center pb-4">
  119. {{ $t("empty.collections") }}
  120. </span>
  121. <ButtonSecondary
  122. v-if="
  123. collectionsType.type == 'team-collections' &&
  124. (collectionsType.selectedTeam == undefined ||
  125. collectionsType.selectedTeam.myRole == 'VIEWER')
  126. "
  127. v-tippy="{ theme: 'tooltip' }"
  128. :title="$t('team.no_access')"
  129. :label="$t('add.new')"
  130. class="mb-4"
  131. filled
  132. />
  133. <ButtonSecondary
  134. v-else
  135. :label="$t('add.new')"
  136. filled
  137. class="mb-4"
  138. @click.native="displayModalAdd(true)"
  139. />
  140. </div>
  141. <div
  142. v-if="filterText.length !== 0 && filteredCollections.length === 0"
  143. class="flex flex-col text-secondaryLight p-4 items-center justify-center"
  144. >
  145. <i class="opacity-75 pb-2 material-icons">manage_search</i>
  146. <span class="text-center">
  147. {{ $t("state.nothing_found") }} "{{ filterText }}"
  148. </span>
  149. </div>
  150. <CollectionsAdd
  151. :show="showModalAdd"
  152. @submit="addNewRootCollection"
  153. @hide-modal="displayModalAdd(false)"
  154. />
  155. <CollectionsEdit
  156. :show="showModalEdit"
  157. :editing-coll-name="editingCollection ? editingCollection.name : ''"
  158. :placeholder-coll-name="editingCollection ? editingCollection.name : ''"
  159. @hide-modal="displayModalEdit(false)"
  160. @submit="updateEditingCollection"
  161. />
  162. <CollectionsAddFolder
  163. :show="showModalAddFolder"
  164. :folder="editingFolder"
  165. :folder-path="editingFolderPath"
  166. @add-folder="onAddFolder($event)"
  167. @hide-modal="displayModalAddFolder(false)"
  168. />
  169. <CollectionsEditFolder
  170. :show="showModalEditFolder"
  171. @submit="updateEditingFolder"
  172. @hide-modal="displayModalEditFolder(false)"
  173. />
  174. <CollectionsEditRequest
  175. :show="showModalEditRequest"
  176. :placeholder-req-name="editingRequest ? editingRequest.name : ''"
  177. @submit="updateEditingRequest"
  178. @hide-modal="displayModalEditRequest(false)"
  179. />
  180. <CollectionsImportExport
  181. :show="showModalImportExport"
  182. :collections-type="collectionsType"
  183. @hide-modal="displayModalImportExport(false)"
  184. @update-team-collections="updateTeamCollections"
  185. />
  186. </AppSection>
  187. </template>
  188. <script>
  189. import gql from "graphql-tag"
  190. import cloneDeep from "lodash/cloneDeep"
  191. import { defineComponent } from "@nuxtjs/composition-api"
  192. import CollectionsMyCollection from "./my/Collection.vue"
  193. import CollectionsTeamsCollection from "./teams/Collection.vue"
  194. import { currentUser$ } from "~/helpers/fb/auth"
  195. import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
  196. import * as teamUtils from "~/helpers/teams/utils"
  197. import {
  198. restCollections$,
  199. addRESTCollection,
  200. editRESTCollection,
  201. addRESTFolder,
  202. removeRESTCollection,
  203. editRESTFolder,
  204. removeRESTRequest,
  205. editRESTRequest,
  206. saveRESTRequestAs,
  207. } from "~/newstore/collections"
  208. import {
  209. useReadonlyStream,
  210. useStreamSubscriber,
  211. } from "~/helpers/utils/composables"
  212. export default defineComponent({
  213. components: {
  214. CollectionsMyCollection,
  215. CollectionsTeamsCollection,
  216. },
  217. props: {
  218. doc: Boolean,
  219. selected: { type: Array, default: () => [] },
  220. saveRequest: Boolean,
  221. picked: { type: Object, default: () => {} },
  222. },
  223. setup() {
  224. const { subscribeToStream } = useStreamSubscriber()
  225. return {
  226. subscribeTo: subscribeToStream,
  227. collections: useReadonlyStream(restCollections$, []),
  228. currentUser: useReadonlyStream(currentUser$, null),
  229. }
  230. },
  231. data() {
  232. return {
  233. showModalAdd: false,
  234. showModalEdit: false,
  235. showModalImportExport: false,
  236. showModalAddFolder: false,
  237. showModalEditFolder: false,
  238. showModalEditRequest: false,
  239. editingCollection: undefined,
  240. editingCollectionIndex: undefined,
  241. editingFolder: undefined,
  242. editingFolderName: undefined,
  243. editingFolderIndex: undefined,
  244. editingFolderPath: undefined,
  245. editingRequest: undefined,
  246. editingRequestIndex: undefined,
  247. filterText: "",
  248. collectionsType: {
  249. type: "my-collections",
  250. selectedTeam: undefined,
  251. },
  252. teamCollectionAdapter: new TeamCollectionAdapter(null),
  253. teamCollectionsNew: [],
  254. }
  255. },
  256. computed: {
  257. showTeamCollections() {
  258. if (this.currentUser == null) {
  259. return false
  260. }
  261. return true
  262. },
  263. filteredCollections() {
  264. const collections =
  265. this.collectionsType.type === "my-collections"
  266. ? this.collections
  267. : this.teamCollectionsNew
  268. if (!this.filterText) {
  269. return collections
  270. }
  271. if (this.collectionsType.type === "team-collections") {
  272. return []
  273. }
  274. const filterText = this.filterText.toLowerCase()
  275. const filteredCollections = []
  276. for (const collection of collections) {
  277. const filteredRequests = []
  278. const filteredFolders = []
  279. for (const request of collection.requests) {
  280. if (request.name.toLowerCase().includes(filterText))
  281. filteredRequests.push(request)
  282. }
  283. for (const folder of this.collectionsType.type === "team-collections"
  284. ? collection.children
  285. : collection.folders) {
  286. const filteredFolderRequests = []
  287. for (const request of folder.requests) {
  288. if (request.name.toLowerCase().includes(filterText))
  289. filteredFolderRequests.push(request)
  290. }
  291. if (filteredFolderRequests.length > 0) {
  292. const filteredFolder = Object.assign({}, folder)
  293. filteredFolder.requests = filteredFolderRequests
  294. filteredFolders.push(filteredFolder)
  295. }
  296. }
  297. if (
  298. filteredRequests.length + filteredFolders.length > 0 ||
  299. collection.name.toLowerCase().includes(filterText)
  300. ) {
  301. const filteredCollection = Object.assign({}, collection)
  302. filteredCollection.requests = filteredRequests
  303. filteredCollection.folders = filteredFolders
  304. filteredCollections.push(filteredCollection)
  305. }
  306. }
  307. return filteredCollections
  308. },
  309. },
  310. watch: {
  311. "collectionsType.type": function emitstuff() {
  312. this.$emit("update-collection", this.$data.collectionsType.type)
  313. },
  314. "collectionsType.selectedTeam"(value) {
  315. if (value?.id) this.teamCollectionAdapter.changeTeamID(value.id)
  316. },
  317. },
  318. mounted() {
  319. this.subscribeTo(this.teamCollectionAdapter.collections$, (colls) => {
  320. this.teamCollectionsNew = cloneDeep(colls)
  321. })
  322. },
  323. methods: {
  324. updateTeamCollections() {
  325. // TODO: Remove this at some point
  326. },
  327. updateSelectedTeam(newSelectedTeam) {
  328. this.collectionsType.selectedTeam = newSelectedTeam
  329. this.$emit("update-coll-type", this.collectionsType)
  330. },
  331. updateCollectionType(newCollectionType) {
  332. this.collectionsType.type = newCollectionType
  333. this.$emit("update-coll-type", this.collectionsType)
  334. },
  335. // Intented to be called by the CollectionAdd modal submit event
  336. addNewRootCollection(name) {
  337. if (this.collectionsType.type === "my-collections") {
  338. addRESTCollection({
  339. name,
  340. folders: [],
  341. requests: [],
  342. })
  343. } else if (
  344. this.collectionsType.type === "team-collections" &&
  345. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  346. ) {
  347. teamUtils
  348. .createNewRootCollection(
  349. this.$apollo,
  350. name,
  351. this.collectionsType.selectedTeam.id
  352. )
  353. .then(() => {
  354. this.$toast.success(this.$t("collection.created"), {
  355. icon: "done",
  356. })
  357. })
  358. .catch((e) => {
  359. this.$toast.error(this.$t("error.something_went_wrong"), {
  360. icon: "error_outline",
  361. })
  362. console.error(e)
  363. })
  364. }
  365. this.displayModalAdd(false)
  366. },
  367. // Intented to be called by CollectionEdit modal submit event
  368. updateEditingCollection(newName) {
  369. if (!newName) {
  370. this.$toast.error(this.$t("collection.invalid_name"), {
  371. icon: "error_outline",
  372. })
  373. return
  374. }
  375. if (this.collectionsType.type === "my-collections") {
  376. const collectionUpdated = {
  377. ...this.editingCollection,
  378. name: newName,
  379. }
  380. editRESTCollection(this.editingCollectionIndex, collectionUpdated)
  381. } else if (
  382. this.collectionsType.type === "team-collections" &&
  383. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  384. ) {
  385. teamUtils
  386. .renameCollection(this.$apollo, newName, this.editingCollection.id)
  387. .then(() => {
  388. this.$toast.success(this.$t("collection.renamed"), {
  389. icon: "done",
  390. })
  391. })
  392. .catch((e) => {
  393. this.$toast.error(this.$t("error.something_went_wrong"), {
  394. icon: "error_outline",
  395. })
  396. console.error(e)
  397. })
  398. }
  399. this.displayModalEdit(false)
  400. },
  401. // Intended to be called by CollectionEditFolder modal submit event
  402. updateEditingFolder(name) {
  403. if (this.collectionsType.type === "my-collections") {
  404. editRESTFolder(this.editingFolderPath, { ...this.editingFolder, name })
  405. } else if (
  406. this.collectionsType.type === "team-collections" &&
  407. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  408. ) {
  409. teamUtils
  410. .renameCollection(this.$apollo, name, this.editingFolder.id)
  411. .then(() => {
  412. this.$toast.success(this.$t("folder.renamed"), {
  413. icon: "done",
  414. })
  415. })
  416. .catch((e) => {
  417. this.$toast.error(this.$t("error.something_went_wrong"), {
  418. icon: "error_outline",
  419. })
  420. console.error(e)
  421. })
  422. }
  423. this.displayModalEditFolder(false)
  424. },
  425. // Intented to by called by CollectionsEditRequest modal submit event
  426. updateEditingRequest(requestUpdateData) {
  427. const requestUpdated = {
  428. ...this.editingRequest,
  429. name: requestUpdateData.name || this.editingRequest.name,
  430. }
  431. if (this.collectionsType.type === "my-collections") {
  432. editRESTRequest(
  433. this.editingFolderPath,
  434. this.editingRequestIndex,
  435. requestUpdated
  436. )
  437. } else if (
  438. this.collectionsType.type === "team-collections" &&
  439. this.collectionsType.selectedTeam.myRole !== "VIEWER"
  440. ) {
  441. const requestName = requestUpdateData.name || this.editingRequest.name
  442. teamUtils
  443. .updateRequest(
  444. this.$apollo,
  445. requestUpdated,
  446. requestName,
  447. this.editingRequestIndex
  448. )
  449. .then(() => {
  450. this.$toast.success(this.$t("request.renamed"), {
  451. icon: "done",
  452. })
  453. this.$emit("update-team-collections")
  454. })
  455. .catch((e) => {
  456. this.$toast.error(this.$t("error.something_went_wrong"), {
  457. icon: "error_outline",
  458. })
  459. console.error(e)
  460. })
  461. }
  462. this.displayModalEditRequest(false)
  463. },
  464. displayModalAdd(shouldDisplay) {
  465. this.showModalAdd = shouldDisplay
  466. },
  467. displayModalEdit(shouldDisplay) {
  468. this.showModalEdit = shouldDisplay
  469. if (!shouldDisplay) this.resetSelectedData()
  470. },
  471. displayModalImportExport(shouldDisplay) {
  472. this.showModalImportExport = shouldDisplay
  473. },
  474. displayModalAddFolder(shouldDisplay) {
  475. this.showModalAddFolder = shouldDisplay
  476. if (!shouldDisplay) this.resetSelectedData()
  477. },
  478. displayModalEditFolder(shouldDisplay) {
  479. this.showModalEditFolder = shouldDisplay
  480. if (!shouldDisplay) this.resetSelectedData()
  481. },
  482. displayModalEditRequest(shouldDisplay) {
  483. this.showModalEditRequest = shouldDisplay
  484. if (!shouldDisplay) this.resetSelectedData()
  485. },
  486. editCollection(collection, collectionIndex) {
  487. this.$data.editingCollection = collection
  488. this.$data.editingCollectionIndex = collectionIndex
  489. this.displayModalEdit(true)
  490. },
  491. onAddFolder({ name, folder, path }) {
  492. if (this.collectionsType.type === "my-collections") {
  493. addRESTFolder(name, path)
  494. } else if (this.collectionsType.type === "team-collections") {
  495. if (this.collectionsType.selectedTeam.myRole !== "VIEWER") {
  496. this.$apollo
  497. .mutate({
  498. mutation: gql`
  499. mutation CreateChildCollection(
  500. $childTitle: String!
  501. $collectionID: ID!
  502. ) {
  503. createChildCollection(
  504. childTitle: $childTitle
  505. collectionID: $collectionID
  506. ) {
  507. id
  508. }
  509. }
  510. `,
  511. // Parameters
  512. variables: {
  513. childTitle: name,
  514. collectionID: folder.id,
  515. },
  516. })
  517. .then(() => {
  518. this.$toast.success(this.$t("folder.created"), {
  519. icon: "done",
  520. })
  521. this.$emit("update-team-collections")
  522. })
  523. .catch((e) => {
  524. this.$toast.error(this.$t("error.something_went_wrong"), {
  525. icon: "error_outline",
  526. })
  527. console.error(e)
  528. })
  529. }
  530. }
  531. this.displayModalAddFolder(false)
  532. },
  533. addFolder(payload) {
  534. const { folder, path } = payload
  535. this.$data.editingFolder = folder
  536. this.$data.editingFolderPath = path
  537. this.displayModalAddFolder(true)
  538. },
  539. editFolder(payload) {
  540. const { collectionIndex, folder, folderIndex, folderPath } = payload
  541. this.$data.editingCollectionIndex = collectionIndex
  542. this.$data.editingFolder = folder
  543. this.$data.editingFolderIndex = folderIndex
  544. this.$data.editingFolderPath = folderPath
  545. this.$data.collectionsType = this.collectionsType
  546. this.displayModalEditFolder(true)
  547. },
  548. editRequest(payload) {
  549. const {
  550. collectionIndex,
  551. folderIndex,
  552. folderName,
  553. request,
  554. requestIndex,
  555. folderPath,
  556. } = payload
  557. this.$data.editingCollectionIndex = collectionIndex
  558. this.$data.editingFolderIndex = folderIndex
  559. this.$data.editingFolderName = folderName
  560. this.$data.editingRequest = request
  561. this.$data.editingRequestIndex = requestIndex
  562. this.editingFolderPath = folderPath
  563. this.$emit("select-request", requestIndex)
  564. this.displayModalEditRequest(true)
  565. },
  566. resetSelectedData() {
  567. this.$data.editingCollection = undefined
  568. this.$data.editingCollectionIndex = undefined
  569. this.$data.editingFolder = undefined
  570. this.$data.editingFolderIndex = undefined
  571. this.$data.editingRequest = undefined
  572. this.$data.editingRequestIndex = undefined
  573. },
  574. expandCollection(collectionID) {
  575. this.teamCollectionAdapter.expandCollection(collectionID)
  576. },
  577. removeCollection({ collectionsType, collectionIndex, collectionID }) {
  578. if (collectionsType.type === "my-collections") {
  579. // Cancel pick if picked collection is deleted
  580. if (
  581. this.picked &&
  582. this.picked.pickedType === "my-collection" &&
  583. this.picked.collectionIndex === collectionIndex
  584. ) {
  585. this.$emit("select", { picked: null })
  586. }
  587. removeRESTCollection(collectionIndex)
  588. this.$toast.success(this.$t("state.deleted"), {
  589. icon: "delete",
  590. })
  591. } else if (collectionsType.type === "team-collections") {
  592. // Cancel pick if picked collection is deleted
  593. if (
  594. this.picked &&
  595. this.picked.pickedType === "teams-collection" &&
  596. this.picked.collectionID === collectionID
  597. ) {
  598. this.$emit("select", { picked: null })
  599. }
  600. if (collectionsType.selectedTeam.myRole !== "VIEWER") {
  601. this.$apollo
  602. .mutate({
  603. // Query
  604. mutation: gql`
  605. mutation ($collectionID: ID!) {
  606. deleteCollection(collectionID: $collectionID)
  607. }
  608. `,
  609. // Parameters
  610. variables: {
  611. collectionID,
  612. },
  613. })
  614. .then(() => {
  615. this.$toast.success(this.$t("state.deleted"), {
  616. icon: "delete",
  617. })
  618. })
  619. .catch((e) => {
  620. this.$toast.error(this.$t("error.something_went_wrong"), {
  621. icon: "error_outline",
  622. })
  623. console.error(e)
  624. })
  625. }
  626. }
  627. },
  628. removeRequest({ requestIndex, folderPath }) {
  629. if (this.collectionsType.type === "my-collections") {
  630. // Cancel pick if the picked item is being deleted
  631. if (
  632. this.picked &&
  633. this.picked.pickedType === "my-request" &&
  634. this.picked.folderPath === folderPath &&
  635. this.picked.requestIndex === requestIndex
  636. ) {
  637. this.$emit("select", { picked: null })
  638. }
  639. removeRESTRequest(folderPath, requestIndex)
  640. this.$toast.success(this.$t("state.deleted"), {
  641. icon: "delete",
  642. })
  643. } else if (this.collectionsType.type === "team-collections") {
  644. // Cancel pick if the picked item is being deleted
  645. if (
  646. this.picked &&
  647. this.picked.pickedType === "teams-request" &&
  648. this.picked.requestID === requestIndex
  649. ) {
  650. this.$emit("select", { picked: null })
  651. }
  652. teamUtils
  653. .deleteRequest(this.$apollo, requestIndex)
  654. .then(() => {
  655. this.$toast.success(this.$t("state.deleted"), {
  656. icon: "delete",
  657. })
  658. })
  659. .catch((e) => {
  660. this.$toast.error(this.$t("error.something_went_wrong"), {
  661. icon: "error_outline",
  662. })
  663. console.error(e)
  664. })
  665. }
  666. },
  667. duplicateRequest({ folderPath, request }) {
  668. saveRESTRequestAs(folderPath, {
  669. ...cloneDeep(request),
  670. name: `${request.name} - ${this.$t("action.duplicate")}`,
  671. })
  672. },
  673. },
  674. })
  675. </script>