From 47e009267b77c859d7c2f771311a0fe58a5b7bbb Mon Sep 17 00:00:00 2001 From: Nivedin <53208152+nivedin@users.noreply.github.com> Date: Wed, 13 Dec 2023 22:43:18 +0530 Subject: [PATCH] feat: collection level headers and authorization (#3505) Co-authored-by: Andrew Bastin --- .../hoppscotch-cli/src/types/collections.ts | 4 +- packages/hoppscotch-cli/src/types/request.ts | 2 +- packages/hoppscotch-cli/src/utils/checks.ts | 11 +- .../hoppscotch-cli/src/utils/collections.ts | 94 ++- packages/hoppscotch-cli/src/utils/mutators.ts | 34 +- .../assets/themes/base-themes.scss | 1 + packages/hoppscotch-common/locales/en.json | 5 + .../hoppscotch-common/src/components.d.ts | 1 + .../app/spotlight/entry/GQLRequest.vue | 4 +- .../app/spotlight/entry/RESTRequest.vue | 4 +- .../src/components/collections/Collection.vue | 24 +- .../components/collections/ImportExport.vue | 41 +- .../components/collections/MyCollections.vue | 41 +- .../src/components/collections/Properties.vue | 166 +++++ .../components/collections/SaveRequest.vue | 61 ++ .../collections/TeamCollections.vue | 32 +- .../components/collections/graphql/Add.vue | 92 +-- .../collections/graphql/AddFolder.vue | 78 ++- .../collections/graphql/Collection.vue | 67 +- .../components/collections/graphql/Edit.vue | 13 +- .../collections/graphql/EditFolder.vue | 83 ++- .../collections/graphql/EditRequest.vue | 92 ++- .../components/collections/graphql/Folder.vue | 87 ++- .../collections/graphql/ImportExport.vue | 32 +- .../collections/graphql/Request.vue | 90 +-- .../components/collections/graphql/index.vue | 660 ++++++++++++------ .../src/components/collections/index.vue | 334 ++++++++- .../src/components/graphql/Authorization.vue | 45 ++ .../src/components/graphql/Headers.vue | 310 +++++++- .../src/components/graphql/RequestOptions.vue | 55 +- .../src/components/graphql/RequestTab.vue | 1 + .../src/components/http/Authorization.vue | 51 +- .../src/components/http/Headers.vue | 137 +++- .../src/components/http/RequestOptions.vue | 13 +- .../src/components/http/RequestTab.vue | 1 + .../ImportExportSteps/MyCollectionImport.vue | 4 +- .../src/helpers/RequestRunner.ts | 42 +- .../mutations/UpdateTeamCollection.graphql | 15 + .../gql/queries/GetCollectionChildren.graphql | 1 + .../gql/queries/GetCollectionTitle.graphql | 5 - .../queries/GetCollectionTitleAndData.graphql | 6 + .../gql/queries/GetSingleCollection.graphql | 1 + .../gql/queries/RootCollectionsOfTeam.graphql | 1 + .../subscriptions/TeamCollectionAdded.graphql | 1 + .../TeamCollectionUpdated.graphql | 1 + .../src/helpers/backend/helpers.ts | 32 +- .../backend/mutations/TeamCollection.ts | 18 + .../src/helpers/collection/collection.ts | 137 +++- .../src/helpers/collection/request.ts | 10 +- .../src/helpers/graphql/default.ts | 2 +- .../src/helpers/graphql/document.ts | 7 + .../import-export/export/gqlCollections.ts | 6 +- .../import-export/export/myCollections.ts | 6 +- .../src/helpers/import-export/import/hopp.ts | 41 +- .../helpers/import-export/import/hoppGql.ts | 6 +- .../helpers/import-export/import/insomnia.ts | 2 +- .../helpers/import-export/import/openapi.ts | 5 +- .../helpers/import-export/import/postman.ts | 2 +- .../src/helpers/rest/document.ts | 7 + .../src/helpers/teams/TeamCollection.ts | 1 + .../helpers/teams/TeamCollectionAdapter.ts | 111 ++- .../helpers/types/HoppInheritedProperties.ts | 19 + .../src/helpers/utils/EffectiveURL.ts | 59 +- .../src/newstore/collections.ts | 188 +++-- .../hoppscotch-common/src/pages/import.vue | 6 +- .../persistence/__tests__/__mocks__/index.ts | 15 +- .../persistence/validation-schemas/index.ts | 27 + .../searchers/collections.searcher.ts | 26 +- .../hoppscotch-data/src/collection/index.ts | 80 ++- .../hoppscotch-data/src/collection/v/1.ts | 36 + .../hoppscotch-data/src/collection/v/2.ts | 57 ++ packages/hoppscotch-data/src/graphql/index.ts | 1 + packages/hoppscotch-data/src/graphql/v/2.ts | 47 +- packages/hoppscotch-data/src/rest/index.ts | 2 + packages/hoppscotch-data/src/rest/v/1.ts | 100 ++- packages/hoppscotch-data/tsconfig.decl.json | 2 +- packages/hoppscotch-data/tsconfig.json | 2 +- .../collections/collections.platform.ts | 12 +- .../platform/collections/collections.sync.ts | 2 +- .../collections/gqlCollections.sync.ts | 2 +- .../CreateGQLChildUserCollection.graphql | 3 + .../CreateGQLRootUserCollection.graphql | 5 +- .../CreateRESTChildUserCollection.graphql | 3 + .../CreateRESTRootUserCollection.graphql | 5 +- .../mutations/UpdateUserCollection.graphql | 15 + .../queries/GetRootGQLUserCollections.graphql | 2 + .../queries/GetUserRootCollections.graphql | 2 + .../UserCollectionCreated.graphql | 1 + .../UserCollectionUpdated.graphql | 1 + .../platform/collections/collections.api.ts | 28 +- .../collections/collections.platform.ts | 61 +- .../platform/collections/collections.sync.ts | 70 +- .../collections/gqlCollections.sync.ts | 71 +- .../src/assets/scss/histoire.scss | 1 + .../src/components/smart/Modal.vue | 4 +- 95 files changed, 3221 insertions(+), 970 deletions(-) create mode 100644 packages/hoppscotch-common/src/components/collections/Properties.vue create mode 100644 packages/hoppscotch-common/src/helpers/backend/gql/mutations/UpdateTeamCollection.graphql delete mode 100644 packages/hoppscotch-common/src/helpers/backend/gql/queries/GetCollectionTitle.graphql create mode 100644 packages/hoppscotch-common/src/helpers/backend/gql/queries/GetCollectionTitleAndData.graphql create mode 100644 packages/hoppscotch-common/src/helpers/types/HoppInheritedProperties.ts create mode 100644 packages/hoppscotch-data/src/collection/v/1.ts create mode 100644 packages/hoppscotch-data/src/collection/v/2.ts create mode 100644 packages/hoppscotch-selfhost-web/src/api/mutations/UpdateUserCollection.graphql diff --git a/packages/hoppscotch-cli/src/types/collections.ts b/packages/hoppscotch-cli/src/types/collections.ts index 0b341fc4b..f5483e2d4 100644 --- a/packages/hoppscotch-cli/src/types/collections.ts +++ b/packages/hoppscotch-cli/src/types/collections.ts @@ -1,8 +1,8 @@ -import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; +import { HoppCollection } from "@hoppscotch/data"; import { HoppEnvs } from "./request"; export type CollectionRunnerParam = { - collections: HoppCollection[]; + collections: HoppCollection[]; envs: HoppEnvs; delay?: number; }; diff --git a/packages/hoppscotch-cli/src/types/request.ts b/packages/hoppscotch-cli/src/types/request.ts index 9e24ff7c8..2407542f1 100644 --- a/packages/hoppscotch-cli/src/types/request.ts +++ b/packages/hoppscotch-cli/src/types/request.ts @@ -33,7 +33,7 @@ export type HoppEnvs = { export type CollectionStack = { path: string; - collection: HoppCollection; + collection: HoppCollection; }; export type RequestReport = { diff --git a/packages/hoppscotch-cli/src/utils/checks.ts b/packages/hoppscotch-cli/src/utils/checks.ts index dc184b528..f935cbe4a 100644 --- a/packages/hoppscotch-cli/src/utils/checks.ts +++ b/packages/hoppscotch-cli/src/utils/checks.ts @@ -1,8 +1,4 @@ -import { - HoppCollection, - HoppRESTRequest, - isHoppRESTRequest, -} from "@hoppscotch/data"; +import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data"; import * as A from "fp-ts/Array"; import { CommanderError } from "commander"; import { HoppCLIError, HoppErrnoException } from "../types/errors"; @@ -24,9 +20,7 @@ export const hasProperty =

( * @returns True, if unknown parameter is valid Hoppscotch REST Collection; * False, otherwise. */ -export const isRESTCollection = ( - param: unknown -): param is HoppCollection => { +export const isRESTCollection = (param: unknown): param is HoppCollection => { if (!!param && typeof param === "object") { if (!hasProperty(param, "v") || typeof param.v !== "number") { return false; @@ -62,7 +56,6 @@ export const isRESTCollection = ( return false; }; - /** * Checks if given error data is of type HoppCLIError, based on existence * of code property. diff --git a/packages/hoppscotch-cli/src/utils/collections.ts b/packages/hoppscotch-cli/src/utils/collections.ts index c671d7c96..f13245513 100644 --- a/packages/hoppscotch-cli/src/utils/collections.ts +++ b/packages/hoppscotch-cli/src/utils/collections.ts @@ -3,7 +3,7 @@ import { pipe } from "fp-ts/function"; import { bold } from "chalk"; import { log } from "console"; import round from "lodash/round"; -import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; +import { HoppCollection } from "@hoppscotch/data"; import { HoppEnvs, CollectionStack, @@ -41,58 +41,58 @@ const { WARN, FAIL } = exceptionColors; * @param param Data of hopp-collection with hopp-requests, envs to be processed. * @returns List of report for each processed request. */ -export const collectionsRunner = - async (param: CollectionRunnerParam): Promise => - { - const envs: HoppEnvs = param.envs; - const delay = param.delay ?? 0; - const requestsReport: RequestReport[] = []; - const collectionStack: CollectionStack[] = getCollectionStack( - param.collections - ); +export const collectionsRunner = async ( + param: CollectionRunnerParam +): Promise => { + const envs: HoppEnvs = param.envs; + const delay = param.delay ?? 0; + const requestsReport: RequestReport[] = []; + const collectionStack: CollectionStack[] = getCollectionStack( + param.collections + ); - while (collectionStack.length) { - // Pop out top-most collection from stack to be processed. - const { collection, path } = collectionStack.pop(); + while (collectionStack.length) { + // Pop out top-most collection from stack to be processed. + const { collection, path } = collectionStack.pop(); - // Processing each request in collection - for (const request of collection.requests) { - const _request = preProcessRequest(request); - const requestPath = `${path}/${_request.name}`; - const processRequestParams: ProcessRequestParams = { - path: requestPath, - request: _request, - envs, - delay, - }; + // Processing each request in collection + for (const request of collection.requests) { + const _request = preProcessRequest(request); + const requestPath = `${path}/${_request.name}`; + const processRequestParams: ProcessRequestParams = { + path: requestPath, + request: _request, + envs, + delay, + }; - // Request processing initiated message. - log(WARN(`\nRunning: ${bold(requestPath)}`)); + // Request processing initiated message. + log(WARN(`\nRunning: ${bold(requestPath)}`)); - // Processing current request. - const result = await processRequest(processRequestParams)(); + // Processing current request. + const result = await processRequest(processRequestParams)(); - // Updating global & selected envs with new envs from processed-request output. - const { global, selected } = result.envs; - envs.global = global; - envs.selected = selected; + // Updating global & selected envs with new envs from processed-request output. + const { global, selected } = result.envs; + envs.global = global; + envs.selected = selected; - // Storing current request's report. - const requestReport = result.report; - requestsReport.push(requestReport); - } - - // Pushing remaining folders realted collection to stack. - for (const folder of collection.folders) { - collectionStack.push({ - path: `${path}/${folder.name}`, - collection: folder, - }); - } + // Storing current request's report. + const requestReport = result.report; + requestsReport.push(requestReport); } - return requestsReport; - }; + // Pushing remaining folders realted collection to stack. + for (const folder of collection.folders) { + collectionStack.push({ + path: `${path}/${folder.name}`, + collection: folder, + }); + } + } + + return requestsReport; +}; /** * Transforms collections to generate collection-stack which describes each collection's @@ -100,9 +100,7 @@ export const collectionsRunner = * @param collections Hopp-collection objects to be mapped to collection-stack type. * @returns Mapped collections to collection-stack. */ -const getCollectionStack = ( - collections: HoppCollection[] -): CollectionStack[] => +const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] => pipe( collections, A.map( diff --git a/packages/hoppscotch-cli/src/utils/mutators.ts b/packages/hoppscotch-cli/src/utils/mutators.ts index 1015ed2de..79053ed12 100644 --- a/packages/hoppscotch-cli/src/utils/mutators.ts +++ b/packages/hoppscotch-cli/src/utils/mutators.ts @@ -2,7 +2,7 @@ import fs from "fs/promises"; import { FormDataEntry } from "../types/request"; import { error } from "../types/errors"; import { isRESTCollection, isHoppErrnoException } from "./checks"; -import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; +import { HoppCollection } from "@hoppscotch/data"; /** * Parses array of FormDataEntry to FormData. @@ -35,20 +35,20 @@ export const parseErrorMessage = (e: unknown) => { }; export async function readJsonFile(path: string): Promise { - if(!path.endsWith('.json')) { - throw error({ code: "INVALID_FILE_TYPE", data: path }) + if (!path.endsWith(".json")) { + throw error({ code: "INVALID_FILE_TYPE", data: path }); } try { - await fs.access(path) + await fs.access(path); } catch (e) { - throw error({ code: "FILE_NOT_FOUND", path: path }) + throw error({ code: "FILE_NOT_FOUND", path: path }); } try { - return JSON.parse((await fs.readFile(path)).toString()) - } catch(e) { - throw error({ code: "UNKNOWN_ERROR", data: e }) + return JSON.parse((await fs.readFile(path)).toString()); + } catch (e) { + throw error({ code: "UNKNOWN_ERROR", data: e }); } } @@ -56,22 +56,24 @@ export async function readJsonFile(path: string): Promise { * Parses collection json file for given path:context.path, and validates * the parsed collectiona array. * @param path Collection json file path. - * @returns For successful parsing we get array of HoppCollection, + * @returns For successful parsing we get array of HoppCollection, */ export async function parseCollectionData( path: string -): Promise[]> { - let contents = await readJsonFile(path) +): Promise { + let contents = await readJsonFile(path); - const maybeArrayOfCollections: unknown[] = Array.isArray(contents) ? contents : [contents] + const maybeArrayOfCollections: unknown[] = Array.isArray(contents) + ? contents + : [contents]; - if(maybeArrayOfCollections.some((x) => !isRESTCollection(x))) { + if (maybeArrayOfCollections.some((x) => !isRESTCollection(x))) { throw error({ code: "MALFORMED_COLLECTION", path, data: "Please check the collection data.", - }) + }); } - return maybeArrayOfCollections as HoppCollection[] -}; + return maybeArrayOfCollections as HoppCollection[]; +} diff --git a/packages/hoppscotch-common/assets/themes/base-themes.scss b/packages/hoppscotch-common/assets/themes/base-themes.scss index d8258a347..73a228cb7 100644 --- a/packages/hoppscotch-common/assets/themes/base-themes.scss +++ b/packages/hoppscotch-common/assets/themes/base-themes.scss @@ -17,6 +17,7 @@ --lower-tertiary-sticky-fold: 7.125rem; --lower-fourth-sticky-fold: 9.188rem; --sidebar-primary-sticky-fold: 2rem; + --properties-primary-sticky-fold: 2.05rem; } @mixin light-theme { diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index f1764c887..1d70b503b 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -33,6 +33,7 @@ "open_workspace": "Open workspace", "paste": "Paste", "prettify": "Prettify", + "properties":"Properties", "remove": "Remove", "rename": "Rename", "restore": "Restore", @@ -172,6 +173,8 @@ "name_length_insufficient": "Collection name should be at least 3 characters long", "new": "New Collection", "order_changed": "Collection Order Updated", + "properties":"Colection Properties", + "properties_updated": "Collection Properties Updated", "renamed": "Collection renamed", "request_in_use": "Request in use", "save_as": "Save as", @@ -354,6 +357,8 @@ "offline_short": "You're using Hoppscotch offline.", "post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.", "pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.", + "collection_properties_authorization": " This authorization will be set for every request in this collection.", + "collection_properties_header": "This header will be set for every request in this collection.", "script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.", "test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again", "tests": "Write a test script to automate debugging." diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index 38218176a..6e714187b 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -56,6 +56,7 @@ declare module 'vue' { CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default'] CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default'] CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default'] + CollectionsProperties: typeof import('./components/collections/Properties.vue')['default'] CollectionsRequest: typeof import('./components/collections/Request.vue')['default'] CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default'] CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default'] diff --git a/packages/hoppscotch-common/src/components/app/spotlight/entry/GQLRequest.vue b/packages/hoppscotch-common/src/components/app/spotlight/entry/GQLRequest.vue index b91ef903c..bc27b93c8 100644 --- a/packages/hoppscotch-common/src/components/app/spotlight/entry/GQLRequest.vue +++ b/packages/hoppscotch-common/src/components/app/spotlight/entry/GQLRequest.vue @@ -13,7 +13,7 @@ diff --git a/packages/hoppscotch-common/src/components/collections/SaveRequest.vue b/packages/hoppscotch-common/src/components/collections/SaveRequest.vue index 381addd9c..0f440da48 100644 --- a/packages/hoppscotch-common/src/components/collections/SaveRequest.vue +++ b/packages/hoppscotch-common/src/components/collections/SaveRequest.vue @@ -74,6 +74,7 @@ import { Picked } from "~/helpers/types/HoppPicked" import { useI18n } from "@composables/i18n" import { useToast } from "@composables/toast" import { + cascadeParentCollectionForHeaderAuth, editGraphqlRequest, editRESTRequest, saveGraphqlRequestAs, @@ -239,6 +240,16 @@ const saveRequestAs = async () => { }, } + const { auth, headers } = cascadeParentCollectionForHeaderAuth( + `${picked.value.collectionIndex}`, + "rest" + ) + + RESTTabs.currentActiveTab.value.document.inheritedProperties = { + auth, + headers, + } + platform.analytics?.logEvent({ type: "HOPP_SAVE_REQUEST", createdNow: true, @@ -266,6 +277,16 @@ const saveRequestAs = async () => { }, } + const { auth, headers } = cascadeParentCollectionForHeaderAuth( + picked.value.folderPath, + "rest" + ) + + RESTTabs.currentActiveTab.value.document.inheritedProperties = { + auth, + headers, + } + platform.analytics?.logEvent({ type: "HOPP_SAVE_REQUEST", createdNow: true, @@ -294,6 +315,16 @@ const saveRequestAs = async () => { }, } + const { auth, headers } = cascadeParentCollectionForHeaderAuth( + picked.value.folderPath, + "rest" + ) + + RESTTabs.currentActiveTab.value.document.inheritedProperties = { + auth, + headers, + } + platform.analytics?.logEvent({ type: "HOPP_SAVE_REQUEST", createdNow: false, @@ -378,6 +409,16 @@ const saveRequestAs = async () => { workspaceType: "team", }) + const { auth, headers } = cascadeParentCollectionForHeaderAuth( + picked.value.folderPath, + "graphql" + ) + + GQLTabs.currentActiveTab.value.document.inheritedProperties = { + auth, + headers, + } + requestSaved() } else if (picked.value.pickedType === "gql-my-folder") { // TODO: Check for GQL request ? @@ -393,6 +434,16 @@ const saveRequestAs = async () => { workspaceType: "team", }) + const { auth, headers } = cascadeParentCollectionForHeaderAuth( + picked.value.folderPath, + "graphql" + ) + + GQLTabs.currentActiveTab.value.document.inheritedProperties = { + auth, + headers, + } + requestSaved() } else if (picked.value.pickedType === "gql-my-collection") { // TODO: Check for GQL request ? @@ -408,6 +459,16 @@ const saveRequestAs = async () => { workspaceType: "team", }) + const { auth, headers } = cascadeParentCollectionForHeaderAuth( + `${picked.value.collectionIndex}`, + "graphql" + ) + + GQLTabs.currentActiveTab.value.document.inheritedProperties = { + auth, + headers, + } + requestSaved() } } diff --git a/packages/hoppscotch-common/src/components/collections/TeamCollections.vue b/packages/hoppscotch-common/src/components/collections/TeamCollections.vue index 4df036a79..c85568a45 100644 --- a/packages/hoppscotch-common/src/components/collections/TeamCollections.vue +++ b/packages/hoppscotch-common/src/components/collections/TeamCollections.vue @@ -88,6 +88,13 @@ collection: node.data.data.data, }) " + @edit-properties=" + node.data.type === 'collections' && + emit('edit-properties', { + collectionIndex: node.id, + collection: node.data.data.data, + }) + " @export-data=" node.data.type === 'collections' && emit('export-data', node.data.data.data) @@ -159,6 +166,13 @@ folder: node.data.data.data, }) " + @edit-properties=" + node.data.type === 'folders' && + emit('edit-properties', { + collectionIndex: node.id, + collection: node.data.data.data, + }) + " @export-data=" node.data.type === 'folders' && emit('export-data', node.data.data.data) @@ -238,6 +252,7 @@ selectRequest({ request: node.data.data.data.request, requestIndex: node.data.data.data.id, + folderPath: getPath(node.id), }) " @share-request=" @@ -452,6 +467,13 @@ const emit = defineEmits<{ folder: TeamCollection } ): void + ( + event: "edit-properties", + payload: { + collectionIndex: string + collection: TeamCollection + } + ): void ( event: "edit-request", payload: { @@ -482,7 +504,7 @@ const emit = defineEmits<{ request: HoppRESTRequest requestIndex: string isActive: boolean - folderPath?: string | undefined + folderPath: string } ): void ( @@ -530,6 +552,12 @@ const emit = defineEmits<{ (event: "display-modal-import-export"): void }>() +const getPath = (path: string) => { + const pathArray = path.split("/") + pathArray.pop() + return pathArray.join("/") +} + const teamCollectionsList = toRef(props, "teamCollectionList") const hasNoTeamAccess = computed( @@ -586,6 +614,7 @@ const isActiveRequest = (requestID: string) => { const selectRequest = (data: { request: HoppRESTRequest requestIndex: string + folderPath: string | null }) => { const { request, requestIndex } = data if (props.saveRequest) { @@ -598,6 +627,7 @@ const selectRequest = (data: { request: request, requestIndex: requestIndex, isActive: isActiveRequest(requestIndex), + folderPath: data.folderPath, }) } } diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Add.vue b/packages/hoppscotch-common/src/components/collections/graphql/Add.vue index b5d287e22..2d6e8baae 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Add.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Add.vue @@ -32,58 +32,58 @@ - diff --git a/packages/hoppscotch-common/src/components/collections/graphql/AddFolder.vue b/packages/hoppscotch-common/src/components/collections/graphql/AddFolder.vue index 968ea8d30..71dd41a9c 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/AddFolder.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/AddFolder.vue @@ -3,7 +3,7 @@ v-if="show" dialog :title="t('folder.new')" - @close="$emit('hide-modal')" + @close="hideModal" > - diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue b/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue index ba3ff1953..02f62c507 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue @@ -128,6 +128,21 @@ } " /> + @@ -155,7 +170,15 @@ @edit-folder="$emit('edit-folder', $event)" @edit-request="$emit('edit-request', $event)" @duplicate-request="$emit('duplicate-request', $event)" + @edit-properties=" + $emit('edit-properties', { + collectionIndex: `${collectionIndex}/${String(index)}`, + collection: folder, + }) + " @select="$emit('select', $event)" + @select-request="$emit('select-request', $event)" + @drop-request="$emit('drop-request', $event)" /> ({}) }, - isFiltered: Boolean, -}) + saveRequest: boolean + collectionIndex: number | null + collection: HoppCollection + isFiltered: boolean +}>() const colorMode = useColorMode() const toast = useToast() @@ -248,7 +271,23 @@ const emit = defineEmits<{ (e: "add-request", i: any): void (e: "add-folder", i: any): void (e: "edit-folder", i: any): void + ( + e: "edit-properties", + payload: { + collectionIndex: string | null + collection: HoppCollection + } + ): void (e: "edit-collection"): void + (e: "select-request", i: any): void + ( + e: "drop-request", + payload: { + folderPath: string + requestIndex: string + collectionIndex: number | null + } + ): void }>() // Template refs @@ -324,6 +363,10 @@ const dropEvent = ({ dataTransfer }: any) => { dragging.value = !dragging.value const folderPath = dataTransfer.getData("folderPath") const requestIndex = dataTransfer.getData("requestIndex") - moveGraphqlRequest(folderPath, requestIndex, `${props.collectionIndex}`) + emit("drop-request", { + folderPath, + requestIndex, + collectionIndex: props.collectionIndex, + }) } diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Edit.vue b/packages/hoppscotch-common/src/components/collections/graphql/Edit.vue index c6bc7c5a4..0c9c9fdd9 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Edit.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Edit.vue @@ -37,13 +37,14 @@ import { ref, watch } from "vue" import { editGraphqlCollection } from "~/newstore/collections" import { useToast } from "@composables/toast" import { useI18n } from "@composables/i18n" +import { HoppCollection } from "@hoppscotch/data" -const props = defineProps({ - show: Boolean, - editingCollection: { type: Object, default: () => ({}) }, - editingCollectionIndex: { type: Number, default: null }, - editingCollectionName: { type: String, default: null }, -}) +const props = defineProps<{ + show: boolean + editingCollectionIndex: number | null + editingCollection: HoppCollection | null + editingCollectionName: string +}>() const emit = defineEmits<{ (e: "hide-modal"): void diff --git a/packages/hoppscotch-common/src/components/collections/graphql/EditFolder.vue b/packages/hoppscotch-common/src/components/collections/graphql/EditFolder.vue index c6810a7a3..f9aef64aa 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/EditFolder.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/EditFolder.vue @@ -32,52 +32,47 @@ - diff --git a/packages/hoppscotch-common/src/components/collections/graphql/EditRequest.vue b/packages/hoppscotch-common/src/components/collections/graphql/EditRequest.vue index 76c5295d2..38c087df9 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/EditRequest.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/EditRequest.vue @@ -32,61 +32,55 @@ - diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue b/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue index 4735bde16..5852daddc 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue @@ -10,24 +10,25 @@ @dragend="dragging = false" @contextmenu.prevent="options.tippy.show()" > - - - - - - {{ folder.name ? folder.name : folder.title }} + + - + + + {{ folder.name ? folder.name : folder.title }} + + +

+
@@ -148,7 +164,14 @@ @edit-folder="emit('edit-folder', $event)" @edit-request="emit('edit-request', $event)" @duplicate-request="emit('duplicate-request', $event)" + @edit-properties=" + emit('edit-properties', { + collectionIndex: `${folderPath}/${String(subFolderIndex)}`, + collection: subFolder, + }) + " @select="emit('select', $event)" + @select-request="$emit('select-request', $event)" /> ({}) }, - folderIndex: { type: Number, default: null }, - collectionIndex: { type: Number, default: null }, - folderPath: { type: String, default: null }, - isFiltered: Boolean, -}) + saveRequest: boolean + folder: HoppCollection + folderIndex: number + collectionIndex: number + folderPath: string + isFiltered: boolean +}>() const emit = defineEmits([ "select", @@ -229,6 +256,9 @@ const emit = defineEmits([ "add-folder", "edit-folder", "duplicate-request", + "edit-properties", + "select-request", + "drop-request", ]) // Template refs @@ -303,6 +333,11 @@ const dropEvent = ({ dataTransfer }: any) => { dragging.value = !dragging.value const folderPath = dataTransfer.getData("folderPath") const requestIndex = dataTransfer.getData("requestIndex") - moveGraphqlRequest(folderPath, requestIndex, props.folderPath) + + emit("drop-request", { + folderPath, + requestIndex, + collectionIndex: props.folderPath, + }) } diff --git a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue index f01036cec..db8123d94 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue @@ -11,7 +11,7 @@ diff --git a/packages/hoppscotch-common/src/components/collections/index.vue b/packages/hoppscotch-common/src/components/collections/index.vue index 7f96c55d0..235864360 100644 --- a/packages/hoppscotch-common/src/components/collections/index.vue +++ b/packages/hoppscotch-common/src/components/collections/index.vue @@ -38,6 +38,7 @@ @add-request="addRequest" @edit-collection="editCollection" @edit-folder="editFolder" + @edit-properties="editProperties" @export-data="exportData" @remove-collection="removeCollection" @remove-folder="removeFolder" @@ -69,6 +70,7 @@ @add-folder="addFolder" @edit-collection="editCollection" @edit-folder="editFolder" + @edit-properties="editProperties" @export-data="exportData" @remove-collection="removeCollection" @remove-folder="removeFolder" @@ -151,6 +153,12 @@ :show="showTeamModalAdd" @hide-modal="displayTeamModalAdd(false)" /> + @@ -181,10 +189,13 @@ import { moveRESTFolder, navigateToFolderWithIndexPath, restCollectionStore, + cascadeParentCollectionForHeaderAuth, } from "~/newstore/collections" import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter" import { HoppCollection, + HoppRESTAuth, + HoppRESTHeaders, HoppRESTRequest, makeCollection, } from "@hoppscotch/data" @@ -193,10 +204,10 @@ import { GQLError } from "~/helpers/backend/GQLClient" import { createNewRootCollection, createChildCollection, - renameCollection, deleteCollection, moveRESTTeamCollection, updateOrderRESTTeamCollection, + updateTeamCollection, } from "~/helpers/backend/mutations/TeamCollection" import { updateTeamRequest, @@ -220,6 +231,7 @@ import { getFoldersByPath, resolveSaveContextOnCollectionReorder, updateSaveContextForAffectedRequests, + updateInheritedPropertiesForAffectedRequests, resetTeamRequestsContext, } from "~/helpers/collection/collection" import { currentReorderingStatus$ } from "~/newstore/reordering" @@ -227,6 +239,7 @@ import { defineActionHandler, invokeAction } from "~/helpers/actions" import { WorkspaceService } from "~/services/workspace.service" import { useService } from "dioc/vue" import { RESTTabService } from "~/services/tab/rest" +import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" const t = useI18n() const toast = useToast() @@ -266,15 +279,11 @@ const collectionsType = ref({ }) // Collection Data -const editingCollection = ref< - HoppCollection | TeamCollection | null ->(null) +const editingCollection = ref(null) const editingCollectionName = ref(null) const editingCollectionIndex = ref(null) const editingCollectionID = ref(null) -const editingFolder = ref< - HoppCollection | TeamCollection | null ->(null) +const editingFolder = ref(null) const editingFolderName = ref(null) const editingFolderPath = ref(null) const editingRequest = ref(null) @@ -282,6 +291,18 @@ const editingRequestName = ref("") const editingRequestIndex = ref(null) const editingRequestID = ref(null) +const editingProperties = ref<{ + collection: Omit | TeamCollection | null + isRootCollection: boolean + path: string + inheritedProperties?: HoppInheritedProperty +}>({ + collection: null, + isRootCollection: false, + path: "", + inheritedProperties: undefined, +}) + const confirmModalTitle = ref(null) const filterTexts = ref("") @@ -520,6 +541,7 @@ const showModalEditCollection = ref(false) const showModalEditFolder = ref(false) const showModalEditRequest = ref(false) const showModalImportExport = ref(false) +const showModalEditProperties = ref(false) const showConfirmModal = ref(false) const showTeamModalAdd = ref(false) @@ -565,6 +587,12 @@ const displayModalImportExport = (show: boolean) => { if (!show) resetSelectedData() } +const displayModalEditProperties = (show: boolean) => { + showModalEditProperties.value = show + + if (!show) resetSelectedData() +} + const displayConfirmModal = (show: boolean) => { showConfirmModal.value = show @@ -584,6 +612,11 @@ const addNewRootCollection = (name: string) => { name, folders: [], requests: [], + headers: [], + auth: { + authType: "inherit", + authActive: false, + }, }) ) @@ -625,7 +658,7 @@ const addNewRootCollection = (name: string) => { const addRequest = (payload: { path: string - folder: HoppCollection | TeamCollection + folder: HoppCollection | TeamCollection }) => { const { path, folder } = payload editingFolder.value = folder @@ -639,11 +672,13 @@ const onAddRequest = (requestName: string) => { name: requestName, } + const path = editingFolderPath.value + if (!path) return if (collectionsType.value.type === "my-collections") { - const path = editingFolderPath.value - if (!path) return const insertionIndex = saveRESTRequestAs(path, newRequest) + const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest") + tabs.createNewTab({ request: newRequest, isDirty: false, @@ -652,6 +687,10 @@ const onAddRequest = (requestName: string) => { folderPath: path, requestIndex: insertionIndex, }, + inheritedProperties: { + auth, + headers, + }, }) platform.analytics?.logEvent({ @@ -692,7 +731,8 @@ const onAddRequest = (requestName: string) => { }, (result) => { const { createRequestInCollection } = result - + const { auth, headers } = + teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path) tabs.createNewTab({ request: newRequest, isDirty: false, @@ -702,6 +742,10 @@ const onAddRequest = (requestName: string) => { collectionID: createRequestInCollection.collection.id, teamID: createRequestInCollection.collection.team.id, }, + inheritedProperties: { + auth, + headers, + }, }) modalLoadingState.value = false @@ -714,7 +758,7 @@ const onAddRequest = (requestName: string) => { const addFolder = (payload: { path: string - folder: HoppCollection | TeamCollection + folder: HoppCollection | TeamCollection }) => { const { path, folder } = payload editingFolder.value = folder @@ -773,15 +817,13 @@ const onAddFolder = (folderName: string) => { const editCollection = (payload: { collectionIndex: string - collection: HoppCollection | TeamCollection + collection: HoppCollection | TeamCollection }) => { const { collectionIndex, collection } = payload editingCollection.value = collection if (collectionsType.value.type === "my-collections") { editingCollectionIndex.value = parseInt(collectionIndex) - editingCollectionName.value = ( - collection as HoppCollection - ).name + editingCollectionName.value = (collection as HoppCollection).name } else { editingCollectionName.value = (collection as TeamCollection).title } @@ -816,7 +858,7 @@ const updateEditingCollection = (newName: string) => { modalLoadingState.value = true pipe( - renameCollection(editingCollection.value.id, newName), + updateTeamCollection(editingCollection.value.id, undefined, newName), TE.match( (err: GQLError) => { toast.error(`${getErrorMessage(err)}`) @@ -834,13 +876,13 @@ const updateEditingCollection = (newName: string) => { const editFolder = (payload: { folderPath: string | undefined - folder: HoppCollection | TeamCollection + folder: HoppCollection | TeamCollection }) => { const { folderPath, folder } = payload editingFolder.value = folder if (collectionsType.value.type === "my-collections" && folderPath) { editingFolderPath.value = folderPath - editingFolderName.value = (folder as HoppCollection).name + editingFolderName.value = (folder as HoppCollection).name } else { editingFolderName.value = (folder as TeamCollection).title } @@ -854,7 +896,7 @@ const updateEditingFolder = (newName: string) => { if (!editingFolderPath.value) return editRESTFolder(editingFolderPath.value, { - ...(editingFolder.value as HoppCollection), + ...(editingFolder.value as HoppCollection), name: newName, }) displayModalEditFolder(false) @@ -865,7 +907,7 @@ const updateEditingFolder = (newName: string) => { /* renameCollection can be used to rename both collections and folders since folder is treated as collection in the BE. */ pipe( - renameCollection(editingFolder.value.id, newName), + updateTeamCollection(editingFolder.value.id, undefined, newName), TE.match( (err: GQLError) => { if (err.error === "team_coll/short_title") { @@ -1279,16 +1321,18 @@ const selectPicked = (payload: Picked | null) => { */ const selectRequest = (selectedRequest: { request: HoppRESTRequest - folderPath: string | undefined + folderPath: string requestIndex: string isActive: boolean }) => { const { request, folderPath, requestIndex } = selectedRequest - // If there is a request with this save context, switch into it let possibleTab = null if (collectionsType.value.type === "team-collections") { + const { auth, headers } = + teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath) + possibleTab = tabs.getTabRefWithSaveContext({ originLocation: "team-collection", requestID: requestIndex, @@ -1302,10 +1346,19 @@ const selectRequest = (selectedRequest: { saveContext: { originLocation: "team-collection", requestID: requestIndex, + collectionID: folderPath, + }, + inheritedProperties: { + auth, + headers, }, }) } } else { + const { auth, headers } = cascadeParentCollectionForHeaderAuth( + folderPath, + "rest" + ) possibleTab = tabs.getTabRefWithSaveContext({ originLocation: "user-collection", requestIndex: parseInt(requestIndex), @@ -1323,6 +1376,10 @@ const selectRequest = (selectedRequest: { folderPath: folderPath!, requestIndex: parseInt(requestIndex), }, + inheritedProperties: { + auth, + headers, + }, }) } } @@ -1349,16 +1406,17 @@ const dropRequest = (payload: { }) => { const { folderPath, requestIndex, destinationCollectionIndex } = payload - if (!requestIndex || !destinationCollectionIndex) return + if (!requestIndex || !destinationCollectionIndex || !folderPath) return - if (collectionsType.value.type === "my-collections" && folderPath) { - moveRESTRequest( - folderPath, - pathToLastIndex(requestIndex), - destinationCollectionIndex + let possibleTab = null + + if (collectionsType.value.type === "my-collections") { + const { auth, headers } = cascadeParentCollectionForHeaderAuth( + destinationCollectionIndex, + "rest" ) - const possibleTab = tabs.getTabRefWithSaveContext({ + possibleTab = tabs.getTabRefWithSaveContext({ originLocation: "user-collection", folderPath, requestIndex: pathToLastIndex(requestIndex), @@ -1374,6 +1432,11 @@ const dropRequest = (payload: { destinationCollectionIndex ).length, } + + possibleTab.value.document.inheritedProperties = { + auth, + headers, + } } // When it's drop it's basically getting deleted from last folder. reordering last folder accordingly @@ -1383,6 +1446,11 @@ const dropRequest = (payload: { folderPath, length: getRequestsByPath(myCollections.value, folderPath).length, }) + moveRESTRequest( + folderPath, + pathToLastIndex(requestIndex), + destinationCollectionIndex + ) toast.success(`${t("request.moved")}`) draggingToRoot.value = false @@ -1406,8 +1474,12 @@ const dropRequest = (payload: { requestMoveLoading.value.indexOf(requestIndex), 1 ) + const { auth, headers } = + teamCollectionAdapter.cascadeParentCollectionForHeaderAuth( + destinationCollectionIndex + ) - const possibleTab = tabs.getTabRefWithSaveContext({ + possibleTab = tabs.getTabRefWithSaveContext({ originLocation: "team-collection", requestID: requestIndex, }) @@ -1417,6 +1489,10 @@ const dropRequest = (payload: { originLocation: "team-collection", requestID: requestIndex, } + possibleTab.value.document.inheritedProperties = { + auth, + headers, + } } toast.success(`${t("request.moved")}`) } @@ -1537,6 +1613,22 @@ const dropCollection = (payload: { `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}` ) + const { auth, headers } = cascadeParentCollectionForHeaderAuth( + `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`, + "rest" + ) + + const inheritedProperty = { + auth, + headers, + } + + updateInheritedPropertiesForAffectedRequests( + `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`, + inheritedProperty, + "rest" + ) + draggingToRoot.value = false toast.success(`${t("collection.moved")}`) } else if (hasTeamWriteAccess.value) { @@ -1562,6 +1654,22 @@ const dropCollection = (payload: { collectionMoveLoading.value.indexOf(collectionIndexDragged), 1 ) + + const { auth, headers } = + teamCollectionAdapter.cascadeParentCollectionForHeaderAuth( + destinationCollectionIndex + ) + + const inheritedProperty = { + auth, + headers, + } + + updateInheritedPropertiesForAffectedRequests( + `${destinationCollectionIndex}`, + inheritedProperty, + "rest" + ) } ) )() @@ -1846,13 +1954,11 @@ const initializeDownloadCollection = async ( * Triggered by the export button in the tippy menu * @param collection - Collection or folder to be exported */ -const exportData = async ( - collection: HoppCollection | TeamCollection -) => { +const exportData = async (collection: HoppCollection | TeamCollection) => { if (collectionsType.value.type === "my-collections") { const collectionJSON = JSON.stringify(collection) - const name = (collection as HoppCollection).name + const name = (collection as HoppCollection).name initializeDownloadCollection(collectionJSON, name) } else { @@ -1893,6 +1999,164 @@ const shareRequest = ({ request }: { request: HoppRESTRequest }) => { } } +const editProperties = (payload: { + collectionIndex: string + collection: HoppCollection | TeamCollection +}) => { + const { collection, collectionIndex } = payload + + if (collectionsType.value.type === "my-collections") { + const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder + + let inheritedProperties = { + auth: { + parentID: "", + parentName: "", + inheritedAuth: { + authType: "inherit", + authActive: true, + }, + }, + headers: [ + { + parentID: "", + parentName: "", + inheritedHeaders: [], + }, + ], + } as HoppInheritedProperty + + if (parentIndex) { + const { auth, headers } = cascadeParentCollectionForHeaderAuth( + parentIndex, + "rest" + ) + + inheritedProperties = { + auth, + headers, + } + } + + editingProperties.value = { + collection, + isRootCollection: isAlreadyInRoot(collectionIndex), + path: collectionIndex, + inheritedProperties, + } + } else if (hasTeamWriteAccess.value) { + const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder + + const data = (collection as TeamCollection).data + ? JSON.parse((collection as TeamCollection).data ?? "") + : null + + let inheritedProperties = undefined + let coll = { + id: collection.id, + name: (collection as TeamCollection).title, + auth: { + authType: "inherit", + authActive: true, + } as HoppRESTAuth, + headers: [] as HoppRESTHeaders, + folders: null, + requests: null, + } + + if (parentIndex) { + const { auth, headers } = + teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(parentIndex) + + inheritedProperties = { + auth, + headers, + } + } + + if (data) { + coll = { + ...coll, + auth: data.auth, + headers: data.headers as HoppRESTHeaders, + } + } + + editingProperties.value = { + collection: coll, + isRootCollection: isAlreadyInRoot(collectionIndex), + path: collectionIndex, + inheritedProperties, + } + } + + displayModalEditProperties(true) +} + +const setCollectionProperties = (newCollection: { + collection: HoppCollection + path: string + isRootCollection: boolean +}) => { + const { collection, path, isRootCollection } = newCollection + + if (collectionsType.value.type === "my-collections") { + if (isRootCollection) { + editRESTCollection(parseInt(path), collection) + } else { + editRESTFolder(path, collection) + } + + const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest") + + nextTick(() => { + updateInheritedPropertiesForAffectedRequests( + path, + { + auth, + headers, + }, + "rest" + ) + }) + toast.success(t("collection.properties_updated")) + } else if (hasTeamWriteAccess.value && collection.id) { + const data = { + auth: collection.auth, + headers: collection.headers, + } + pipe( + updateTeamCollection(collection.id, JSON.stringify(data), undefined), + TE.match( + (err: GQLError) => { + toast.error(`${getErrorMessage(err)}`) + }, + () => { + toast.success(t("collection.properties_updated")) + } + ) + )() + + //This is a hack to update the inherited properties of the requests if there an tab opened + // since it takes a little bit of time to update the collection tree + setTimeout(() => { + const { auth, headers } = + teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path) + updateInheritedPropertiesForAffectedRequests( + path, + { + auth, + headers, + }, + "rest", + "team" + ) + }, 200) + } + + displayModalEditProperties(false) +} + const resolveConfirmModal = (title: string | null) => { if (title === `${t("confirm.remove_collection")}`) onRemoveCollection() else if (title === `${t("confirm.remove_request")}`) onRemoveRequest() diff --git a/packages/hoppscotch-common/src/components/graphql/Authorization.vue b/packages/hoppscotch-common/src/components/graphql/Authorization.vue index 49a857cbb..9e718ced4 100644 --- a/packages/hoppscotch-common/src/components/graphql/Authorization.vue +++ b/packages/hoppscotch-common/src/components/graphql/Authorization.vue @@ -37,6 +37,18 @@ } " /> + +
+ + Inherited + {{ getAuthName(inheritedProperties.auth.inheritedAuth.authType) }} + from Parent Collection {{ inheritedProperties?.auth.parentName }} + + + Please save this request in any collection to inherit the + authorization + +
() const emit = defineEmits<{ (e: "update:modelValue", value: HoppGQLAuth): void }>() +onMounted(() => { + if (props.isRootCollection && auth.value.authType === "inherit") { + auth.value = { + authType: "none", + authActive: true, + } + } +}) + const auth = useVModel(props, "modelValue", emit) const AUTH_KEY_NAME = { @@ -224,12 +261,20 @@ const AUTH_KEY_NAME = { "oauth-2": "OAuth 2.0", "api-key": "API key", none: "None", + inherit: "Inherit", } as const const authType = pluckRef(auth, "authType") + const authName = computed(() => AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None" ) + +const getAuthName = (type: HoppGQLAuth["authType"] | undefined) => { + if (!type) return "None" + return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None" +} + const authActive = pluckRef(auth, "authActive") const clearContent = () => { diff --git a/packages/hoppscotch-common/src/components/graphql/Headers.vue b/packages/hoppscotch-common/src/components/graphql/Headers.vue index b808a086f..81da644a4 100644 --- a/packages/hoppscotch-common/src/components/graphql/Headers.vue +++ b/packages/hoppscotch-common/src/components/graphql/Headers.vue @@ -77,22 +77,11 @@ tabindex="-1" /> - - + + + + + + + + + () +const props = defineProps<{ + modelValue: HoppGQLRequest + isCollectionProperty?: boolean + inheritedProperties?: HoppInheritedProperty +}>() const emit = defineEmits<{ (e: "update:modelValue", value: HoppGQLRequest): void @@ -413,7 +524,11 @@ const deleteHeader = (index: number) => { }) } - workingHeaders.value.splice(index, 1) + workingHeaders.value = pipe( + workingHeaders.value, + A.deleteAt(index), + O.getOrElseW(() => throwError("Working Headers Deletion Out of Bounds")) + ) } const clearContent = () => { @@ -429,4 +544,151 @@ const clearContent = () => { bulkHeaders.value = "" } + +const getComputedAuthHeaders = ( + req?: HoppGQLRequest, + auth?: HoppGQLRequest["auth"] +) => { + const request = auth ? { auth: auth ?? { authActive: false } } : req + // If Authorization header is also being user-defined, that takes priority + if (req && req.headers.find((h) => h.key.toLowerCase() === "authorization")) + return [] + + if (!request) return [] + + if (!request.auth || !request.auth.authActive) return [] + + const headers: HoppGQLHeader[] = [] + + // TODO: Support a better b64 implementation than btoa ? + if (request.auth.authType === "basic") { + const username = request.auth.username + const password = request.auth.password + + headers.push({ + active: true, + key: "Authorization", + value: `Basic ${btoa(`${username}:${password}`)}`, + }) + } else if ( + request.auth.authType === "bearer" || + request.auth.authType === "oauth-2" + ) { + headers.push({ + active: true, + key: "Authorization", + value: `Bearer ${request.auth.token}`, + }) + } else if (request.auth.authType === "api-key") { + const { key, addTo } = request.auth + + if (addTo === "Headers" && key) { + headers.push({ + active: true, + key, + value: request.auth.value ?? "", + }) + } + } + + return headers +} + +const getComputedHeaders = (req: HoppGQLRequest) => { + return [ + ...getComputedAuthHeaders(req).map((header) => ({ + source: "auth" as const, + header, + })), + ] +} + +const computedHeaders = computed(() => + getComputedHeaders(request.value).map((header, index) => ({ + id: `header-${index}`, + ...header, + })) +) + +const inheritedProperties = computed(() => { + if (!props.inheritedProperties?.auth || !props.inheritedProperties.headers) + return [] + + //filter out headers that are already in the request headers + + const inheritedHeaders = props.inheritedProperties.headers.filter( + (header) => + !request.value.headers.some( + (requestHeader) => requestHeader.key === header.inheritedHeader?.key + ) + ) + + const headers = inheritedHeaders + .filter( + (header) => + header.inheritedHeader !== null && + 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, + }, + })) + + let auth = [] as { + inheritedFrom: string + source: "auth" + id: string + header: { + key: string + value: string + active: boolean + } + }[] + + const computedAuthHeader = getComputedAuthHeaders( + request.value, + props.inheritedProperties.auth.inheritedAuth + )[0] + + if ( + computedAuthHeader && + request.value.auth.authType === "inherit" && + request.value.auth.authActive + ) { + auth = [ + { + inheritedFrom: props.inheritedProperties?.auth.parentName, + source: "auth", + id: `header-auth`, + header: computedAuthHeader, + }, + ] + } + + return [...headers, ...auth] +}) + +const masking = ref(true) + +const toggleMask = () => { + masking.value = !masking.value +} + +const mask = (header: any) => { + if (header.source === "auth" && masking.value) + return header.header.value.replace(/\S/gi, "*") + return header.header.value +} + +// const changeTab = (tab: ComputedHeader["source"]) => { +// if (tab === "auth") emit("change-tab", "authorization") +// else emit("change-tab", "bodyParams") +// } diff --git a/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue b/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue index 77c674a59..2d04fcc8c 100644 --- a/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue +++ b/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue @@ -34,10 +34,16 @@ :label="`${t('tab.headers')}`" :info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`" > - + - + (), { response: null, optionTab: "query", } ) -const emit = defineEmits(["update:modelValue", "update:response"]) +const emit = defineEmits<{ + (e: "update:modelValue", value: HoppGQLRequest): void + (e: "update:optionTab", value: GQLOptionTabs): void + (e: "update:response", value: GQLResponseEvent[]): void +}>() + const selectedOptionTab = useVModel(props, "optionTab", emit) -const request = ref(props.modelValue) - -watch( - () => request.value, - (newVal) => { - emit("update:modelValue", newVal) - }, - { deep: true } -) +const request = useVModel(props, "modelValue", emit) const url = computedWithControl( () => tabs.currentActiveTab.value, @@ -131,10 +136,30 @@ const runQuery = async ( startPageProgress() try { const runURL = clone(url.value) - const runHeaders = clone(request.value.headers) const runQuery = clone(request.value.query) const runVariables = clone(request.value.variables) - const runAuth = clone(request.value.auth) + const runAuth = + request.value.auth.authType === "inherit" && request.value.auth.authActive + ? clone(tabs.currentActiveTab.value.document.inheritedProperties?.auth) + : clone(request.value.auth) + + const inheritedHeaders = + tabs.currentActiveTab.value.document.inheritedProperties?.headers.map( + (header) => { + if (header.inheritedHeader) { + return header.inheritedHeader + } + return [] + } + ) + + let runHeaders: HoppGQLRequest["headers"] = [] + + if (inheritedHeaders) { + runHeaders = [...inheritedHeaders, ...clone(request.value.headers)] + } else { + runHeaders = clone(request.value.headers) + } await runGQLOperation({ name: request.value.name, @@ -142,7 +167,7 @@ const runQuery = async ( headers: runHeaders, query: runQuery, variables: runVariables, - auth: runAuth, + auth: runAuth ?? { authType: "none", authActive: false }, operationName: definition?.name?.value, operationType: definition?.operation ?? "query", }) diff --git a/packages/hoppscotch-common/src/components/graphql/RequestTab.vue b/packages/hoppscotch-common/src/components/graphql/RequestTab.vue index 32fe03460..7e3f26bf4 100644 --- a/packages/hoppscotch-common/src/components/graphql/RequestTab.vue +++ b/packages/hoppscotch-common/src/components/graphql/RequestTab.vue @@ -5,6 +5,7 @@ v-model="tab.document.request" v-model:response="tab.document.response" v-model:option-tab="tab.document.optionTabPreference" + v-model:inherited-properties="tab.document.inheritedProperties" :tab-id="tab.id" /> diff --git a/packages/hoppscotch-common/src/components/http/Authorization.vue b/packages/hoppscotch-common/src/components/http/Authorization.vue index 191d1b7e5..3cf9a2a66 100644 --- a/packages/hoppscotch-common/src/components/http/Authorization.vue +++ b/packages/hoppscotch-common/src/components/http/Authorization.vue @@ -1,7 +1,12 @@