123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- <template>
- <div>
- <div class="sticky top-0 z-10 flex border-b bg-primary border-dividerLight">
- <input
- v-model="filterText"
- type="search"
- autocomplete="off"
- class="flex flex-1 p-4 py-2 bg-transparent"
- :placeholder="`${t('action.search')}`"
- />
- <div class="flex">
- <ButtonSecondary
- v-tippy="{ theme: 'tooltip' }"
- to="https://docs.hoppscotch.io/features/history"
- blank
- :title="t('app.wiki')"
- svg="help-circle"
- />
- <ButtonSecondary
- v-tippy="{ theme: 'tooltip' }"
- data-testid="clear_history"
- :disabled="history.length === 0"
- svg="trash-2"
- :title="t('action.clear_all')"
- @click.native="confirmRemove = true"
- />
- </div>
- </div>
- <div class="flex flex-col">
- <details
- v-for="(
- filteredHistoryGroup, filteredHistoryGroupIndex
- ) in filteredHistoryGroups"
- :key="`filteredHistoryGroup-${filteredHistoryGroupIndex}`"
- class="flex flex-col"
- open
- >
- <summary
- class="flex items-center justify-between flex-1 min-w-0 cursor-pointer transition focus:outline-none text-secondaryLight text-tiny group"
- >
- <span
- class="px-4 py-2 truncate transition group-hover:text-secondary capitalize-first"
- >
- {{ filteredHistoryGroupIndex }}
- </span>
- <ButtonSecondary
- v-tippy="{ theme: 'tooltip' }"
- svg="trash"
- color="red"
- :title="$t('action.remove')"
- class="hidden group-hover:inline-flex"
- @click.native="deleteBatchHistoryEntry(filteredHistoryGroup)"
- />
- </summary>
- <div
- v-for="(entry, index) in filteredHistoryGroup"
- :key="`entry-${index}`"
- >
- <component
- :is="page == 'rest' ? 'HistoryRestCard' : 'HistoryGraphqlCard'"
- :id="index"
- :entry="entry.entry"
- :show-more="showMore"
- @toggle-star="toggleStar(entry.entry)"
- @delete-entry="deleteHistory(entry.entry)"
- @use-entry="useHistory(entry.entry)"
- />
- </div>
- </details>
- </div>
- <div
- v-if="!(filteredHistory.length !== 0 || history.length === 0)"
- class="flex flex-col items-center justify-center p-4 text-secondaryLight"
- >
- <i class="pb-2 opacity-75 material-icons">manage_search</i>
- <span class="my-2 text-center">
- {{ t("state.nothing_found") }} "{{ filterText }}"
- </span>
- </div>
- <div
- v-if="history.length === 0"
- class="flex flex-col items-center justify-center p-4 text-secondaryLight"
- >
- <img
- :src="`/images/states/${$colorMode.value}/history.svg`"
- loading="lazy"
- class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
- :alt="`${t('empty.history')}`"
- />
- <span class="mb-4 text-center">
- {{ t("empty.history") }}
- </span>
- </div>
- <SmartConfirmModal
- :show="confirmRemove"
- :title="`${t('confirm.remove_history')}`"
- @hide-modal="confirmRemove = false"
- @resolve="clearHistory"
- />
- <HttpReqChangeConfirmModal
- :show="confirmChange"
- @hide-modal="confirmChange = false"
- @save-change="saveRequestChange"
- @discard-change="discardRequestChange"
- />
- <CollectionsSaveRequest
- mode="rest"
- :show="showSaveRequestModal"
- @hide-modal="showSaveRequestModal = false"
- />
- </div>
- </template>
- <script setup lang="ts">
- import { computed, ref, Ref } from "@nuxtjs/composition-api"
- import {
- HoppRESTRequest,
- isEqualHoppRESTRequest,
- safelyExtractRESTRequest,
- } from "@hoppscotch/data"
- import groupBy from "lodash/groupBy"
- import { useTimeAgo } from "@vueuse/core"
- import { pipe } from "fp-ts/function"
- import * as A from "fp-ts/Array"
- import * as E from "fp-ts/Either"
- import {
- useI18n,
- useReadonlyStream,
- useToast,
- } from "~/helpers/utils/composables"
- import {
- restHistory$,
- graphqlHistory$,
- clearRESTHistory,
- clearGraphqlHistory,
- toggleGraphqlHistoryEntryStar,
- toggleRESTHistoryEntryStar,
- deleteGraphqlHistoryEntry,
- deleteRESTHistoryEntry,
- RESTHistoryEntry,
- GQLHistoryEntry,
- } from "~/newstore/history"
- import {
- getDefaultRESTRequest,
- getRESTRequest,
- getRESTSaveContext,
- setRESTRequest,
- setRESTSaveContext,
- } from "~/newstore/RESTSession"
- import { editRESTRequest } from "~/newstore/collections"
- import { runMutation } from "~/helpers/backend/GQLClient"
- import { UpdateRequestDocument } from "~/helpers/backend/graphql"
- import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
- type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
- type TimedHistoryEntry = {
- entry: HistoryEntry
- timeAgo: Ref<string>
- }
- const props = defineProps<{
- page: "rest" | "graphql"
- }>()
- const toast = useToast()
- const t = useI18n()
- const filterText = ref("")
- const showMore = ref(false)
- const confirmRemove = ref(false)
- const clickedHistory = ref<HistoryEntry | null>(null)
- const confirmChange = ref(false)
- const showSaveRequestModal = ref(false)
- const history = useReadonlyStream<RESTHistoryEntry[] | GQLHistoryEntry[]>(
- props.page === "rest" ? restHistory$ : graphqlHistory$,
- []
- )
- const deepCheckForRegex = (value: unknown, regExp: RegExp): boolean => {
- if (value === null || value === undefined) return false
- if (typeof value === "string") return regExp.test(value)
- if (typeof value === "number") return regExp.test(value.toString())
- if (typeof value === "object")
- return Object.values(value).some((input) =>
- deepCheckForRegex(input, regExp)
- )
- if (Array.isArray(value))
- return value.some((input) => deepCheckForRegex(input, regExp))
- return false
- }
- const filteredHistory = computed(() =>
- pipe(
- history.value as HistoryEntry[],
- A.filter(
- (
- input
- ): input is HistoryEntry & {
- updatedOn: NonNullable<HistoryEntry["updatedOn"]>
- } => {
- return (
- !!input.updatedOn &&
- (filterText.value.length === 0 ||
- deepCheckForRegex(input, new RegExp(filterText.value, "gi")))
- )
- }
- ),
- A.map(
- (entry): TimedHistoryEntry => ({
- entry,
- timeAgo: useTimeAgo(entry.updatedOn),
- })
- )
- )
- )
- const filteredHistoryGroups = computed(() =>
- groupBy(filteredHistory.value, (entry) => entry.timeAgo.value)
- )
- const clearHistory = () => {
- if (props.page === "rest") clearRESTHistory()
- else clearGraphqlHistory()
- toast.success(`${t("state.history_deleted")}`)
- }
- const setRestReq = (request: HoppRESTRequest | null | undefined) => {
- setRESTRequest(safelyExtractRESTRequest(request, getDefaultRESTRequest()))
- }
- // NOTE: For GQL, the HistoryGraphqlCard component already implements useEntry
- // (That is not a really good behaviour tho ¯\_(ツ)_/¯)
- const useHistory = (entry: RESTHistoryEntry) => {
- const currentFullReq = getRESTRequest()
- // Initial state trigers a popup
- if (!clickedHistory.value) {
- clickedHistory.value = entry
- confirmChange.value = true
- return
- }
- // Checks if there are any change done in current request and the history request
- if (
- !isEqualHoppRESTRequest(
- currentFullReq,
- clickedHistory.value.request as HoppRESTRequest
- )
- ) {
- clickedHistory.value = entry
- confirmChange.value = true
- } else {
- props.page === "rest" && setRestReq(entry.request as HoppRESTRequest)
- clickedHistory.value = entry
- }
- }
- /** Save current request to the collection */
- const saveRequestChange = () => {
- const saveCtx = getRESTSaveContext()
- saveCurrentRequest(saveCtx)
- confirmChange.value = false
- }
- /** Discard changes and change the current request and remove the collection context */
- const discardRequestChange = () => {
- const saveCtx = getRESTSaveContext()
- if (saveCtx) {
- setRESTSaveContext(null)
- }
- clickedHistory.value &&
- setRestReq(clickedHistory.value.request as HoppRESTRequest)
- confirmChange.value = false
- }
- const saveCurrentRequest = (saveCtx: HoppRequestSaveContext | null) => {
- if (!saveCtx) {
- showSaveRequestModal.value = true
- return
- }
- if (saveCtx.originLocation === "user-collection") {
- try {
- editRESTRequest(
- saveCtx.folderPath,
- saveCtx.requestIndex,
- getRESTRequest()
- )
- clickedHistory.value &&
- setRestReq(clickedHistory.value.request as HoppRESTRequest)
- setRESTSaveContext(null)
- toast.success(`${t("request.saved")}`)
- } catch (e) {
- console.error(e)
- setRESTSaveContext(null)
- saveCurrentRequest(null)
- }
- } else if (saveCtx.originLocation === "team-collection") {
- const req = getRESTRequest()
- try {
- runMutation(UpdateRequestDocument, {
- requestID: saveCtx.requestID,
- data: {
- title: req.name,
- request: JSON.stringify(req),
- },
- })().then((result) => {
- if (E.isLeft(result)) {
- toast.error(`${t("profile.no_permission")}`)
- } else {
- toast.success(`${t("request.saved")}`)
- }
- })
- clickedHistory.value &&
- setRestReq(clickedHistory.value.request as HoppRESTRequest)
- setRESTSaveContext(null)
- } catch (error) {
- showSaveRequestModal.value = true
- toast.error(`${t("error.something_went_wrong")}`)
- console.error(error)
- setRESTSaveContext(null)
- }
- }
- }
- const isRESTHistoryEntry = (
- entries: TimedHistoryEntry[]
- ): entries is Array<TimedHistoryEntry & { entry: RESTHistoryEntry }> =>
- // If the page is rest, then we can guarantee what we have is a RESTHistoryEnry
- props.page === "rest"
- const deleteBatchHistoryEntry = (entries: TimedHistoryEntry[]) => {
- if (isRESTHistoryEntry(entries)) {
- entries.forEach((entry) => {
- deleteRESTHistoryEntry(entry.entry)
- })
- } else {
- entries.forEach((entry) => {
- deleteGraphqlHistoryEntry(entry.entry as GQLHistoryEntry)
- })
- }
- toast.success(`${t("state.deleted")}`)
- }
- const deleteHistory = (entry: HistoryEntry) => {
- if (props.page === "rest") deleteRESTHistoryEntry(entry as RESTHistoryEntry)
- else deleteGraphqlHistoryEntry(entry as GQLHistoryEntry)
- toast.success(`${t("state.deleted")}`)
- }
- const toggleStar = (entry: HistoryEntry) => {
- // History entry type specified because function does not know the type
- if (props.page === "rest")
- toggleRESTHistoryEntryStar(entry as RESTHistoryEntry)
- else toggleGraphqlHistoryEntryStar(entry as GQLHistoryEntry)
- }
- </script>
|