From 58857be6504ffbce565180073312567ed8b61f57 Mon Sep 17 00:00:00 2001
From: Nivedin <53208152+nivedin@users.noreply.github.com>
Date: Mon, 30 Sep 2024 19:06:53 +0530
Subject: [PATCH] feat: save api responses (#4382)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
---
packages/hoppscotch-common/locales/en.json | 17 +-
.../hoppscotch-common/src/components.d.ts | 8 +
.../app/spotlight/entry/RESTRequest.vue | 4 +-
.../src/components/collections/AddRequest.vue | 3 +
.../src/components/collections/Collection.vue | 2 +-
.../components/collections/EditResponse.vue | 95 +++
.../collections/ExampleResponse.vue | 247 +++++++
.../components/collections/MyCollections.vue | 57 +-
.../src/components/collections/Request.vue | 60 +-
.../components/collections/SaveRequest.vue | 30 +-
.../collections/TeamCollections.vue | 57 +-
.../src/components/collections/index.vue | 604 +++++++++++++++++-
.../src/components/embeds/Request.vue | 4 +-
.../src/components/embeds/index.vue | 4 +-
.../src/components/graphql/RequestOptions.vue | 8 +-
.../src/components/history/index.vue | 1 +
.../src/components/http/ImportCurl.vue | 2 +
.../src/components/http/Request.vue | 8 +-
.../src/components/http/RequestOptions.vue | 39 +-
.../src/components/http/RequestTab.vue | 6 +-
.../src/components/http/Response.vue | 122 +++-
.../src/components/http/ResponseInterface.vue | 15 +-
.../src/components/http/SaveResponseName.vue | 81 +++
.../src/components/http/TabHead.vue | 46 +-
.../http/example/LenseBodyRenderer.vue | 85 +++
.../src/components/http/example/Response.vue | 23 +
.../components/http/example/ResponseMeta.vue | 70 ++
.../http/example/ResponseRequest.vue | 265 ++++++++
.../components/http/example/ResponseTab.vue | 48 ++
.../src/components/lenses/HeadersRenderer.vue | 21 +-
.../lenses/HeadersRendererEntry.vue | 65 +-
.../lenses/ResponseBodyRenderer.vue | 20 +-
.../lenses/renderers/HTMLLensRenderer.vue | 53 +-
.../lenses/renderers/JSONLensRenderer.vue | 123 +++-
.../lenses/renderers/RawLensRenderer.vue | 78 ++-
.../lenses/renderers/XMLLensRenderer.vue | 89 ++-
.../src/components/share/Request.vue | 2 +-
.../src/components/share/index.vue | 1 +
.../src/components/smart/EnvInput.vue | 26 +-
.../src/composables/codemirror.ts | 9 +-
.../src/composables/lens-actions.ts | 25 +-
.../src/helpers/RequestRunner.ts | 4 +-
.../hoppscotch-common/src/helpers/actions.ts | 10 +-
.../src/helpers/collection/collection.ts | 16 +-
.../src/helpers/collection/request.ts | 5 +-
.../helpers/curl/__tests__/curlparser.spec.js | 28 +
.../src/helpers/curl/curlparser.ts | 1 +
.../editor/extensions/HoppEnvironment.ts | 21 +-
.../src/helpers/findStatusGroup.ts | 2 +-
.../helpers/import-export/import/insomnia.ts | 3 +
.../helpers/import-export/import/openapi.ts | 123 +++-
.../helpers/import-export/import/postman.ts | 175 +++--
.../src/helpers/keybindings.ts | 3 +-
.../src/helpers/rest/default.ts | 1 +
.../src/helpers/rest/document.ts | 46 +-
.../src/helpers/rest/labelColoring.ts | 7 +-
.../src/helpers/teams/TeamRequest.ts | 9 +
.../src/helpers/teams/TeamsSearch.service.ts | 1 +
.../src/helpers/utils/statusCodes.ts | 35 +
.../hoppscotch-common/src/newstore/history.ts | 1 +
.../hoppscotch-common/src/pages/e/_id.vue | 5 +-
.../hoppscotch-common/src/pages/index.vue | 39 +-
.../hoppscotch-common/src/pages/r/_id.vue | 1 +
.../menu/__tests__/url.menu.spec.ts | 1 +
.../context-menu/menu/parameter.menu.ts | 16 +-
.../services/context-menu/menu/url.menu.ts | 1 +
.../src/services/inspection/index.ts | 24 +-
.../inspectors/environment.inspector.ts | 28 +-
.../inspection/inspectors/header.inspector.ts | 10 +-
.../inspectors/response.inspector.ts | 7 +-
.../persistence/__tests__/__mocks__/index.ts | 8 +-
.../persistence/validation-schemas/index.ts | 18 +-
.../__tests__/history.searcher.spec.ts | 1 +
.../searchers/collections.searcher.ts | 1 +
.../spotlight/searchers/history.searcher.ts | 5 +-
.../spotlight/searchers/request.searcher.ts | 7 +-
.../searchers/teamRequest.searcher.ts | 1 +
.../src/services/tab/rest.ts | 16 +-
packages/hoppscotch-data/src/rest/index.ts | 15 +-
packages/hoppscotch-data/src/rest/v/2.ts | 6 -
packages/hoppscotch-data/src/rest/v/8.ts | 70 +-
.../hoppscotch-data/src/utils/statusCodes.ts | 101 +++
.../collections/collections.platform.ts | 4 +-
.../collections/collections.platform.ts | 2 +
84 files changed, 3080 insertions(+), 321 deletions(-)
create mode 100644 packages/hoppscotch-common/src/components/collections/EditResponse.vue
create mode 100644 packages/hoppscotch-common/src/components/collections/ExampleResponse.vue
create mode 100644 packages/hoppscotch-common/src/components/http/SaveResponseName.vue
create mode 100644 packages/hoppscotch-common/src/components/http/example/LenseBodyRenderer.vue
create mode 100644 packages/hoppscotch-common/src/components/http/example/Response.vue
create mode 100644 packages/hoppscotch-common/src/components/http/example/ResponseMeta.vue
create mode 100644 packages/hoppscotch-common/src/components/http/example/ResponseRequest.vue
create mode 100644 packages/hoppscotch-common/src/components/http/example/ResponseTab.vue
create mode 100644 packages/hoppscotch-data/src/utils/statusCodes.ts
diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json
index c036de5b7..b1e96397a 100644
--- a/packages/hoppscotch-common/locales/en.json
+++ b/packages/hoppscotch-common/locales/en.json
@@ -43,6 +43,7 @@
"rename": "Rename",
"restore": "Restore",
"save": "Save",
+ "save_as_example": "Save as example",
"scroll_to_bottom": "Scroll to bottom",
"scroll_to_top": "Scroll to top",
"search": "Search",
@@ -224,6 +225,7 @@
"remove_folder": "Are you sure you want to permanently delete this folder?",
"remove_history": "Are you sure you want to permanently delete all history?",
"remove_request": "Are you sure you want to permanently delete this request?",
+ "remove_response": "Are you sure you want to permanently delete this response?",
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
"remove_team": "Are you sure you want to delete this workspace?",
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
@@ -391,7 +393,8 @@
"codegen": "{request_name} - code",
"graphql_response": "GraphQL-Response",
"lens": "{request_name} - response",
- "realtime_response": "Realtime-Response"
+ "realtime_response": "Realtime-Response",
+ "response_interface": "Response-Interface"
},
"filter": {
"all": "All",
@@ -528,7 +531,9 @@
"confirm": "Confirm",
"customize_request": "Customize Request",
"edit_request": "Edit Request",
+ "edit_response": "Edit Response",
"import_export": "Import / Export",
+ "response_name": "Response Name",
"share_request": "Share Request"
},
"mqtt": {
@@ -595,6 +600,7 @@
},
"request": {
"added": "Request added",
+ "add": "Add Request",
"authorization": "Authorization",
"body": "Request Body",
"choose_language": "Choose language",
@@ -631,6 +637,7 @@
"rename": "Rename Request",
"renamed": "Request renamed",
"request_variables": "Request variables",
+ "response_name_exists": "Response name already exists",
"run": "Run",
"save": "Save",
"save_as": "Save as",
@@ -650,14 +657,18 @@
"response": {
"audio": "Audio",
"body": "Response Body",
+ "duplicated": "Response duplicated",
+ "duplicate_name_error": "Same name response already exists",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Headers",
"html": "HTML",
"image": "Image",
"json": "JSON",
"pdf": "PDF",
+ "please_save_request": "Save the request to create example",
"preview_html": "Preview HTML",
"raw": "Raw",
+ "renamed": "Response renamed",
"size": "Size",
"status": "Status",
"time": "Time",
@@ -666,7 +677,9 @@
"waiting_for_connection": "waiting for connection",
"xml": "XML",
"generate_data_schema": "Generate Data Schema",
- "data_schema": "Data Schema"
+ "data_schema": "Data Schema",
+ "saved": "Response saved",
+ "invalid_name": "Please provide a name for the response"
},
"settings": {
"accent_color": "Accent color",
diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts
index e51899b8d..d08d3f35e 100644
--- a/packages/hoppscotch-common/src/components.d.ts
+++ b/packages/hoppscotch-common/src/components.d.ts
@@ -49,6 +49,8 @@ declare module 'vue' {
CollectionsEdit: typeof import('./components/collections/Edit.vue')['default']
CollectionsEditFolder: typeof import('./components/collections/EditFolder.vue')['default']
CollectionsEditRequest: typeof import('./components/collections/EditRequest.vue')['default']
+ CollectionsEditResponse: typeof import('./components/collections/EditResponse.vue')['default']
+ CollectionsExampleResponse: typeof import('./components/collections/ExampleResponse.vue')['default']
CollectionsGraphql: typeof import('./components/collections/graphql/index.vue')['default']
CollectionsGraphqlAdd: typeof import('./components/collections/graphql/Add.vue')['default']
CollectionsGraphqlAddFolder: typeof import('./components/collections/graphql/AddFolder.vue')['default']
@@ -142,6 +144,11 @@ declare module 'vue' {
HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
HttpCodegen: typeof import('./components/http/Codegen.vue')['default']
HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
+ HttpExampleLenseBodyRenderer: typeof import('./components/http/example/LenseBodyRenderer.vue')['default']
+ HttpExampleResponse: typeof import('./components/http/example/Response.vue')['default']
+ HttpExampleResponseMeta: typeof import('./components/http/example/ResponseMeta.vue')['default']
+ HttpExampleResponseRequest: typeof import('./components/http/example/ResponseRequest.vue')['default']
+ HttpExampleResponseTab: typeof import('./components/http/example/ResponseTab.vue')['default']
HttpHeaders: typeof import('./components/http/Headers.vue')['default']
HttpImportCurl: typeof import('./components/http/ImportCurl.vue')['default']
HttpKeyValue: typeof import('./components/http/KeyValue.vue')['default']
@@ -157,6 +164,7 @@ declare module 'vue' {
HttpResponse: typeof import('./components/http/Response.vue')['default']
HttpResponseInterface: typeof import('./components/http/ResponseInterface.vue')['default']
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
+ HttpSaveResponseName: typeof import('./components/http/SaveResponseName.vue')['default']
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
HttpTabHead: typeof import('./components/http/TabHead.vue')['default']
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
diff --git a/packages/hoppscotch-common/src/components/app/spotlight/entry/RESTRequest.vue b/packages/hoppscotch-common/src/components/app/spotlight/entry/RESTRequest.vue
index 062b47459..0ba4cb15b 100644
--- a/packages/hoppscotch-common/src/components/app/spotlight/entry/RESTRequest.vue
+++ b/packages/hoppscotch-common/src/components/app/spotlight/entry/RESTRequest.vue
@@ -7,9 +7,9 @@
{{ request.method.toUpperCase() }}
diff --git a/packages/hoppscotch-common/src/components/collections/AddRequest.vue b/packages/hoppscotch-common/src/components/collections/AddRequest.vue
index ae3ebd537..8633cf823 100644
--- a/packages/hoppscotch-common/src/components/collections/AddRequest.vue
+++ b/packages/hoppscotch-common/src/components/collections/AddRequest.vue
@@ -92,6 +92,9 @@ watch(
() => props.show,
(show) => {
if (show) {
+ if (tabs.currentActiveTab.value.document.type === "example-response")
+ return
+
editingName.value = tabs.currentActiveTab.value.document.request.name
}
}
diff --git a/packages/hoppscotch-common/src/components/collections/Collection.vue b/packages/hoppscotch-common/src/components/collections/Collection.vue
index 60860a1b0..0faf43bb6 100644
--- a/packages/hoppscotch-common/src/components/collections/Collection.vue
+++ b/packages/hoppscotch-common/src/components/collections/Collection.vue
@@ -62,7 +62,7 @@
diff --git a/packages/hoppscotch-common/src/components/collections/EditResponse.vue b/packages/hoppscotch-common/src/components/collections/EditResponse.vue
new file mode 100644
index 000000000..44bc9a15e
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/collections/EditResponse.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/collections/ExampleResponse.vue b/packages/hoppscotch-common/src/components/collections/ExampleResponse.vue
new file mode 100644
index 000000000..c5105aea2
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/collections/ExampleResponse.vue
@@ -0,0 +1,247 @@
+
+
+
+
+
+ {{ response.code ?? response.status }}
+
+
+
+
+
+ {{ responseName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ emit('edit-response', {
+ responseName: responseName,
+ responseID: saveContext.exampleID,
+ })
+ hide()
+ }
+ "
+ />
+ {
+ emit('duplicate-response', {
+ responseName: responseName,
+ responseID: saveContext.exampleID,
+ })
+ hide()
+ }
+ "
+ />
+ {
+ emit('remove-response', {
+ responseName: responseName,
+ responseID: saveContext.exampleID,
+ })
+ hide()
+ }
+ "
+ />
+
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/collections/MyCollections.vue b/packages/hoppscotch-common/src/components/collections/MyCollections.vue
index a379d47d3..fcf1706d5 100644
--- a/packages/hoppscotch-common/src/components/collections/MyCollections.vue
+++ b/packages/hoppscotch-common/src/components/collections/MyCollections.vue
@@ -105,7 +105,8 @@
})
"
@dragging="
- (isDraging) => highlightChildren(isDraging ? node.id : null)
+ (isDraging: boolean) =>
+ highlightChildren(isDraging ? node.id : null)
"
@toggle-children="
() => {
@@ -187,7 +188,8 @@
})
"
@dragging="
- (isDraging) => highlightChildren(isDraging ? node.id : null)
+ (isDraging: boolean) =>
+ highlightChildren(isDraging ? node.id : null)
"
@toggle-children="
() => {
@@ -228,6 +230,15 @@
request: node.data.data.data,
})
"
+ @edit-response="
+ emit('edit-response', {
+ folderPath: node.data.data.parentIndex,
+ requestIndex: pathToIndex(node.id),
+ request: node.data.data.data,
+ responseName: $event.responseName,
+ responseID: $event.responseID,
+ })
+ "
@duplicate-request="
node.data.type === 'requests' &&
emit('duplicate-request', {
@@ -235,6 +246,15 @@
request: node.data.data.data,
})
"
+ @duplicate-response="
+ emit('duplicate-response', {
+ folderPath: node.data.data.parentIndex,
+ requestIndex: pathToIndex(node.id),
+ request: node.data.data.data,
+ responseName: $event.responseName,
+ responseID: $event.responseID,
+ })
+ "
@remove-request="
node.data.type === 'requests' &&
emit('remove-request', {
@@ -242,6 +262,15 @@
requestIndex: pathToIndex(node.id),
})
"
+ @remove-response="
+ emit('remove-response', {
+ folderPath: node.data.data.parentIndex,
+ requestIndex: pathToIndex(node.id),
+ request: node.data.data.data,
+ responseName: $event.responseName,
+ responseID: $event.responseID,
+ })
+ "
@select-request="
node.data.type === 'requests' &&
selectRequest({
@@ -250,6 +279,15 @@
requestIndex: pathToIndex(node.id),
})
"
+ @select-response="
+ emit('select-response', {
+ responseName: $event.responseName,
+ responseID: $event.responseID,
+ request: node.data.data.data,
+ folderPath: node.data.data.parentIndex,
+ requestIndex: pathToIndex(node.id),
+ })
+ "
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
@@ -431,6 +469,14 @@ const props = defineProps({
},
})
+type ResponsePayload = {
+ folderPath: string
+ requestIndex: string
+ request: HoppRESTRequest
+ responseName: string
+ responseID: string
+}
+
const emit = defineEmits<{
(event: "display-modal-add"): void
(
@@ -483,6 +529,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
}
): void
+ (event: "edit-response", payload: ResponsePayload): void
(
event: "duplicate-request",
payload: {
@@ -490,6 +537,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
}
): void
+ (event: "duplicate-response", payload: ResponsePayload): void
(event: "export-data", payload: HoppCollection): void
(event: "remove-collection", payload: string): void
(event: "remove-folder", payload: string): void
@@ -500,6 +548,7 @@ const emit = defineEmits<{
requestIndex: string
}
): void
+ (event: "remove-response", payload: ResponsePayload): void
(
event: "select-request",
payload: {
@@ -550,6 +599,7 @@ const emit = defineEmits<{
): void
(event: "select", payload: Picked | null): void
(event: "display-modal-import-export"): void
+ (event: "select-response", payload: ResponsePayload): void
}>()
const refFilterCollection = toRef(props, "filteredCollections")
@@ -600,7 +650,8 @@ const isActiveRequest = (folderPath: string, requestIndex: number) => {
(active) =>
active.originLocation === "user-collection" &&
active.folderPath === folderPath &&
- active.requestIndex === requestIndex
+ active.requestIndex === requestIndex &&
+ active.exampleID === undefined
),
O.isSome
)
diff --git a/packages/hoppscotch-common/src/components/collections/Request.vue b/packages/hoppscotch-common/src/components/collections/Request.vue
index 7e6c6c8bd..718c16be0 100644
--- a/packages/hoppscotch-common/src/components/collections/Request.vue
+++ b/packages/hoppscotch-common/src/components/collections/Request.vue
@@ -13,7 +13,7 @@
@dragend="resetDragState"
>
@@ -174,6 +209,8 @@ import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconShare2 from "~icons/lucide/share-2"
+import IconArrowRight from "~icons/lucide/chevron-right"
+import IconArrowDown from "~icons/lucide/chevron-down"
import { ref, PropType, watch, computed } from "vue"
import { HoppRESTRequest } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n"
@@ -247,8 +284,14 @@ const props = defineProps({
},
})
+type ResponsePayload = {
+ responseName: string
+ responseID: string
+}
+
const emit = defineEmits<{
(event: "edit-request"): void
+ (event: "edit-response", payload: ResponsePayload): void
(event: "duplicate-request"): void
(event: "remove-request"): void
(event: "select-request"): void
@@ -256,6 +299,10 @@ const emit = defineEmits<{
(event: "drag-request", payload: DataTransfer): void
(event: "update-request-order", payload: DataTransfer): void
(event: "update-last-request-order", payload: DataTransfer): void
+ (event: "duplicate-response", payload: ResponsePayload): void
+ (event: "remove-response", payload: ResponsePayload): void
+ (event: "select-response", payload: ResponsePayload): void
+ (event: "toggle-children"): void
}>()
const tippyActions = ref(null)
@@ -269,6 +316,8 @@ const dragging = ref(false)
const ordering = ref(false)
const orderingLastItem = ref(false)
+const isResponseVisible = ref(false)
+
const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
type: "collection",
id: "",
@@ -288,6 +337,11 @@ const selectRequest = () => {
emit("select-request")
}
+const toggleRequestResponse = () => {
+ emit("toggle-children")
+ isResponseVisible.value = !isResponseVisible.value
+}
+
const dragStart = ({ dataTransfer }: DragEvent) => {
if (dataTransfer) {
emit("drag-request", dataTransfer)
diff --git a/packages/hoppscotch-common/src/components/collections/SaveRequest.vue b/packages/hoppscotch-common/src/components/collections/SaveRequest.vue
index 2d03fa08d..569af1a73 100644
--- a/packages/hoppscotch-common/src/components/collections/SaveRequest.vue
+++ b/packages/hoppscotch-common/src/components/collections/SaveRequest.vue
@@ -149,7 +149,10 @@ const gqlRequestName = computedWithControl(
const restRequestName = computedWithControl(
() => RESTTabs.currentActiveTab.value,
- () => RESTTabs.currentActiveTab.value.document.request.name
+ () =>
+ RESTTabs.currentActiveTab.value.document.type === "request"
+ ? RESTTabs.currentActiveTab.value.document.request.name
+ : ""
)
const reqName = computed(() => {
@@ -166,7 +169,10 @@ const requestContext = computed(() => {
return props.request
}
- if (props.mode === "rest") {
+ if (
+ props.mode === "rest" &&
+ RESTTabs.currentActiveTab.value.document.type === "request"
+ ) {
return RESTTabs.currentActiveTab.value.document.request
}
@@ -184,7 +190,10 @@ const {
watch(
() => [RESTTabs.currentActiveTab.value, GQLTabs.currentActiveTab.value],
() => {
- if (props.mode === "rest") {
+ if (
+ props.mode === "rest" &&
+ RESTTabs.currentActiveTab.value.document.type === "request"
+ ) {
requestName.value =
RESTTabs.currentActiveTab.value?.document.request.name ?? ""
} else {
@@ -249,9 +258,15 @@ const saveRequestAs = async () => {
const requestUpdated =
props.mode === "rest"
- ? cloneDeep(RESTTabs.currentActiveTab.value.document.request)
+ ? cloneDeep(
+ RESTTabs.currentActiveTab.value.document.type === "request"
+ ? RESTTabs.currentActiveTab.value.document.request
+ : null
+ )
: cloneDeep(GQLTabs.currentActiveTab.value.document.request)
+ if (!requestUpdated) return
+
requestUpdated.name = requestName.value
if (picked.value.pickedType === "my-collection") {
@@ -263,13 +278,17 @@ const saveRequestAs = async () => {
requestUpdated
)
+ if (RESTTabs.currentActiveTab.value.document.type !== "request") return
+
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
+ type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: `${picked.value.collectionIndex}`,
requestIndex: insertionIndex,
+ exampleID: undefined,
},
}
@@ -303,6 +322,7 @@ const saveRequestAs = async () => {
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
+ type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: picked.value.folderPath,
@@ -341,6 +361,7 @@ const saveRequestAs = async () => {
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
+ type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: picked.value.folderPath,
@@ -541,6 +562,7 @@ const updateTeamCollectionOrFolder = (
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
+ type: "request",
saveContext: {
originLocation: "team-collection",
requestID: createRequestInCollection.id,
diff --git a/packages/hoppscotch-common/src/components/collections/TeamCollections.vue b/packages/hoppscotch-common/src/components/collections/TeamCollections.vue
index 861e94efc..1174b19e2 100644
--- a/packages/hoppscotch-common/src/components/collections/TeamCollections.vue
+++ b/packages/hoppscotch-common/src/components/collections/TeamCollections.vue
@@ -123,7 +123,7 @@
})
"
@dragging="
- (isDraging) =>
+ (isDraging: boolean) =>
highlightChildren(isDraging ? node.data.data.data.id : null)
"
@toggle-children="
@@ -220,7 +220,7 @@
})
"
@dragging="
- (isDraging) =>
+ (isDraging: boolean) =>
highlightChildren(isDraging ? node.data.data.data.id : null)
"
@toggle-children="
@@ -267,6 +267,15 @@
request: node.data.data.data.request,
})
"
+ @edit-response="
+ emit('edit-response', {
+ folderPath: node.data.data.parentIndex,
+ requestIndex: node.data.data.data.id,
+ request: node.data.data.data.request,
+ responseName: $event.responseName,
+ responseID: $event.responseID,
+ })
+ "
@duplicate-request="
node.data.type === 'requests' &&
emit('duplicate-request', {
@@ -274,6 +283,15 @@
request: node.data.data.data.request,
})
"
+ @duplicate-response="
+ emit('duplicate-response', {
+ folderPath: node.data.data.parentIndex,
+ requestIndex: node.data.data.data.id,
+ request: node.data.data.data.request,
+ responseName: $event.responseName,
+ responseID: $event.responseID,
+ })
+ "
@remove-request="
node.data.type === 'requests' &&
emit('remove-request', {
@@ -281,6 +299,15 @@
requestIndex: node.data.data.data.id,
})
"
+ @remove-response="
+ emit('remove-response', {
+ folderPath: node.data.data.parentIndex,
+ requestIndex: node.data.data.data.id,
+ request: node.data.data.data.request,
+ responseName: $event.responseName,
+ responseID: $event.responseID,
+ })
+ "
@select-request="
node.data.type === 'requests' &&
selectRequest({
@@ -289,6 +316,15 @@
folderPath: getPath(node.id),
})
"
+ @select-response="
+ emit('select-response', {
+ responseName: $event.responseName,
+ responseID: $event.responseID,
+ request: node.data.data.data.request,
+ folderPath: getPath(node.id),
+ requestIndex: node.data.data.data.id,
+ })
+ "
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
@@ -488,6 +524,14 @@ const props = defineProps({
const isShowingSearchResults = computed(() => props.filterText.length > 0)
+type ResponsePayload = {
+ folderPath: string
+ requestIndex: string
+ request: HoppRESTRequest
+ responseName: string
+ responseID: string
+}
+
const emit = defineEmits<{
(
event: "add-request",
@@ -537,6 +581,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
}
): void
+ (event: "edit-response", payload: ResponsePayload): void
(
event: "duplicate-request",
payload: {
@@ -544,6 +589,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
}
): void
+ (event: "duplicate-response", payload: ResponsePayload): void
(event: "export-data", payload: TeamCollection): void
(event: "remove-collection", payload: string): void
(event: "remove-folder", payload: string): void
@@ -554,6 +600,7 @@ const emit = defineEmits<{
requestIndex: string
}
): void
+ (event: "remove-response", payload: ResponsePayload): void
(
event: "select-request",
payload: {
@@ -563,6 +610,7 @@ const emit = defineEmits<{
folderPath: string
}
): void
+ (event: "select-response", payload: ResponsePayload): void
(
event: "share-request",
payload: {
@@ -682,7 +730,8 @@ const isActiveRequest = (requestID: string) => {
O.filter(
(active) =>
active.originLocation === "team-collection" &&
- active.requestID === requestID
+ active.requestID === requestID &&
+ active.exampleID === undefined
),
O.isSome
)
@@ -704,7 +753,7 @@ const selectRequest = (data: {
request: request,
requestIndex: requestIndex,
isActive: isActiveRequest(requestIndex),
- folderPath: data.folderPath,
+ folderPath: data.folderPath ?? "",
})
}
}
diff --git a/packages/hoppscotch-common/src/components/collections/index.vue b/packages/hoppscotch-common/src/components/collections/index.vue
index 8ec2c4bb7..2363feef5 100644
--- a/packages/hoppscotch-common/src/components/collections/index.vue
+++ b/packages/hoppscotch-common/src/components/collections/index.vue
@@ -35,25 +35,29 @@
:picked="picked"
@add-folder="addFolder"
@add-request="addRequest"
+ @edit-request="editRequest"
@edit-collection="editCollection"
@edit-folder="editFolder"
+ @edit-response="editResponse"
+ @drop-request="dropRequest"
+ @drop-collection="dropCollection"
+ @display-modal-add="displayModalAdd(true)"
+ @display-modal-import-export="displayModalImportExport(true)"
@duplicate-collection="duplicateCollection"
+ @duplicate-request="duplicateRequest"
+ @duplicate-response="duplicateResponse"
@edit-properties="editProperties"
@export-data="exportData"
@remove-collection="removeCollection"
@remove-folder="removeFolder"
+ @remove-request="removeRequest"
+ @remove-response="removeResponse"
@share-request="shareRequest"
- @drop-collection="dropCollection"
+ @select="selectPicked"
+ @select-response="selectResponse"
+ @select-request="selectRequest"
@update-request-order="updateRequestOrder"
@update-collection-order="updateCollectionOrder"
- @edit-request="editRequest"
- @duplicate-request="duplicateRequest"
- @remove-request="removeRequest"
- @select-request="selectRequest"
- @select="selectPicked"
- @drop-request="dropRequest"
- @display-modal-add="displayModalAdd(true)"
- @display-modal-import-export="displayModalImportExport(true)"
/>
+
(null)
const editingFolderPath = ref(null)
const editingRequest = ref(null)
const editingRequestName = ref("")
+const editingResponseName = ref("")
+const editingResponseOldName = ref("")
const editingRequestIndex = ref(null)
const editingRequestID = ref(null)
+const editingResponseID = ref(null)
const editingProperties = ref({
collection: null,
@@ -665,6 +685,7 @@ const showModalAddFolder = ref(false)
const showModalEditCollection = ref(false)
const showModalEditFolder = ref(false)
const showModalEditRequest = ref(false)
+const showModalEditResponse = ref(false)
const showModalImportExport = ref(false)
const showModalEditProperties = ref(false)
const showConfirmModal = ref(false)
@@ -710,6 +731,12 @@ const displayModalEditRequest = (show: boolean) => {
if (!show) resetSelectedData()
}
+const displayModalEditResponse = (show: boolean) => {
+ showModalEditResponse.value = show
+
+ if (!show) resetSelectedData()
+}
+
const displayModalImportExport = (show: boolean) => {
showModalImportExport.value = show
@@ -796,12 +823,21 @@ const addRequest = (payload: {
}
const requestContext = computed(() => {
- return tabs.currentActiveTab.value.document.request
+ return tabs.currentActiveTab.value.document.type === "request"
+ ? tabs.currentActiveTab.value.document.request
+ : null
})
const onAddRequest = (requestName: string) => {
+ const request =
+ tabs.currentActiveTab.value.document.type === "request"
+ ? tabs.currentActiveTab.value.document.request
+ : getDefaultRESTRequest()
+
+ if (!request) return
+
const newRequest = {
- ...cloneDeep(tabs.currentActiveTab.value.document.request),
+ ...cloneDeep(request),
name: requestName,
}
@@ -815,6 +851,7 @@ const onAddRequest = (requestName: string) => {
tabs.createNewTab({
request: newRequest,
isDirty: false,
+ type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: path,
@@ -869,6 +906,7 @@ const onAddRequest = (requestName: string) => {
tabs.createNewTab({
request: newRequest,
isDirty: false,
+ type: "request",
saveContext: {
originLocation: "team-collection",
requestID: createRequestInCollection.id,
@@ -1127,7 +1165,10 @@ const updateEditingRequest = (newName: string) => {
editRESTRequest(folderPath, requestIndex, requestUpdated)
- if (possibleActiveTab) {
+ if (
+ possibleActiveTab &&
+ possibleActiveTab.value.document.type === "request"
+ ) {
possibleActiveTab.value.document.request.name = requestUpdated.name
nextTick(() => {
possibleActiveTab.value.document.isDirty = false
@@ -1168,7 +1209,7 @@ const updateEditingRequest = (newName: string) => {
requestID,
})
- if (possibleTab) {
+ if (possibleTab && possibleTab.value.document.type === "request") {
possibleTab.value.document.request.name = requestName
nextTick(() => {
possibleTab.value.document.isDirty = false
@@ -1177,6 +1218,171 @@ const updateEditingRequest = (newName: string) => {
}
}
+type ResponseConfigPayload = {
+ folderPath: string | undefined
+ requestIndex: string
+ request: HoppRESTRequest
+ responseName: string
+ responseID: string
+}
+
+const editResponse = (payload: ResponseConfigPayload) => {
+ const { folderPath, requestIndex, request, responseID, responseName } =
+ payload
+
+ editingRequest.value = request
+ editingRequestName.value = request.name ?? ""
+ editingResponseID.value = responseID
+ editingResponseName.value = responseName
+
+ //need to store the old name for updating the response key
+ editingResponseOldName.value = responseName
+ if (collectionsType.value.type === "my-collections" && folderPath) {
+ editingFolderPath.value = folderPath
+ editingRequestIndex.value = parseInt(requestIndex)
+ } else {
+ editingRequestID.value = requestIndex
+ }
+ displayModalEditResponse(true)
+}
+
+const updateEditingResponse = (newName: string) => {
+ const request = cloneDeep(editingRequest.value)
+ if (!request) return
+
+ const responseOldName = editingResponseOldName.value
+
+ if (!responseOldName) return
+
+ if (responseOldName !== newName) {
+ // Convert object to entries array (preserving order)
+ const entries = Object.entries(request.responses)
+
+ // Replace the old key with the new key in the array
+ const updatedEntries = entries.map(([key, value]) =>
+ key === responseOldName
+ ? [newName, { ...value, name: newName }]
+ : [key, value]
+ )
+
+ // Convert the array back into an object
+ request.responses = Object.fromEntries(updatedEntries)
+ }
+
+ if (collectionsType.value.type === "my-collections") {
+ const folderPath = editingFolderPath.value
+ const requestIndex = editingRequestIndex.value
+
+ if (folderPath === null || requestIndex === null) return
+
+ const possibleExampleActiveTab = tabs.getTabRefWithSaveContext({
+ originLocation: "user-collection",
+ requestIndex,
+ folderPath,
+ exampleID: editingResponseID.value ?? undefined,
+ })
+
+ const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
+ originLocation: "user-collection",
+ requestIndex,
+ folderPath,
+ })
+
+ editRESTRequest(folderPath, requestIndex, request)
+
+ if (
+ possibleExampleActiveTab &&
+ possibleExampleActiveTab.value.document.type === "example-response"
+ ) {
+ possibleExampleActiveTab.value.document.response.name = newName
+
+ nextTick(() => {
+ possibleExampleActiveTab.value.document.isDirty = false
+ possibleExampleActiveTab.value.document.saveContext = {
+ originLocation: "user-collection",
+ folderPath: folderPath,
+ requestIndex: requestIndex,
+ exampleID: editingResponseID.value!,
+ }
+ })
+ }
+
+ // update the request tab responses if it's open
+ if (
+ possibleRequestActiveTab &&
+ possibleRequestActiveTab.value.document.type === "request"
+ ) {
+ possibleRequestActiveTab.value.document.request.responses =
+ request.responses
+ }
+
+ displayModalEditResponse(false)
+
+ toast.success(t("response.renamed"))
+ } else if (hasTeamWriteAccess.value) {
+ modalLoadingState.value = true
+
+ const requestID = editingRequestID.value
+
+ if (!requestID) return
+
+ const data = {
+ request: JSON.stringify(request),
+ title: request.name,
+ }
+
+ pipe(
+ updateTeamRequest(requestID, data),
+ TE.match(
+ (err: GQLError) => {
+ toast.error(`${getErrorMessage(err)}`)
+ modalLoadingState.value = false
+ },
+ () => {
+ modalLoadingState.value = false
+ toast.success(t("response.renamed"))
+ displayModalEditResponse(false)
+ }
+ )
+ )()
+
+ const possibleActiveResponseTab = tabs.getTabRefWithSaveContext({
+ originLocation: "team-collection",
+ requestID,
+ exampleID: editingResponseID.value ?? undefined,
+ })
+
+ const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
+ originLocation: "team-collection",
+ requestID,
+ })
+
+ if (
+ possibleActiveResponseTab &&
+ possibleActiveResponseTab.value.document.type === "example-response"
+ ) {
+ possibleActiveResponseTab.value.document.response.name = newName
+ nextTick(() => {
+ possibleActiveResponseTab.value.document.isDirty = false
+ possibleActiveResponseTab.value.document.saveContext = {
+ originLocation: "team-collection",
+ requestID,
+ exampleID: editingResponseID.value!,
+ }
+ })
+ }
+
+ // update the request tab responses if it's open
+ if (
+ possibleRequestActiveTab &&
+ possibleRequestActiveTab.value.document.type === "request"
+ ) {
+ possibleRequestActiveTab.value.document.request.responses =
+ request.responses
+ }
+ }
+}
+
const duplicateRequest = (payload: {
folderPath: string
request: HoppRESTRequest
@@ -1220,6 +1426,93 @@ const duplicateRequest = (payload: {
}
}
+const duplicateResponse = (payload: ResponseConfigPayload) => {
+ const { folderPath, requestIndex, request, responseName } = payload
+
+ const response = request.responses[responseName]
+
+ if (!response || !folderPath || !requestIndex) return
+
+ const newName = `${responseName} - ${t("action.duplicate")}`
+
+ // if the new name is already taken, show a toast and return
+ if (Object.keys(request.responses).includes(newName)) {
+ toast.error(t("response.duplicate_name_error"))
+ return
+ }
+
+ const newResponse = {
+ ...cloneDeep(response),
+ name: newName,
+ }
+
+ const updatedRequest = {
+ ...request,
+ responses: {
+ ...request.responses,
+ [newResponse.name]: newResponse,
+ },
+ }
+
+ if (collectionsType.value.type === "my-collections") {
+ editRESTRequest(folderPath, parseInt(requestIndex), updatedRequest)
+ toast.success(t("response.duplicated"))
+
+ const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
+ originLocation: "user-collection",
+ requestIndex: parseInt(requestIndex),
+ folderPath,
+ })
+
+ // update the request tab responses if it's open
+ if (
+ possibleRequestActiveTab &&
+ possibleRequestActiveTab.value.document.type === "request"
+ ) {
+ possibleRequestActiveTab.value.document.request.responses =
+ updatedRequest.responses
+ }
+ } else if (hasTeamWriteAccess.value) {
+ duplicateRequestLoading.value = true
+
+ if (!collectionsType.value.selectedTeam) return
+
+ const data = {
+ request: JSON.stringify(updatedRequest),
+ title: request.name,
+ }
+
+ pipe(
+ updateTeamRequest(requestIndex, data),
+ TE.match(
+ (err: GQLError) => {
+ toast.error(`${getErrorMessage(err)}`)
+ modalLoadingState.value = false
+ },
+ () => {
+ modalLoadingState.value = false
+ toast.success(t("response.duplicated"))
+ displayModalEditResponse(false)
+ }
+ )
+ )()
+
+ // update the request tab responses if it's open
+ const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
+ originLocation: "team-collection",
+ requestID: requestIndex,
+ })
+
+ if (
+ possibleRequestActiveTab &&
+ possibleRequestActiveTab.value.document.type === "request"
+ ) {
+ possibleRequestActiveTab.value.document.request.responses =
+ updatedRequest.responses
+ }
+ }
+}
+
const removeCollection = (id: string) => {
if (collectionsType.value.type === "my-collections")
editingCollectionIndex.value = parseInt(id)
@@ -1406,9 +1699,12 @@ const onRemoveRequest = () => {
})
// If there is a tab attached to this request, dissociate its state and mark it dirty
- if (possibleTab) {
+ if (possibleTab && possibleTab.value.document.type === "request") {
possibleTab.value.document.saveContext = null
possibleTab.value.document.isDirty = true
+
+ // since the request is deleted, we need to remove the saved responses as well
+ possibleTab.value.document.request.responses = {}
}
const requestToRemove = navigateToFolderWithIndexPath(
@@ -1464,9 +1760,176 @@ const onRemoveRequest = () => {
requestID,
})
- if (possibleTab) {
+ if (possibleTab && possibleTab.value.document.type === "request") {
possibleTab.value.document.saveContext = null
possibleTab.value.document.isDirty = true
+
+ // since the request is deleted, we need to remove the saved responses as well
+ possibleTab.value.document.request.responses = {}
+ }
+ }
+}
+
+const removeResponse = (payload: ResponseConfigPayload) => {
+ const { folderPath, requestIndex, request, responseID, responseName } =
+ payload
+ if (collectionsType.value.type === "my-collections" && folderPath) {
+ editingFolderPath.value = folderPath
+ editingRequestIndex.value = parseInt(requestIndex)
+ editingResponseID.value = responseID
+ editingRequest.value = request
+ editingResponseName.value = responseName
+ } else {
+ editingRequestID.value = requestIndex
+ editingResponseID.value = payload.responseID
+ editingRequest.value = request
+ editingResponseName.value = responseName
+ }
+ confirmModalTitle.value = `${t("confirm.remove_response")}`
+ displayConfirmModal(true)
+}
+
+const onRemoveResponse = () => {
+ const request = cloneDeep(editingRequest.value)
+
+ if (!request) return
+
+ const responseName = editingResponseName.value
+ const responseID = editingResponseID.value
+
+ delete request.responses[responseName]
+
+ const requestUpdated: HoppRESTRequest = {
+ ...request,
+ }
+
+ if (collectionsType.value.type === "my-collections") {
+ const folderPath = editingFolderPath.value
+ const requestIndex = editingRequestIndex.value
+
+ if (folderPath === null || requestIndex === null) return
+
+ editRESTRequest(folderPath, requestIndex, requestUpdated)
+
+ const possibleActiveResponseTab = tabs.getTabRefWithSaveContext({
+ originLocation: "user-collection",
+ folderPath,
+ requestIndex,
+ exampleID: responseID ?? undefined,
+ })
+
+ const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
+ originLocation: "user-collection",
+ requestIndex,
+ folderPath,
+ })
+
+ // If there is a tab attached to this request, close it and set the active tab to the first one
+ if (
+ possibleActiveResponseTab &&
+ possibleActiveResponseTab.value.document.type === "example-response"
+ ) {
+ const activeTabs = tabs.getActiveTabs()
+
+ // if the last tab is the one we are closing, we need to create a new tab
+ if (
+ activeTabs.value.length === 1 &&
+ activeTabs.value[0].id === possibleActiveResponseTab.value.id
+ ) {
+ tabs.createNewTab({
+ request: getDefaultRESTRequest(),
+ isDirty: false,
+ type: "request",
+ saveContext: undefined,
+ })
+ tabs.closeTab(possibleActiveResponseTab.value.id)
+ } else {
+ tabs.closeTab(possibleActiveResponseTab.value.id)
+ tabs.setActiveTab(activeTabs.value[0].id)
+ }
+ }
+
+ // update the request tab responses if it's open
+ if (
+ possibleRequestActiveTab &&
+ possibleRequestActiveTab.value.document.type === "request"
+ ) {
+ possibleRequestActiveTab.value.document.request.responses =
+ requestUpdated.responses
+ }
+
+ toast.success(t("state.deleted"))
+ displayConfirmModal(false)
+ } else if (hasTeamWriteAccess.value) {
+ const requestID = editingRequestID.value
+
+ if (!requestID) return
+
+ modalLoadingState.value = true
+
+ const data = {
+ request: JSON.stringify(requestUpdated),
+ title: request.name,
+ }
+
+ pipe(
+ updateTeamRequest(requestID, data),
+ TE.match(
+ (err: GQLError) => {
+ toast.error(`${getErrorMessage(err)}`)
+ modalLoadingState.value = false
+ },
+ () => {
+ modalLoadingState.value = false
+ toast.success(t("state.deleted"))
+ displayConfirmModal(false)
+ }
+ )
+ )()
+
+ const possibleActiveResponseTab = tabs.getTabRefWithSaveContext({
+ originLocation: "team-collection",
+ requestID,
+ exampleID: responseID ?? undefined,
+ })
+
+ const possibleRequestActiveTab = tabs.getTabRefWithSaveContext({
+ originLocation: "team-collection",
+ requestID,
+ })
+
+ // If there is a tab attached to this request, close it and set the active tab to the first one
+ if (
+ possibleActiveResponseTab &&
+ possibleActiveResponseTab.value.document.type === "example-response"
+ ) {
+ const activeTabs = tabs.getActiveTabs()
+
+ // if the last tab is the one we are closing, we need to create a new tab
+ if (
+ activeTabs.value.length === 1 &&
+ activeTabs.value[0].id === possibleActiveResponseTab.value.id
+ ) {
+ tabs.createNewTab({
+ request: getDefaultRESTRequest(),
+ isDirty: false,
+ type: "request",
+ saveContext: undefined,
+ })
+ tabs.closeTab(possibleActiveResponseTab.value.id)
+ } else {
+ tabs.closeTab(possibleActiveResponseTab.value.id)
+ tabs.setActiveTab(activeTabs.value[0].id)
+ }
+ }
+
+ // update the request tab responses if it's open
+ if (
+ possibleRequestActiveTab &&
+ possibleRequestActiveTab.value.document.type === "request"
+ ) {
+ possibleRequestActiveTab.value.document.request.responses =
+ requestUpdated.responses
}
}
}
@@ -1516,10 +1979,12 @@ const selectRequest = (selectedRequest: {
tabs.createNewTab({
request: cloneDeep(request),
isDirty: false,
+ type: "request",
saveContext: {
originLocation: "team-collection",
requestID: requestIndex,
collectionID: folderPath,
+ exampleID: undefined,
},
inheritedProperties: inheritedProperties,
})
@@ -1541,6 +2006,7 @@ const selectRequest = (selectedRequest: {
tabs.createNewTab({
request: cloneDeep(request),
isDirty: false,
+ type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: folderPath!,
@@ -1555,6 +2021,72 @@ const selectRequest = (selectedRequest: {
}
}
+const selectResponse = (payload: {
+ folderPath: string
+ requestIndex: string
+ responseName: string
+ request: HoppRESTRequest
+ responseID: string
+}) => {
+ const { folderPath, requestIndex, responseName, request, responseID } =
+ payload
+
+ const response = request.responses[responseName]
+
+ if (collectionsType.value.type === "my-collections") {
+ const possibleTab = tabs.getTabRefWithSaveContext({
+ originLocation: "user-collection",
+ requestIndex: parseInt(requestIndex),
+ folderPath: folderPath!,
+ exampleID: responseID,
+ })
+
+ if (possibleTab) {
+ tabs.setActiveTab(possibleTab.value.id)
+ } else {
+ tabs.createNewTab({
+ response: {
+ ...cloneDeep(response),
+ name: responseName,
+ },
+ isDirty: false,
+ type: "example-response",
+ saveContext: {
+ originLocation: "user-collection",
+ folderPath: folderPath!,
+ requestIndex: parseInt(requestIndex),
+ exampleID: responseID,
+ },
+ })
+ }
+ } else {
+ const possibleTab = tabs.getTabRefWithSaveContext({
+ originLocation: "team-collection",
+ requestID: requestIndex,
+ exampleID: responseID,
+ })
+
+ if (possibleTab) {
+ tabs.setActiveTab(possibleTab.value.id)
+ } else {
+ tabs.createNewTab({
+ response: {
+ ...cloneDeep(response),
+ name: responseName,
+ },
+ isDirty: false,
+ type: "example-response",
+ saveContext: {
+ originLocation: "team-collection",
+ requestID: requestIndex,
+ collectionID: folderPath,
+ exampleID: responseID,
+ },
+ })
+ }
+ }
+}
+
/**
* Used to get the index of the request from the path
* @param path The path of the request
@@ -1593,7 +2125,7 @@ const dropRequest = (payload: {
})
// If there is a tab attached to this request, change save its save context
- if (possibleTab) {
+ if (possibleTab && possibleTab.value.document.type === "request") {
possibleTab.value.document.saveContext = {
originLocation: "user-collection",
folderPath: destinationCollectionIndex,
@@ -1654,7 +2186,7 @@ const dropRequest = (payload: {
requestID: requestIndex,
})
- if (possibleTab) {
+ if (possibleTab && possibleTab.value.document.type === "request") {
possibleTab.value.document.saveContext = {
originLocation: "team-collection",
requestID: requestIndex,
@@ -1872,6 +2404,13 @@ const dropToRoot = ({ dataTransfer }: DragEvent) => {
} else {
moveRESTFolder(collectionIndexDragged, null)
toast.success(`${t("collection.moved")}`)
+
+ const rootLength = myCollections.value.length
+
+ updateSaveContextForAffectedRequests(
+ collectionIndexDragged,
+ `${rootLength - 1}`
+ )
}
draggingToRoot.value = false
@@ -2352,6 +2891,7 @@ const resolveConfirmModal = (title: string | null) => {
if (title === `${t("confirm.remove_collection")}`) onRemoveCollection()
else if (title === `${t("confirm.remove_request")}`) onRemoveRequest()
else if (title === `${t("confirm.remove_folder")}`) onRemoveFolder()
+ else if (title === `${t("confirm.remove_response")}`) onRemoveResponse()
else {
console.error(
`Confirm modal title ${title} is not handled by the component`
diff --git a/packages/hoppscotch-common/src/components/embeds/Request.vue b/packages/hoppscotch-common/src/components/embeds/Request.vue
index 3fb78ff07..c072c3b23 100644
--- a/packages/hoppscotch-common/src/components/embeds/Request.vue
+++ b/packages/hoppscotch-common/src/components/embeds/Request.vue
@@ -64,13 +64,13 @@ import { useStreamSubscriber } from "~/composables/stream"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { HoppTab } from "~/services/tab"
-import { HoppRESTDocument } from "~/helpers/rest/document"
+import { HoppRequestDocument } from "~/helpers/rest/document"
const toast = useToast()
const t = useI18n()
const props = defineProps<{
- modelTab: HoppTab
+ modelTab: HoppTab
sharedRequestURL: string
}>()
diff --git a/packages/hoppscotch-common/src/components/embeds/index.vue b/packages/hoppscotch-common/src/components/embeds/index.vue
index 3aed03f21..a5d57f2e1 100644
--- a/packages/hoppscotch-common/src/components/embeds/index.vue
+++ b/packages/hoppscotch-common/src/components/embeds/index.vue
@@ -27,11 +27,11 @@
import { computed, useModel } from "vue"
import { ref } from "vue"
import { HoppTab } from "~/services/tab"
-import { HoppRESTDocument } from "~/helpers/rest/document"
+import { HoppRequestDocument } from "~/helpers/rest/document"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
const props = defineProps<{
- modelTab: HoppTab
+ modelTab: HoppTab
properties: RESTOptionTabs[]
sharedRequestID: string
}>()
diff --git a/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue b/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue
index f54aa333a..afadb96ae 100644
--- a/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue
+++ b/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue
@@ -75,6 +75,7 @@ import { InterceptorService } from "~/services/interceptor.service"
import { editGraphqlRequest } from "~/newstore/collections"
import { GQLTabService } from "~/services/tab/graphql"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
+import { HoppRESTHeaders } from "@hoppscotch/data"
const VALID_GQL_OPERATIONS = [
"query",
@@ -158,7 +159,10 @@ const runQuery = async (
let runHeaders: HoppGQLRequest["headers"] = []
if (inheritedHeaders) {
- runHeaders = [...inheritedHeaders, ...clone(request.value.headers)]
+ runHeaders = [
+ ...inheritedHeaders,
+ ...clone(request.value.headers),
+ ] as HoppRESTHeaders
} else {
runHeaders = clone(request.value.headers)
}
@@ -266,7 +270,7 @@ const changeOptionTab = (e: GQLOptionTabs) => {
}
defineActionHandler("request.send-cancel", runQuery)
-defineActionHandler("request.save", saveRequest)
+defineActionHandler("request-response.save", saveRequest)
defineActionHandler("request.save-as", () => {
showSaveRequestModal.value = true
})
diff --git a/packages/hoppscotch-common/src/components/history/index.vue b/packages/hoppscotch-common/src/components/history/index.vue
index 2c5d5b318..04f82a0a8 100644
--- a/packages/hoppscotch-common/src/components/history/index.vue
+++ b/packages/hoppscotch-common/src/components/history/index.vue
@@ -300,6 +300,7 @@ const useHistory = (entry: RESTHistoryEntry) => {
tabs.createNewTab({
request: entry.request,
isDirty: false,
+ type: "request",
})
}
diff --git a/packages/hoppscotch-common/src/components/http/ImportCurl.vue b/packages/hoppscotch-common/src/components/http/ImportCurl.vue
index 36deeaa01..ff2c17380 100644
--- a/packages/hoppscotch-common/src/components/http/ImportCurl.vue
+++ b/packages/hoppscotch-common/src/components/http/ImportCurl.vue
@@ -147,6 +147,8 @@ const handleImport = () => {
type: "HOPP_REST_IMPORT_CURL",
})
+ if (tabs.currentActiveTab.value.document.type === "example-response") return
+
tabs.currentActiveTab.value.document.request = req
} catch (e) {
console.error(e)
diff --git a/packages/hoppscotch-common/src/components/http/Request.vue b/packages/hoppscotch-common/src/components/http/Request.vue
index e486f1a4b..e84360fed 100644
--- a/packages/hoppscotch-common/src/components/http/Request.vue
+++ b/packages/hoppscotch-common/src/components/http/Request.vue
@@ -263,7 +263,7 @@ import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection"
import { InterceptorService } from "~/services/interceptor.service"
import { HoppTab } from "~/services/tab"
-import { HoppRESTDocument } from "~/helpers/rest/document"
+import { HoppRequestDocument } from "~/helpers/rest/document"
import { RESTTabService } from "~/services/tab/rest"
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
import { WorkspaceService } from "~/services/workspace.service"
@@ -288,7 +288,7 @@ const toast = useToast()
const { subscribeToStream } = useStreamSubscriber()
-const props = defineProps<{ modelValue: HoppTab }>()
+const props = defineProps<{ modelValue: HoppTab }>()
const emit = defineEmits(["update:modelValue"])
const tab = useVModel(props, "modelValue", emit)
@@ -583,10 +583,10 @@ defineActionHandler("request.reset", clearContent)
defineActionHandler("request.share-request", shareRequest)
defineActionHandler("request.method.next", cycleDownMethod)
defineActionHandler("request.method.prev", cycleUpMethod)
-defineActionHandler("request.save", saveRequest)
+defineActionHandler("request-response.save", saveRequest)
defineActionHandler("request.save-as", (req) => {
showSaveRequestModal.value = true
- if (req?.requestType === "rest") {
+ if (req?.requestType === "rest" && req.request) {
request.value = req.request
}
})
diff --git a/packages/hoppscotch-common/src/components/http/RequestOptions.vue b/packages/hoppscotch-common/src/components/http/RequestOptions.vue
index 55c079e17..43b913fb6 100644
--- a/packages/hoppscotch-common/src/components/http/RequestOptions.vue
+++ b/packages/hoppscotch-common/src/components/http/RequestOptions.vue
@@ -50,26 +50,35 @@
/>
-
+
-
+
import { useI18n } from "@composables/i18n"
-import { HoppRESTRequest } from "@hoppscotch/data"
+import {
+ HoppRESTRequest,
+ HoppRESTResponseOriginalRequest,
+} from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { computed } from "vue"
import { defineActionHandler } from "~/helpers/actions"
@@ -109,7 +121,7 @@ const t = useI18n()
// v-model integration with props and emit
const props = withDefaults(
defineProps<{
- modelValue: HoppRESTRequest
+ modelValue: HoppRESTRequest | HoppRESTResponseOriginalRequest
optionTab: RESTOptionTabs
properties?: string[]
inheritedProperties?: HoppInheritedProperty
@@ -128,6 +140,17 @@ const emit = defineEmits<{
const request = useVModel(props, "modelValue", emit)
const selectedOptionTab = useVModel(props, "optionTab", emit)
+const showPreRequestScriptTab = computed(() => {
+ return (
+ props.properties?.includes("preRequestScript") ??
+ "preRequestScript" in request.value
+ )
+})
+
+const showTestsTab = computed(() => {
+ return props.properties?.includes("tests") ?? "testScript" in request.value
+})
+
const changeOptionTab = (e: RESTOptionTabs) => {
selectedOptionTab.value = e
}
diff --git a/packages/hoppscotch-common/src/components/http/RequestTab.vue b/packages/hoppscotch-common/src/components/http/RequestTab.vue
index f009f96d8..3fc63907a 100644
--- a/packages/hoppscotch-common/src/components/http/RequestTab.vue
+++ b/packages/hoppscotch-common/src/components/http/RequestTab.vue
@@ -20,14 +20,14 @@ import { useVModel } from "@vueuse/core"
import { cloneDeep } from "lodash-es"
import { isEqualHoppRESTRequest } from "@hoppscotch/data"
import { HoppTab } from "~/services/tab"
-import { HoppRESTDocument } from "~/helpers/rest/document"
+import { HoppRequestDocument } from "~/helpers/rest/document"
// TODO: Move Response and Request execution code to over here
-const props = defineProps<{ modelValue: HoppTab }>()
+const props = defineProps<{ modelValue: HoppTab }>()
const emit = defineEmits<{
- (e: "update:modelValue", val: HoppTab): void
+ (e: "update:modelValue", val: HoppTab): void
}>()
const tab = useVModel(props, "modelValue", emit)
diff --git a/packages/hoppscotch-common/src/components/http/Response.vue b/packages/hoppscotch-common/src/components/http/Response.vue
index fa4748ac2..0869b32d2 100644
--- a/packages/hoppscotch-common/src/components/http/Response.vue
+++ b/packages/hoppscotch-common/src/components/http/Response.vue
@@ -4,22 +4,44 @@
+
diff --git a/packages/hoppscotch-common/src/components/http/ResponseInterface.vue b/packages/hoppscotch-common/src/components/http/ResponseInterface.vue
index 0927b3f8d..fc83716c4 100644
--- a/packages/hoppscotch-common/src/components/http/ResponseInterface.vue
+++ b/packages/hoppscotch-common/src/components/http/ResponseInterface.vue
@@ -179,9 +179,15 @@ const response = computed(() => {
const pageCategory = getCurrentPageCategory()
if (pageCategory === "rest") {
- const res = restTabs.currentActiveTab.value.document.response
- if (res?.type === "success" || res?.type === "fail") {
- response = getResponseBodyText(res.body)
+ const doc = restTabs.currentActiveTab.value.document
+ if (doc.type === "request") {
+ const res = doc.response
+ if (res?.type === "success" || res?.type === "fail") {
+ response = getResponseBodyText(res.body)
+ }
+ } else {
+ const res = doc.response.body
+ response = res
}
}
@@ -244,6 +250,7 @@ const filteredResponseInterfaces = computed(() => {
const { copyIcon, copyResponse } = useCopyResponse(interfaceCode)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"",
- interfaceCode
+ interfaceCode,
+ t("filename.response_interface")
)
diff --git a/packages/hoppscotch-common/src/components/http/SaveResponseName.vue b/packages/hoppscotch-common/src/components/http/SaveResponseName.vue
new file mode 100644
index 000000000..4ce36e2d5
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/http/SaveResponseName.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/http/TabHead.vue b/packages/hoppscotch-common/src/components/http/TabHead.vue
index 5f90c7056..5aa3549eb 100644
--- a/packages/hoppscotch-common/src/components/http/TabHead.vue
+++ b/packages/hoppscotch-common/src/components/http/TabHead.vue
@@ -1,17 +1,20 @@
- {{ tab.document.request.method }}
+ {{ tabState.method }}
- {{ tab.document.request.name }}
+ {{ tabState.name }}
diff --git a/packages/hoppscotch-common/src/components/http/example/Response.vue b/packages/hoppscotch-common/src/components/http/example/Response.vue
new file mode 100644
index 000000000..b99dfcf90
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/http/example/Response.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/http/example/ResponseMeta.vue b/packages/hoppscotch-common/src/components/http/example/ResponseMeta.vue
new file mode 100644
index 000000000..2063ab039
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/http/example/ResponseMeta.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
{{ t("response.status") }}:
+
+ {
+ setResponseStatusCode(statusCode)
+ }
+ "
+ />
+
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/http/example/ResponseRequest.vue b/packages/hoppscotch-common/src/components/http/example/ResponseRequest.vue
new file mode 100644
index 000000000..de0767e93
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/http/example/ResponseRequest.vue
@@ -0,0 +1,265 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/http/example/ResponseTab.vue b/packages/hoppscotch-common/src/components/http/example/ResponseTab.vue
new file mode 100644
index 000000000..3c415318e
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/http/example/ResponseTab.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/lenses/HeadersRenderer.vue b/packages/hoppscotch-common/src/components/lenses/HeadersRenderer.vue
index 2c8cdffe3..5673e9f66 100644
--- a/packages/hoppscotch-common/src/components/lenses/HeadersRenderer.vue
+++ b/packages/hoppscotch-common/src/components/lenses/HeadersRenderer.vue
@@ -19,7 +19,10 @@
@@ -32,23 +35,35 @@ import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import type { HoppRESTResponseHeader } from "~/helpers/types/HoppRESTResponse"
+import { useVModel } from "@vueuse/core"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
- headers: HoppRESTResponseHeader[]
+ modelValue: HoppRESTResponseHeader[]
+ isEditable: boolean
}>()
+const emit = defineEmits<{
+ (e: "update:modelValue"): void
+}>()
+
+const headers = useVModel(props, "modelValue", emit)
+
const copyIcon = refAutoReset(
IconCopy,
1000
)
const copyHeaders = () => {
- copyToClipboard(JSON.stringify(props.headers))
+ copyToClipboard(JSON.stringify(props.modelValue))
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
+
+const deleteHeader = (index: number) => {
+ headers.value.splice(index, 1)
+}
diff --git a/packages/hoppscotch-common/src/components/lenses/HeadersRendererEntry.vue b/packages/hoppscotch-common/src/components/lenses/HeadersRendererEntry.vue
index 2d2432141..f56530b2f 100644
--- a/packages/hoppscotch-common/src/components/lenses/HeadersRendererEntry.vue
+++ b/packages/hoppscotch-common/src/components/lenses/HeadersRendererEntry.vue
@@ -2,25 +2,47 @@
-
-
- {{ header.key }}
+
+
+ {{ headerKey }}
+
-
- {{ header.value }}
+
+ {{ headerValue }}
+
+
@@ -28,21 +50,36 @@
diff --git a/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue b/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue
index f6e8e7217..7a496f109 100644
--- a/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue
+++ b/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue
@@ -13,7 +13,10 @@
>
-
+
()
const emit = defineEmits<{
- (e: "update:document", document: HoppRESTDocument): void
+ (e: "update:document", document: HoppRequestDocument): void
+ (e: "save-as-example"): void
}>()
const doc = useVModel(props, "document", emit)
+const isSavable = computed(() => {
+ return doc.value.response?.type === "success" && doc.value.saveContext
+})
+
const showIndicator = computed(() => {
if (!doc.value.testResults) return false
diff --git a/packages/hoppscotch-common/src/components/lenses/renderers/HTMLLensRenderer.vue b/packages/hoppscotch-common/src/components/lenses/renderers/HTMLLensRenderer.vue
index 9d10236e1..2d54445c9 100644
--- a/packages/hoppscotch-common/src/components/lenses/renderers/HTMLLensRenderer.vue
+++ b/packages/hoppscotch-common/src/components/lenses/renderers/HTMLLensRenderer.vue
@@ -2,6 +2,7 @@