feat: description field for request parameters and headers (#4187)

* feat: key-value component added for reuse

* chore: inspection result added

* chore: add `HoppRESTRequest` schema `v7`

* feat: add `description` for field for REST request headers

* feat: add `description` for field for GraphQL request headers

* fix: synchronization logic b/w bulk edit context and the default view

- Add `HoppGQLRequest` schema `v6`.
- Fix pre-existing issue with changes in bulk edit context not immediately reflecting in the default GQL request headers view.

* feat: support importing `description` fields from external sources

* test: fix failing tests

* chore: include description field for computed headers

Headers computed based on authorization info & inherited entries.

* feat: add `description` field for headers at the collection level

Add `HoppCollection` schema `v3`.

* test: fix failing tests

* ci: update tests workflow target branch trigger

* chore: cleanup

* chore: cleanup

* chore: rely on type inference

---------

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
Anwarul Islam
2024-08-27 15:00:12 +06:00
committed by GitHub
parent 33b0a54af1
commit 43730d66f6
29 changed files with 1366 additions and 1092 deletions

View File

@@ -61,91 +61,19 @@
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<span>
<HoppButtonSecondary
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
content:
index !== workingHeaders?.length - 1
? t('action.drag_to_reorder')
: null,
}"
:icon="IconGripVertical"
class="opacity-0"
:class="{
'draggable-handle cursor-grab group-hover:opacity-100':
index !== workingHeaders?.length - 1,
}"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.key"
:placeholder="`${t('count.header', { count: index + 1 })}`"
:auto-complete-source="commonHeaders"
@change="
updateHeader(index, {
id: header.id,
key: $event,
value: header.value,
active: header.active,
})
"
/>
<SmartEnvInput
v-model="header.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
@change="
updateHeader(index, {
id: header.id,
key: header.key,
value: $event,
active: header.active,
})
"
/>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
header.hasOwnProperty('active')
? header.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:icon="
header.hasOwnProperty('active')
? header.active
? IconCheckCircle
: IconCircle
: IconCheckCircle
"
color="green"
@click="
updateHeader(index, {
id: header.id,
key: header.key,
value: header.value,
active: !header.active,
})
"
/>
</span>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteHeader(index)"
/>
</span>
</div>
<HttpKeyValue
v-model:name="header.key"
v-model:value="header.value"
v-model:description="header.description"
:total="workingHeaders.length"
:index="index"
:entity-id="header.id"
:entity-active="header.active"
:is-active="header.hasOwnProperty('active')"
:key-auto-complete-source="commonHeaders"
@update-entity="updateHeader($event.index, $event.payload)"
@delete-entity="deleteHeader($event)"
/>
</template>
</draggable>
@@ -170,16 +98,27 @@
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.header.key"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<SmartEnvInput
:model-value="mask(header)"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<input
:value="header.header.description"
:placeholder="t('count.description')"
class="flex flex-1 px-4 bg-transparent text-secondaryLight"
type="text"
readonly
/>
<span>
<HoppButtonSecondary
v-if="header.source === 'auth'"
@@ -228,11 +167,13 @@
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.header.key"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<SmartEnvInput
:model-value="
header.source === 'auth' ? mask(header) : header.header.value
@@ -240,6 +181,14 @@
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<input
:value="header.header.description"
:placeholder="t('count.description')"
class="flex flex-1 px-4 bg-transparent text-secondaryLight"
type="text"
readonly
/>
<HoppButtonSecondary
v-if="header.source === 'auth'"
v-tippy="{ theme: 'tooltip' }"
@@ -280,47 +229,44 @@
</template>
<script setup lang="ts">
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit"
import IconPlus from "~icons/lucide/plus"
import IconGripVertical from "~icons/lucide/grip-vertical"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconTrash from "~icons/lucide/trash"
import IconCircle from "~icons/lucide/circle"
import IconWrapText from "~icons/lucide/wrap-text"
import IconArrowUpRight from "~icons/lucide/arrow-up-right"
import IconLock from "~icons/lucide/lock"
import IconEye from "~icons/lucide/eye"
import IconEyeOff from "~icons/lucide/eye-off"
import IconInfo from "~icons/lucide/info"
import { computed, reactive, ref, watch } from "vue"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RA from "fp-ts/ReadonlyArray"
import { pipe, flow } from "fp-ts/function"
import { useCodemirror } from "@composables/codemirror"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { useToast } from "@composables/toast"
import {
GQLHeader,
rawKeyValueEntriesToString,
parseRawKeyValueEntriesE,
RawKeyValueEntry,
HoppGQLRequest,
parseRawKeyValueEntriesE,
rawKeyValueEntriesToString,
RawKeyValueEntry,
} from "@hoppscotch/data"
import draggable from "vuedraggable-es"
import { clone, cloneDeep, isEqual } from "lodash-es"
import { useColorMode } from "@composables/theming"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { commonHeaders } from "~/helpers/headers"
import { useCodemirror } from "@composables/codemirror"
import { objRemoveKey } from "~/helpers/functional/object"
import { useVModel } from "@vueuse/core"
import * as A from "fp-ts/Array"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import { flow, pipe } from "fp-ts/function"
import { clone, cloneDeep, isEqual } from "lodash-es"
import { computed, reactive, ref, toRef, watch } from "vue"
import draggable from "vuedraggable-es"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { HoppGQLHeader } from "~/helpers/graphql"
import { throwError } from "~/helpers/functional/error"
import { objRemoveKey } from "~/helpers/functional/object"
import { HoppGQLHeader } from "~/helpers/graphql"
import { commonHeaders } from "~/helpers/headers"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { toggleNestedSetting } from "~/newstore/settings"
import IconArrowUpRight from "~icons/lucide/arrow-up-right"
import IconEdit from "~icons/lucide/edit"
import IconEye from "~icons/lucide/eye"
import IconEyeOff from "~icons/lucide/eye-off"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconInfo from "~icons/lucide/info"
import IconLock from "~icons/lucide/lock"
import IconPlus from "~icons/lucide/plus"
import IconTrash2 from "~icons/lucide/trash-2"
import IconWrapText from "~icons/lucide/wrap-text"
const colorMode = useColorMode()
const t = useI18n()
@@ -371,6 +317,7 @@ const workingHeaders = ref<Array<GQLHeader & { id: number }>>([
key: "",
value: "",
active: true,
description: "",
},
])
@@ -385,13 +332,14 @@ watch(workingHeaders, (headersList) => {
key: "",
value: "",
active: true,
description: "",
})
}
})
// Sync logic between headers and working headers
watch(
props.modelValue.headers,
() => request.value.headers,
(newHeadersList) => {
// Sync should overwrite working headers
const filteredWorkingHeaders = pipe(
@@ -422,8 +370,18 @@ watch(
)
}
if (!isEqual(newHeadersList, filteredBulkHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(newHeadersList)
const newHeadersListKeyValuePairs = newHeadersList.map(
({ key, value, active }) => ({
key,
value,
active,
})
)
if (!isEqual(newHeadersListKeyValuePairs, filteredBulkHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(
newHeadersListKeyValuePairs
)
}
},
{ immediate: true }
@@ -458,8 +416,20 @@ watch(bulkHeaders, (newBulkHeaders) => {
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(request.value.headers, filteredBulkHeaders)) {
request.value.headers = filteredBulkHeaders
const headers = toRef(request.value, "headers")
const paramKeyValuePairs = headers.value.map(({ key, value, active }) => ({
key,
value,
active,
}))
if (!isEqual(paramKeyValuePairs, filteredBulkHeaders)) {
headers.value = filteredBulkHeaders.map((param, idx) => ({
...param,
// Adding a new key-value pair in the bulk edit context won't have a corresponding entry under `request.value.headers`, hence the fallback
description: headers.value[idx]?.description ?? "",
}))
}
})
@@ -491,6 +461,7 @@ const addHeader = () => {
key: "",
value: "",
active: true,
description: "",
})
}
@@ -547,6 +518,7 @@ const clearContent = () => {
key: "",
value: "",
active: true,
description: "",
},
]
@@ -644,16 +616,21 @@ const inheritedProperties = computed(() => {
header.inheritedHeader !== undefined &&
header.inheritedHeader.active
)
.map((header, index) => ({
inheritedFrom: props.inheritedProperties?.headers[index].parentName,
source: "headers",
id: `header-${index}`,
header: {
key: header.inheritedHeader?.key,
value: header.inheritedHeader?.value,
active: header.inheritedHeader?.active,
},
}))
.map((header, index) => {
const { key, value, active, description } = header.inheritedHeader
return {
inheritedFrom: props.inheritedProperties?.headers[index].parentName,
source: "headers",
id: `header-${index}`,
header: {
key,
value,
active,
description,
},
}
})
let auth = [] as {
inheritedFrom: string