Compare commits

...

47 Commits

Author SHA1 Message Date
nivedin
fa8c7a9f35 fix: ancestor header not updating 2023-12-13 22:35:44 +05:30
nivedin
56f427740d fix: use remaining gql request items when sync 2023-12-13 22:35:44 +05:30
nivedin
e4eaa2cec9 refactor: update rest-gql import-export sync 2023-12-13 22:35:44 +05:30
nivedin
8bb628354e chore: remove console log 2023-12-13 22:35:44 +05:30
nivedin
b3bef85b87 fix: inherit auth-headers closest to path bug 2023-12-13 22:35:44 +05:30
nivedin
80d54565eb chore: add toast for personal collection 2023-12-13 22:35:44 +05:30
Andrew Bastin
9e7f1b58b9 fix: type checking issues for recursive types 2023-12-13 22:35:44 +05:30
nivedin
b07d0c69cc chore: update opened team coll tab and toast added 2023-12-13 22:35:44 +05:30
nivedin
00ff318194 refactor: change the default req auth type to none 2023-12-13 22:35:44 +05:30
nivedin
b5c1fe4619 chore: update type 2023-12-13 22:35:44 +05:30
nivedin
8e72ab44a6 fix: update hopp-data coll header type 2023-12-13 22:35:44 +05:30
nivedin
987d1a74d1 fix: inherit auth from ancestor bug 2023-12-13 22:35:44 +05:30
nivedin
d5d4e51848 fix: header component bug when auth api key undefined 2023-12-13 22:35:44 +05:30
nivedin
509775878a refactor: import-export team coll in new schema 2023-12-13 22:35:44 +05:30
nivedin
95953557de refactor: update all hoppCollection type to remove generic pattern 2023-12-13 22:35:44 +05:30
nivedin
9d2b7cc03f refactor: update inherited property schema 2023-12-13 22:35:44 +05:30
nivedin
276d48e3f8 refactor: import validate using version 2 2023-12-13 22:35:44 +05:30
nivedin
6526247869 refactor: update localpersistance schema 2023-12-13 22:35:44 +05:30
nivedin
08b6e0b747 refactor: open request with header and auth in search 2023-12-13 22:35:44 +05:30
nivedin
79a8bc669e refactor: update collection type using zod 2023-12-13 22:35:44 +05:30
nivedin
b6f3b24b9e fix: add auth and headers for old collection 2023-12-13 22:35:44 +05:30
nivedin
16bbfec736 fix: minor change 2023-12-13 22:35:44 +05:30
nivedin
ebe680c596 chore: minor padding update 2023-12-13 22:35:44 +05:30
nivedin
f79ab298da chore: update properties banner colour 2023-12-13 22:35:44 +05:30
nivedin
83019733e3 refactor: update gql properties sync 2023-12-13 22:35:44 +05:30
nivedin
c18e801420 refactor: update coll properties when syncing 2023-12-13 22:35:44 +05:30
nivedin
bb8c77fa7a chore: remove console 2023-12-13 22:35:44 +05:30
nivedin
951cff9f30 feat: auth-headers in team collection 2023-12-13 22:35:44 +05:30
nivedin
574d800a12 chore: update properties info banner 2023-12-13 22:35:44 +05:30
nivedin
ed825cf648 chore: add inherited auth and header in req runner 2023-12-13 22:35:44 +05:30
nivedin
569c170fec refactor: update inheritence when moving request 2023-12-13 22:35:44 +05:30
nivedin
056a8a8719 chore: filter header for non active values 2023-12-13 22:35:44 +05:30
nivedin
9bfe195253 chore: use samrt-env for gql headers 2023-12-13 22:35:44 +05:30
nivedin
fce68de282 feat: added inherit auth-header for personal gql 2023-12-13 22:35:44 +05:30
nivedin
ad7b3f05b1 chore: update graphql with ts and setup 2023-12-13 22:35:44 +05:30
nivedin
ea8de655d7 chore: root collection auth issue and ui update 2023-12-13 22:35:44 +05:30
nivedin
369d01a399 chore: add computed header for graphql 2023-12-13 22:35:44 +05:30
nivedin
0a54455fe7 refactor: inherit header from multiple collecions 2023-12-13 22:35:44 +05:30
nivedin
b893607ad1 refactor: inherit properties when saving 2023-12-13 22:35:44 +05:30
nivedin
7c3a84246d refactor: add fallback for new request auth state 2023-12-13 22:35:44 +05:30
nivedin
2687592a56 refactor: add auth and header fields coll in sh 2023-12-13 22:35:44 +05:30
nivedin
a48d7e879f refactor: inherited auth and header for req runner 2023-12-13 22:35:44 +05:30
nivedin
d326063659 chore: update types 2023-12-13 22:35:44 +05:30
nivedin
59735c15e4 refactor: update components to encapsulate 2023-12-13 22:35:44 +05:30
nivedin
ae531f5882 chore: update properties UI flow 2023-12-13 22:35:44 +05:30
nivedin
f8aeb42da5 chore: improve UI flow and add i18n 2023-12-13 22:35:44 +05:30
nivedin
1896e5afe1 feat: added properties option for root collection 2023-12-13 22:35:44 +05:30
95 changed files with 3221 additions and 970 deletions

View File

@@ -1,8 +1,8 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; import { HoppCollection } from "@hoppscotch/data";
import { HoppEnvs } from "./request"; import { HoppEnvs } from "./request";
export type CollectionRunnerParam = { export type CollectionRunnerParam = {
collections: HoppCollection<HoppRESTRequest>[]; collections: HoppCollection[];
envs: HoppEnvs; envs: HoppEnvs;
delay?: number; delay?: number;
}; };

View File

@@ -33,7 +33,7 @@ export type HoppEnvs = {
export type CollectionStack = { export type CollectionStack = {
path: string; path: string;
collection: HoppCollection<HoppRESTRequest>; collection: HoppCollection;
}; };
export type RequestReport = { export type RequestReport = {

View File

@@ -1,8 +1,4 @@
import { import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data";
HoppCollection,
HoppRESTRequest,
isHoppRESTRequest,
} from "@hoppscotch/data";
import * as A from "fp-ts/Array"; import * as A from "fp-ts/Array";
import { CommanderError } from "commander"; import { CommanderError } from "commander";
import { HoppCLIError, HoppErrnoException } from "../types/errors"; import { HoppCLIError, HoppErrnoException } from "../types/errors";
@@ -24,9 +20,7 @@ export const hasProperty = <P extends PropertyKey>(
* @returns True, if unknown parameter is valid Hoppscotch REST Collection; * @returns True, if unknown parameter is valid Hoppscotch REST Collection;
* False, otherwise. * False, otherwise.
*/ */
export const isRESTCollection = ( export const isRESTCollection = (param: unknown): param is HoppCollection => {
param: unknown
): param is HoppCollection<HoppRESTRequest> => {
if (!!param && typeof param === "object") { if (!!param && typeof param === "object") {
if (!hasProperty(param, "v") || typeof param.v !== "number") { if (!hasProperty(param, "v") || typeof param.v !== "number") {
return false; return false;
@@ -62,7 +56,6 @@ export const isRESTCollection = (
return false; return false;
}; };
/** /**
* Checks if given error data is of type HoppCLIError, based on existence * Checks if given error data is of type HoppCLIError, based on existence
* of code property. * of code property.

View File

@@ -3,7 +3,7 @@ import { pipe } from "fp-ts/function";
import { bold } from "chalk"; import { bold } from "chalk";
import { log } from "console"; import { log } from "console";
import round from "lodash/round"; import round from "lodash/round";
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; import { HoppCollection } from "@hoppscotch/data";
import { import {
HoppEnvs, HoppEnvs,
CollectionStack, CollectionStack,
@@ -41,58 +41,58 @@ const { WARN, FAIL } = exceptionColors;
* @param param Data of hopp-collection with hopp-requests, envs to be processed. * @param param Data of hopp-collection with hopp-requests, envs to be processed.
* @returns List of report for each processed request. * @returns List of report for each processed request.
*/ */
export const collectionsRunner = export const collectionsRunner = async (
async (param: CollectionRunnerParam): Promise<RequestReport[]> => param: CollectionRunnerParam
{ ): Promise<RequestReport[]> => {
const envs: HoppEnvs = param.envs; const envs: HoppEnvs = param.envs;
const delay = param.delay ?? 0; const delay = param.delay ?? 0;
const requestsReport: RequestReport[] = []; const requestsReport: RequestReport[] = [];
const collectionStack: CollectionStack[] = getCollectionStack( const collectionStack: CollectionStack[] = getCollectionStack(
param.collections param.collections
); );
while (collectionStack.length) { while (collectionStack.length) {
// Pop out top-most collection from stack to be processed. // Pop out top-most collection from stack to be processed.
const { collection, path } = <CollectionStack>collectionStack.pop(); const { collection, path } = <CollectionStack>collectionStack.pop();
// Processing each request in collection // Processing each request in collection
for (const request of collection.requests) { for (const request of collection.requests) {
const _request = preProcessRequest(request); const _request = preProcessRequest(request);
const requestPath = `${path}/${_request.name}`; const requestPath = `${path}/${_request.name}`;
const processRequestParams: ProcessRequestParams = { const processRequestParams: ProcessRequestParams = {
path: requestPath, path: requestPath,
request: _request, request: _request,
envs, envs,
delay, delay,
}; };
// Request processing initiated message. // Request processing initiated message.
log(WARN(`\nRunning: ${bold(requestPath)}`)); log(WARN(`\nRunning: ${bold(requestPath)}`));
// Processing current request. // Processing current request.
const result = await processRequest(processRequestParams)(); const result = await processRequest(processRequestParams)();
// Updating global & selected envs with new envs from processed-request output. // Updating global & selected envs with new envs from processed-request output.
const { global, selected } = result.envs; const { global, selected } = result.envs;
envs.global = global; envs.global = global;
envs.selected = selected; envs.selected = selected;
// Storing current request's report. // Storing current request's report.
const requestReport = result.report; const requestReport = result.report;
requestsReport.push(requestReport); requestsReport.push(requestReport);
}
// Pushing remaining folders realted collection to stack.
for (const folder of collection.folders) {
collectionStack.push({
path: `${path}/${folder.name}`,
collection: folder,
});
}
} }
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 * 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. * @param collections Hopp-collection objects to be mapped to collection-stack type.
* @returns Mapped collections to collection-stack. * @returns Mapped collections to collection-stack.
*/ */
const getCollectionStack = ( const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] =>
collections: HoppCollection<HoppRESTRequest>[]
): CollectionStack[] =>
pipe( pipe(
collections, collections,
A.map( A.map(

View File

@@ -2,7 +2,7 @@ import fs from "fs/promises";
import { FormDataEntry } from "../types/request"; import { FormDataEntry } from "../types/request";
import { error } from "../types/errors"; import { error } from "../types/errors";
import { isRESTCollection, isHoppErrnoException } from "./checks"; import { isRESTCollection, isHoppErrnoException } from "./checks";
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; import { HoppCollection } from "@hoppscotch/data";
/** /**
* Parses array of FormDataEntry to FormData. * Parses array of FormDataEntry to FormData.
@@ -35,20 +35,20 @@ export const parseErrorMessage = (e: unknown) => {
}; };
export async function readJsonFile(path: string): Promise<unknown> { export async function readJsonFile(path: string): Promise<unknown> {
if(!path.endsWith('.json')) { if (!path.endsWith(".json")) {
throw error({ code: "INVALID_FILE_TYPE", data: path }) throw error({ code: "INVALID_FILE_TYPE", data: path });
} }
try { try {
await fs.access(path) await fs.access(path);
} catch (e) { } catch (e) {
throw error({ code: "FILE_NOT_FOUND", path: path }) throw error({ code: "FILE_NOT_FOUND", path: path });
} }
try { try {
return JSON.parse((await fs.readFile(path)).toString()) return JSON.parse((await fs.readFile(path)).toString());
} catch(e) { } catch (e) {
throw error({ code: "UNKNOWN_ERROR", data: e }) throw error({ code: "UNKNOWN_ERROR", data: e });
} }
} }
@@ -56,22 +56,24 @@ export async function readJsonFile(path: string): Promise<unknown> {
* Parses collection json file for given path:context.path, and validates * Parses collection json file for given path:context.path, and validates
* the parsed collectiona array. * the parsed collectiona array.
* @param path Collection json file path. * @param path Collection json file path.
* @returns For successful parsing we get array of HoppCollection<HoppRESTRequest>, * @returns For successful parsing we get array of HoppCollection,
*/ */
export async function parseCollectionData( export async function parseCollectionData(
path: string path: string
): Promise<HoppCollection<HoppRESTRequest>[]> { ): Promise<HoppCollection[]> {
let contents = await readJsonFile(path) 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({ throw error({
code: "MALFORMED_COLLECTION", code: "MALFORMED_COLLECTION",
path, path,
data: "Please check the collection data.", data: "Please check the collection data.",
}) });
} }
return maybeArrayOfCollections as HoppCollection<HoppRESTRequest>[] return maybeArrayOfCollections as HoppCollection[];
}; }

View File

@@ -17,6 +17,7 @@
--lower-tertiary-sticky-fold: 7.125rem; --lower-tertiary-sticky-fold: 7.125rem;
--lower-fourth-sticky-fold: 9.188rem; --lower-fourth-sticky-fold: 9.188rem;
--sidebar-primary-sticky-fold: 2rem; --sidebar-primary-sticky-fold: 2rem;
--properties-primary-sticky-fold: 2.05rem;
} }
@mixin light-theme { @mixin light-theme {

View File

@@ -33,6 +33,7 @@
"open_workspace": "Open workspace", "open_workspace": "Open workspace",
"paste": "Paste", "paste": "Paste",
"prettify": "Prettify", "prettify": "Prettify",
"properties":"Properties",
"remove": "Remove", "remove": "Remove",
"rename": "Rename", "rename": "Rename",
"restore": "Restore", "restore": "Restore",
@@ -172,6 +173,8 @@
"name_length_insufficient": "Collection name should be at least 3 characters long", "name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "New Collection", "new": "New Collection",
"order_changed": "Collection Order Updated", "order_changed": "Collection Order Updated",
"properties":"Colection Properties",
"properties_updated": "Collection Properties Updated",
"renamed": "Collection renamed", "renamed": "Collection renamed",
"request_in_use": "Request in use", "request_in_use": "Request in use",
"save_as": "Save as", "save_as": "Save as",
@@ -354,6 +357,8 @@
"offline_short": "You're using Hoppscotch offline.", "offline_short": "You're using Hoppscotch offline.",
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.", "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.", "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.", "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", "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." "tests": "Write a test script to automate debugging."

View File

@@ -56,6 +56,7 @@ declare module 'vue' {
CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default'] CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default'] CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
CollectionsMyCollections: typeof import('./components/collections/MyCollections.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'] CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default'] CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default'] CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']

View File

@@ -13,7 +13,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import { computed } from "vue" import { computed } from "vue"
import { graphqlCollectionStore } from "~/newstore/collections" import { graphqlCollectionStore } from "~/newstore/collections"
@@ -28,7 +28,7 @@ const pathFolders = computed(() => {
.slice(0, -1) .slice(0, -1)
.map((x) => parseInt(x)) .map((x) => parseInt(x))
const pathItems: HoppCollection<HoppGQLRequest>[] = [] const pathItems: HoppCollection[] = []
let currentFolder = let currentFolder =
graphqlCollectionStore.value.state[folderIndicies.shift()!] graphqlCollectionStore.value.state[folderIndicies.shift()!]

View File

@@ -20,7 +20,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import { computed } from "vue" import { computed } from "vue"
import { restCollectionStore } from "~/newstore/collections" import { restCollectionStore } from "~/newstore/collections"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring" import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
@@ -36,7 +36,7 @@ const pathFolders = computed(() => {
.slice(0, -1) .slice(0, -1)
.map((x) => parseInt(x)) .map((x) => parseInt(x))
const pathItems: HoppCollection<HoppRESTRequest>[] = [] const pathItems: HoppCollection[] = []
let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!] let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!]
pathItems.push(currentFolder) pathItems.push(currentFolder)

View File

@@ -96,6 +96,7 @@
@keyup.e="edit?.$el.click()" @keyup.e="edit?.$el.click()"
@keyup.delete="deleteAction?.$el.click()" @keyup.delete="deleteAction?.$el.click()"
@keyup.x="exportAction?.$el.click()" @keyup.x="exportAction?.$el.click()"
@keyup.p="propertiesAction?.$el.click()"
@keyup.escape="hide()" @keyup.escape="hide()"
> >
<HoppSmartItem <HoppSmartItem
@@ -159,6 +160,18 @@
} }
" "
/> />
<HoppSmartItem
ref="propertiesAction"
:icon="IconSettings2"
:label="t('action.properties')"
:shortcut="['P']"
@click="
() => {
emit('edit-properties')
hide()
}
"
/>
</div> </div>
</template> </template>
</tippy> </tippy>
@@ -193,8 +206,9 @@ import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit" import IconEdit from "~icons/lucide/edit"
import IconFolder from "~icons/lucide/folder" import IconFolder from "~icons/lucide/folder"
import IconFolderOpen from "~icons/lucide/folder-open" import IconFolderOpen from "~icons/lucide/folder-open"
import IconSettings2 from "~icons/lucide/settings-2"
import { ref, computed, watch } from "vue" import { ref, computed, watch } from "vue"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { TeamCollection } from "~/helpers/teams/TeamCollection" import { TeamCollection } from "~/helpers/teams/TeamCollection"
@@ -213,7 +227,7 @@ const props = withDefaults(
defineProps<{ defineProps<{
id: string id: string
parentID?: string | null parentID?: string | null
data: HoppCollection<HoppRESTRequest> | TeamCollection data: HoppCollection | TeamCollection
/** /**
* Collection component can be used for both collections and folders. * Collection component can be used for both collections and folders.
* folderType is used to determine which one it is. * folderType is used to determine which one it is.
@@ -245,6 +259,7 @@ const emit = defineEmits<{
(event: "add-request"): void (event: "add-request"): void
(event: "add-folder"): void (event: "add-folder"): void
(event: "edit-collection"): void (event: "edit-collection"): void
(event: "edit-properties"): void
(event: "export-data"): void (event: "export-data"): void
(event: "remove-collection"): void (event: "remove-collection"): void
(event: "drop-event", payload: DataTransfer): void (event: "drop-event", payload: DataTransfer): void
@@ -261,6 +276,7 @@ const edit = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null) const deleteAction = ref<HTMLButtonElement | null>(null)
const exportAction = ref<HTMLButtonElement | null>(null) const exportAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null) const options = ref<TippyComponent | null>(null)
const propertiesAction = ref<TippyComponent | null>(null)
const dragging = ref(false) const dragging = ref(false)
const ordering = ref(false) const ordering = ref(false)
@@ -294,8 +310,8 @@ const collectionIcon = computed(() => {
}) })
const collectionName = computed(() => { const collectionName = computed(() => {
if ((props.data as HoppCollection<HoppRESTRequest>).name) if ((props.data as HoppCollection).name)
return (props.data as HoppCollection<HoppRESTRequest>).name return (props.data as HoppCollection).name
return (props.data as TeamCollection).title return (props.data as TeamCollection).title
}) })

View File

@@ -29,7 +29,6 @@ import { PropType, computed, ref } from "vue"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { HoppCollection } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import { HoppRESTRequest } from "@hoppscotch/data"
import { appendRESTCollections, restCollections$ } from "~/newstore/collections" import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue" import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
@@ -88,9 +87,7 @@ const showImportFailedError = () => {
toast.error(t("import.failed")) toast.error(t("import.failed"))
} }
const handleImportToStore = async ( const handleImportToStore = async (collections: HoppCollection[]) => {
collections: HoppCollection<HoppRESTRequest>[]
) => {
const importResult = const importResult =
props.collectionsType.type === "my-collections" props.collectionsType.type === "my-collections"
? await importToPersonalWorkspace(collections) ? await importToPersonalWorkspace(collections)
@@ -104,26 +101,47 @@ const handleImportToStore = async (
} }
} }
const importToPersonalWorkspace = ( const importToPersonalWorkspace = (collections: HoppCollection[]) => {
collections: HoppCollection<HoppRESTRequest>[]
) => {
appendRESTCollections(collections) appendRESTCollections(collections)
return E.right({ return E.right({
success: true, success: true,
}) })
} }
const importToTeamsWorkspace = async ( function translateToTeamCollectionFormat(x: HoppCollection) {
collections: HoppCollection<HoppRESTRequest>[] const folders: HoppCollection[] = (x.folders ?? []).map(
) => { translateToTeamCollectionFormat
)
const data = {
auth: x.auth,
headers: x.headers,
}
const obj = {
...x,
folders,
data,
}
if (x.id) obj.id = x.id
return obj
}
const importToTeamsWorkspace = async (collections: HoppCollection[]) => {
if (!hasTeamWriteAccess.value || !selectedTeamID.value) { if (!hasTeamWriteAccess.value || !selectedTeamID.value) {
return E.left({ return E.left({
success: false, success: false,
}) })
} }
const transformedCollection = collections.map((collection) =>
translateToTeamCollectionFormat(collection)
)
const res = await toTeamsImporter( const res = await toTeamsImporter(
JSON.stringify(collections), JSON.stringify(transformedCollection),
selectedTeamID.value selectedTeamID.value
)() )()
@@ -407,7 +425,6 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
}, },
action: async () => { action: async () => {
isHoppTeamCollectionExporterInProgress.value = true isHoppTeamCollectionExporterInProgress.value = true
if ( if (
props.collectionsType.type === "team-collections" && props.collectionsType.type === "team-collections" &&
props.collectionsType.selectedTeam props.collectionsType.selectedTeam

View File

@@ -71,6 +71,13 @@
collection: node.data.data.data, collection: node.data.data.data,
}) })
" "
@edit-properties="
node.data.type === 'collections' &&
emit('edit-properties', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data=" @export-data="
node.data.type === 'collections' && node.data.type === 'collections' &&
emit('export-data', node.data.data.data) emit('export-data', node.data.data.data)
@@ -139,6 +146,13 @@
folder: node.data.data.data, folder: node.data.data.data,
}) })
" "
@edit-properties="
node.data.type === 'folders' &&
emit('edit-properties', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data=" @export-data="
node.data.type === 'folders' && node.data.type === 'folders' &&
emit('export-data', node.data.data.data) emit('export-data', node.data.data.data)
@@ -344,7 +358,7 @@ export type Collection = {
isLastItem: boolean isLastItem: boolean
data: { data: {
parentIndex: null parentIndex: null
data: HoppCollection<HoppRESTRequest> data: HoppCollection
} }
} }
@@ -353,7 +367,7 @@ type Folder = {
isLastItem: boolean isLastItem: boolean
data: { data: {
parentIndex: string parentIndex: string
data: HoppCollection<HoppRESTRequest> data: HoppCollection
} }
} }
@@ -380,7 +394,7 @@ type CollectionType =
const props = defineProps({ const props = defineProps({
filteredCollections: { filteredCollections: {
type: Array as PropType<HoppCollection<HoppRESTRequest>[]>, type: Array as PropType<HoppCollection[]>,
default: () => [], default: () => [],
required: true, required: true,
}, },
@@ -412,28 +426,35 @@ const emit = defineEmits<{
event: "add-request", event: "add-request",
payload: { payload: {
path: string path: string
folder: HoppCollection<HoppRESTRequest> folder: HoppCollection
} }
): void ): void
( (
event: "add-folder", event: "add-folder",
payload: { payload: {
path: string path: string
folder: HoppCollection<HoppRESTRequest> folder: HoppCollection
} }
): void ): void
( (
event: "edit-collection", event: "edit-collection",
payload: { payload: {
collectionIndex: string collectionIndex: string
collection: HoppCollection<HoppRESTRequest> collection: HoppCollection
} }
): void ): void
( (
event: "edit-folder", event: "edit-folder",
payload: { payload: {
folderPath: string folderPath: string
folder: HoppCollection<HoppRESTRequest> folder: HoppCollection
}
): void
(
event: "edit-properties",
payload: {
collectionIndex: string
collection: HoppCollection
} }
): void ): void
( (
@@ -451,7 +472,7 @@ const emit = defineEmits<{
request: HoppRESTRequest request: HoppRESTRequest
} }
): void ): void
(event: "export-data", payload: HoppCollection<HoppRESTRequest>): void (event: "export-data", payload: HoppCollection): void
(event: "remove-collection", payload: string): void (event: "remove-collection", payload: string): void
(event: "remove-folder", payload: string): void (event: "remove-folder", payload: string): void
( (
@@ -665,10 +686,10 @@ const updateCollectionOrder = (
type MyCollectionNode = Collection | Folder | Requests type MyCollectionNode = Collection | Folder | Requests
class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> { class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
constructor(public data: Ref<HoppCollection<HoppRESTRequest>[]>) {} constructor(public data: Ref<HoppCollection[]>) {}
navigateToFolderWithIndexPath( navigateToFolderWithIndexPath(
collections: HoppCollection<HoppRESTRequest>[], collections: HoppCollection[],
indexPaths: number[] indexPaths: number[]
) { ) {
if (indexPaths.length === 0) return null if (indexPaths.length === 0) return null

View File

@@ -0,0 +1,166 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('collection.properties')"
:full-width-body="true"
@close="hideModal"
>
<template #body>
<HoppSmartTabs
v-model="selectedOptionTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4"
render-inactive-tabs
>
<HoppSmartTab :id="'headers'" :label="`${t('tab.headers')}`">
<HttpHeaders
v-model="editableCollection"
:is-collection-property="true"
@change-tab="changeOptionTab"
/>
<div class="bg-bannerInfo p-2 flex items-center">
<icon-lucide-info class="svg-icons mr-2" />
{{ t("helpers.collection_properties_header") }}
<a href="hopp.sh" target="_blank" class="underline">{{
t("action.learn_more")
}}</a>
</div>
</HoppSmartTab>
<HoppSmartTab
:id="'authorization'"
:label="`${t('tab.authorization')}`"
>
<HttpAuthorization
v-model="editableCollection.auth"
:is-collection-property="true"
:is-root-collection="editingProperties?.isRootCollection"
:inherited-properties="editingProperties?.inheritedProperties"
/>
<div class="bg-bannerInfo p-2 flex items-center">
<icon-lucide-info class="svg-icons mr-2" />
{{ t("helpers.collection_properties_authorization") }}
<a href="hopp.sh" target="_blank" class="underline">{{
t("action.learn_more")
}}</a>
</div>
</HoppSmartTab>
</HoppSmartTabs>
</template>
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
:label="t('action.save')"
:loading="loadingState"
outline
@click="saveEditedCollection"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="hideModal"
/>
</span>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { watch, ref } from "vue"
import { useI18n } from "@composables/i18n"
import { HoppCollection } from "@hoppscotch/data"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
import { clone } from "lodash-es"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n()
type EditingProperties = {
collection: HoppCollection | TeamCollection | null
isRootCollection: boolean
path: string
inheritedProperties: HoppInheritedProperty | undefined
}
const props = withDefaults(
defineProps<{
show: boolean
loadingState: boolean
editingProperties: EditingProperties | null
}>(),
{
show: false,
loadingState: false,
editingProperties: null,
}
)
const emit = defineEmits<{
(e: "set-collection-properties", newCollection: any): void
(e: "hide-modal"): void
}>()
const editableCollection = ref({
body: {
contentType: null,
body: null,
},
headers: [],
auth: {
authType: "inherit",
authActive: false,
},
}) as any
const selectedOptionTab = ref("headers")
const changeOptionTab = (tab: RESTOptionTabs) => {
selectedOptionTab.value = tab
}
watch(
() => props.show,
(show) => {
if (show && props.editingProperties?.collection) {
editableCollection.value.auth = clone(
props.editingProperties.collection.auth
)
editableCollection.value.headers = clone(
props.editingProperties.collection.headers
)
} else {
editableCollection.value = {
body: {
contentType: null,
body: null,
},
headers: [],
auth: {
authType: "inherit",
authActive: false,
},
}
}
}
)
const saveEditedCollection = () => {
if (!props.editingProperties) return
const finalCollection = clone(editableCollection.value)
delete finalCollection.body
const collection = {
path: props.editingProperties.path,
collection: {
...props.editingProperties.collection,
...finalCollection,
},
isRootCollection: props.editingProperties.isRootCollection,
}
emit("set-collection-properties", collection)
}
const hideModal = () => {
emit("hide-modal")
}
</script>

View File

@@ -74,6 +74,7 @@ import { Picked } from "~/helpers/types/HoppPicked"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { import {
cascadeParentCollectionForHeaderAuth,
editGraphqlRequest, editGraphqlRequest,
editRESTRequest, editRESTRequest,
saveGraphqlRequestAs, 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({ platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST", type: "HOPP_SAVE_REQUEST",
createdNow: true, 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({ platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST", type: "HOPP_SAVE_REQUEST",
createdNow: true, 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({ platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST", type: "HOPP_SAVE_REQUEST",
createdNow: false, createdNow: false,
@@ -378,6 +409,16 @@ const saveRequestAs = async () => {
workspaceType: "team", workspaceType: "team",
}) })
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved() requestSaved()
} else if (picked.value.pickedType === "gql-my-folder") { } else if (picked.value.pickedType === "gql-my-folder") {
// TODO: Check for GQL request ? // TODO: Check for GQL request ?
@@ -393,6 +434,16 @@ const saveRequestAs = async () => {
workspaceType: "team", workspaceType: "team",
}) })
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved() requestSaved()
} else if (picked.value.pickedType === "gql-my-collection") { } else if (picked.value.pickedType === "gql-my-collection") {
// TODO: Check for GQL request ? // TODO: Check for GQL request ?
@@ -408,6 +459,16 @@ const saveRequestAs = async () => {
workspaceType: "team", workspaceType: "team",
}) })
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${picked.value.collectionIndex}`,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved() requestSaved()
} }
} }

View File

@@ -88,6 +88,13 @@
collection: node.data.data.data, collection: node.data.data.data,
}) })
" "
@edit-properties="
node.data.type === 'collections' &&
emit('edit-properties', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data=" @export-data="
node.data.type === 'collections' && node.data.type === 'collections' &&
emit('export-data', node.data.data.data) emit('export-data', node.data.data.data)
@@ -159,6 +166,13 @@
folder: node.data.data.data, folder: node.data.data.data,
}) })
" "
@edit-properties="
node.data.type === 'folders' &&
emit('edit-properties', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data=" @export-data="
node.data.type === 'folders' && node.data.type === 'folders' &&
emit('export-data', node.data.data.data) emit('export-data', node.data.data.data)
@@ -238,6 +252,7 @@
selectRequest({ selectRequest({
request: node.data.data.data.request, request: node.data.data.data.request,
requestIndex: node.data.data.data.id, requestIndex: node.data.data.data.id,
folderPath: getPath(node.id),
}) })
" "
@share-request=" @share-request="
@@ -452,6 +467,13 @@ const emit = defineEmits<{
folder: TeamCollection folder: TeamCollection
} }
): void ): void
(
event: "edit-properties",
payload: {
collectionIndex: string
collection: TeamCollection
}
): void
( (
event: "edit-request", event: "edit-request",
payload: { payload: {
@@ -482,7 +504,7 @@ const emit = defineEmits<{
request: HoppRESTRequest request: HoppRESTRequest
requestIndex: string requestIndex: string
isActive: boolean isActive: boolean
folderPath?: string | undefined folderPath: string
} }
): void ): void
( (
@@ -530,6 +552,12 @@ const emit = defineEmits<{
(event: "display-modal-import-export"): void (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 teamCollectionsList = toRef(props, "teamCollectionList")
const hasNoTeamAccess = computed( const hasNoTeamAccess = computed(
@@ -586,6 +614,7 @@ const isActiveRequest = (requestID: string) => {
const selectRequest = (data: { const selectRequest = (data: {
request: HoppRESTRequest request: HoppRESTRequest
requestIndex: string requestIndex: string
folderPath: string | null
}) => { }) => {
const { request, requestIndex } = data const { request, requestIndex } = data
if (props.saveRequest) { if (props.saveRequest) {
@@ -598,6 +627,7 @@ const selectRequest = (data: {
request: request, request: request,
requestIndex: requestIndex, requestIndex: requestIndex,
isActive: isActiveRequest(requestIndex), isActive: isActiveRequest(requestIndex),
folderPath: data.folderPath,
}) })
} }
} }

View File

@@ -32,58 +32,58 @@
</HoppSmartModal> </HoppSmartModal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue" import { ref } from "vue"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { HoppGQLRequest, makeCollection } from "@hoppscotch/data" import { makeCollection } from "@hoppscotch/data"
import { addGraphqlCollection } from "~/newstore/collections" import { addGraphqlCollection } from "~/newstore/collections"
import { platform } from "~/platform" import { platform } from "~/platform"
export default defineComponent({ const t = useI18n()
props: { const toast = useToast()
show: Boolean,
},
emits: ["hide-modal"],
setup() {
return {
toast: useToast(),
t: useI18n(),
}
},
data() {
return {
name: null as string | null,
}
},
methods: {
addNewCollection() {
if (!this.name) {
this.toast.error(`${this.t("collection.invalid_name")}`)
return
}
addGraphqlCollection( defineProps<{
makeCollection<HoppGQLRequest>({ show: boolean
name: this.name, }>()
folders: [],
requests: [],
})
)
this.hideModal() const emit = defineEmits<{
(e: "hide-modal"): void
}>()
platform.analytics?.logEvent({ const name = ref<string | null>(null)
type: "HOPP_CREATE_COLLECTION",
isRootCollection: true, const addNewCollection = () => {
platform: "gql", if (!name.value) {
workspaceType: "personal", toast.error(`${t("collection.invalid_name")}`)
}) return
}, }
hideModal() {
this.name = null addGraphqlCollection(
this.$emit("hide-modal") makeCollection({
}, name: name.value,
}, folders: [],
}) requests: [],
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
})
)
hideModal()
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: true,
platform: "gql",
workspaceType: "personal",
})
}
const hideModal = () => {
name.value = null
emit("hide-modal")
}
</script> </script>

View File

@@ -3,7 +3,7 @@
v-if="show" v-if="show"
dialog dialog
:title="t('folder.new')" :title="t('folder.new')"
@close="$emit('hide-modal')" @close="hideModal"
> >
<template #body> <template #body>
<HoppSmartInput <HoppSmartInput
@@ -32,47 +32,49 @@
</HoppSmartModal> </HoppSmartModal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { ref } from "vue"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { defineComponent } from "vue"
export default defineComponent({ const t = useI18n()
props: { const toast = useToast()
show: Boolean,
folderPath: { type: String, default: null }, const props = defineProps<{
collectionIndex: { type: Number, default: null }, show: boolean
}, folderPath?: string
emits: ["hide-modal", "add-folder"], collectionIndex: number
setup() { }>()
return {
toast: useToast(), const emit = defineEmits<{
t: useI18n(), (e: "hide-modal"): void
(
e: "add-folder",
v: {
name: string
path: string | undefined
} }
}, ): void
data() { }>()
return {
name: null,
}
},
methods: {
addFolder() {
if (!this.name) {
this.toast.error(`${this.t("folder.name_length_insufficient")}`)
return
}
this.$emit("add-folder", { const name = ref<string | null>(null)
name: this.name,
path: this.folderPath || `${this.collectionIndex}`,
})
this.hideModal() const addFolder = () => {
}, if (!name.value) {
hideModal() { toast.error(`${t("folder.name_length_insufficient")}`)
this.name = null return
this.$emit("hide-modal") }
},
}, emit("add-folder", {
}) name: name.value,
path: props.folderPath || `${props.collectionIndex}`,
})
hideModal()
}
const hideModal = () => {
name.value = null
emit("hide-modal")
}
</script> </script>

View File

@@ -128,6 +128,21 @@
} }
" "
/> />
<HoppSmartItem
ref="propertiesAction"
:icon="IconSettings2"
:label="t('action.properties')"
:shortcut="['P']"
@click="
() => {
emit('edit-properties', {
collectionIndex: String(collectionIndex),
collection: collection,
})
hide()
}
"
/>
</div> </div>
</template> </template>
</tippy> </tippy>
@@ -155,7 +170,15 @@
@edit-folder="$emit('edit-folder', $event)" @edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)" @edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)" @duplicate-request="$emit('duplicate-request', $event)"
@edit-properties="
$emit('edit-properties', {
collectionIndex: `${collectionIndex}/${String(index)}`,
collection: folder,
})
"
@select="$emit('select', $event)" @select="$emit('select', $event)"
@select-request="$emit('select-request', $event)"
@drop-request="$emit('drop-request', $event)"
/> />
<CollectionsGraphqlRequest <CollectionsGraphqlRequest
v-for="(request, index) in collection.requests" v-for="(request, index) in collection.requests"
@@ -171,6 +194,7 @@
@edit-request="$emit('edit-request', $event)" @edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)" @duplicate-request="$emit('duplicate-request', $event)"
@select="$emit('select', $event)" @select="$emit('select', $event)"
@select-request="$emit('select-request', $event)"
/> />
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if=" v-if="
@@ -214,25 +238,24 @@ import IconFolderPlus from "~icons/lucide/folder-plus"
import IconMoreVertical from "~icons/lucide/more-vertical" import IconMoreVertical from "~icons/lucide/more-vertical"
import IconEdit from "~icons/lucide/edit" import IconEdit from "~icons/lucide/edit"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconSettings2 from "~icons/lucide/settings-2"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { import { removeGraphqlCollection } from "~/newstore/collections"
removeGraphqlCollection,
moveGraphqlRequest,
} from "~/newstore/collections"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql" import { GQLTabService } from "~/services/tab/graphql"
import { HoppCollection } from "@hoppscotch/data"
const props = defineProps({ const props = defineProps<{
picked: { type: Object, default: null }, picked: Picked | null
// Whether the viewing context is related to picking (activates 'select' events) // Whether the viewing context is related to picking (activates 'select' events)
saveRequest: { type: Boolean, default: false }, saveRequest: boolean
collectionIndex: { type: Number, default: null }, collectionIndex: number | null
collection: { type: Object, default: () => ({}) }, collection: HoppCollection
isFiltered: Boolean, isFiltered: boolean
}) }>()
const colorMode = useColorMode() const colorMode = useColorMode()
const toast = useToast() const toast = useToast()
@@ -248,7 +271,23 @@ const emit = defineEmits<{
(e: "add-request", i: any): void (e: "add-request", i: any): void
(e: "add-folder", i: any): void (e: "add-folder", i: any): void
(e: "edit-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: "edit-collection"): void
(e: "select-request", i: any): void
(
e: "drop-request",
payload: {
folderPath: string
requestIndex: string
collectionIndex: number | null
}
): void
}>() }>()
// Template refs // Template refs
@@ -324,6 +363,10 @@ const dropEvent = ({ dataTransfer }: any) => {
dragging.value = !dragging.value dragging.value = !dragging.value
const folderPath = dataTransfer.getData("folderPath") const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex") const requestIndex = dataTransfer.getData("requestIndex")
moveGraphqlRequest(folderPath, requestIndex, `${props.collectionIndex}`) emit("drop-request", {
folderPath,
requestIndex,
collectionIndex: props.collectionIndex,
})
} }
</script> </script>

View File

@@ -37,13 +37,14 @@ import { ref, watch } from "vue"
import { editGraphqlCollection } from "~/newstore/collections" import { editGraphqlCollection } from "~/newstore/collections"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { HoppCollection } from "@hoppscotch/data"
const props = defineProps({ const props = defineProps<{
show: Boolean, show: boolean
editingCollection: { type: Object, default: () => ({}) }, editingCollectionIndex: number | null
editingCollectionIndex: { type: Number, default: null }, editingCollection: HoppCollection | null
editingCollectionName: { type: String, default: null }, editingCollectionName: string
}) }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void

View File

@@ -32,52 +32,47 @@
</HoppSmartModal> </HoppSmartModal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue" import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { editGraphqlFolder } from "~/newstore/collections" import { editGraphqlFolder } from "~/newstore/collections"
export default defineComponent({ const t = useI18n()
props: { const toast = useToast()
show: Boolean,
folder: { type: Object, default: () => ({}) }, const props = defineProps<{
folderPath: { type: String, default: null }, show: boolean
editingFolderName: { type: String, default: null }, folderPath?: string
}, folder: any
emits: ["hide-modal"], editingFolderName: string
setup() { }>()
return {
toast: useToast(), const emit = defineEmits(["hide-modal"])
t: useI18n(),
} const name = ref("")
},
data() { watch(
return { () => props.editingFolderName,
name: "", (val) => {
} name.value = val
}, }
watch: { )
editingFolderName(val) {
this.name = val const editFolder = () => {
}, if (!name.value) {
}, toast.error(`${t("collection.invalid_name")}`)
methods: { return
editFolder() { }
if (!this.name) { editGraphqlFolder(props.folderPath, {
this.toast.error(`${this.t("collection.invalid_name")}`) ...(props.folder as any),
return name: name.value,
} })
editGraphqlFolder(this.folderPath, { hideModal()
...(this.folder as any), }
name: this.name,
}) const hideModal = () => {
this.hideModal() name.value = ""
}, emit("hide-modal")
hideModal() { }
this.name = ""
this.$emit("hide-modal")
},
},
})
</script> </script>

View File

@@ -32,61 +32,55 @@
</HoppSmartModal> </HoppSmartModal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, PropType } from "vue" import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { HoppGQLRequest } from "@hoppscotch/data" import { HoppGQLRequest } from "@hoppscotch/data"
import { editGraphqlRequest } from "~/newstore/collections" import { editGraphqlRequest } from "~/newstore/collections"
export default defineComponent({ const t = useI18n()
props: { const toast = useToast()
show: Boolean,
folderPath: { type: String, default: null },
request: { type: Object as PropType<HoppGQLRequest>, default: () => ({}) },
requestIndex: { type: Number, default: null },
editingRequestName: { type: String, default: null },
},
emits: ["hide-modal"],
setup() {
return {
toast: useToast(),
t: useI18n(),
}
},
data() {
return {
requestUpdateData: {
name: null as any | null,
},
}
},
watch: {
editingRequestName(val) {
this.requestUpdateData.name = val
},
},
methods: {
saveRequest() {
if (!this.requestUpdateData.name) {
this.toast.error(`${this.t("collection.invalid_name")}`)
return
}
// TODO: Type safety goes brrrr. Proper typing plz const props = defineProps<{
const requestUpdated = { show: boolean
...this.$props.request, folderPath?: string
name: this.$data.requestUpdateData.name || this.$props.request.name, requestIndex: number | null
} request: HoppGQLRequest | null
editingRequestName: string
}>()
editGraphqlRequest(this.folderPath, this.requestIndex, requestUpdated) const emit = defineEmits<{
(e: "hide-modal"): void
}>()
this.hideModal() const requestUpdateData = ref({ name: null as string | null })
},
hideModal() { watch(
this.requestUpdateData = { name: null } () => props.editingRequestName,
this.$emit("hide-modal") (val) => {
}, requestUpdateData.value.name = val
}, }
}) )
const saveRequest = () => {
if (!requestUpdateData.value.name) {
toast.error(`${t("collection.invalid_name")}`)
return
}
const requestUpdated = {
...(props.request as any),
name: requestUpdateData.value.name || (props.request as any).name,
}
editGraphqlRequest(props.folderPath, props.requestIndex, requestUpdated)
hideModal()
}
const hideModal = () => {
requestUpdateData.value = { name: null }
emit("hide-modal")
}
</script> </script>

View File

@@ -10,24 +10,25 @@
@dragend="dragging = false" @dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()" @contextmenu.prevent="options.tippy.show()"
> >
<span <div
class="flex cursor-pointer items-center justify-center px-4" class="flex min-w-0 flex-1 items-center justify-center cursor-pointer"
@click="toggleShowChildren()" @click="toggleShowChildren()"
> >
<component <span class="pointer-events-none flex items-center justify-center px-4">
:is="collectionIcon" <component
class="svg-icons" :is="collectionIcon"
:class="{ 'text-accent': isSelected }" class="svg-icons"
/> :class="{ 'text-accent': isSelected }"
</span> />
<span
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ folder.name ? folder.name : folder.title }}
</span> </span>
</span> <span
class="pointer-events-none flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ folder.name ? folder.name : folder.title }}
</span>
</span>
</div>
<div class="flex"> <div class="flex">
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -120,6 +121,21 @@
} }
" "
/> />
<HoppSmartItem
ref="propertiesAction"
:icon="IconSettings2"
:label="t('action.properties')"
:shortcut="['P']"
@click="
() => {
emit('edit-properties', {
collectionIndex: collectionIndex,
collection: collection,
})
hide()
}
"
/>
</div> </div>
</template> </template>
</tippy> </tippy>
@@ -148,7 +164,14 @@
@edit-folder="emit('edit-folder', $event)" @edit-folder="emit('edit-folder', $event)"
@edit-request="emit('edit-request', $event)" @edit-request="emit('edit-request', $event)"
@duplicate-request="emit('duplicate-request', $event)" @duplicate-request="emit('duplicate-request', $event)"
@edit-properties="
emit('edit-properties', {
collectionIndex: `${folderPath}/${String(subFolderIndex)}`,
collection: subFolder,
})
"
@select="emit('select', $event)" @select="emit('select', $event)"
@select-request="$emit('select-request', $event)"
/> />
<CollectionsGraphqlRequest <CollectionsGraphqlRequest
v-for="(request, index) in folder.requests" v-for="(request, index) in folder.requests"
@@ -164,6 +187,7 @@
@edit-request="emit('edit-request', $event)" @edit-request="emit('edit-request', $event)"
@duplicate-request="emit('duplicate-request', $event)" @duplicate-request="emit('duplicate-request', $event)"
@select="emit('select', $event)" @select="emit('select', $event)"
@select-request="$emit('select-request', $event)"
/> />
<HoppSmartPlaceholder <HoppSmartPlaceholder
@@ -197,13 +221,16 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
import IconCheckCircle from "~icons/lucide/check-circle" import IconCheckCircle from "~icons/lucide/check-circle"
import IconFolder from "~icons/lucide/folder" import IconFolder from "~icons/lucide/folder"
import IconFolderOpen from "~icons/lucide/folder-open" import IconFolderOpen from "~icons/lucide/folder-open"
import IconSettings2 from "~icons/lucide/settings-2"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections" import { removeGraphqlFolder } from "~/newstore/collections"
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql" import { GQLTabService } from "~/services/tab/graphql"
import { Picked } from "~/helpers/types/HoppPicked"
import { HoppCollection } from "@hoppscotch/data"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
@@ -211,16 +238,16 @@ const colorMode = useColorMode()
const tabs = useService(GQLTabService) const tabs = useService(GQLTabService)
const props = defineProps({ const props = defineProps<{
picked: { type: Object, default: null }, picked: Picked
// Whether the request is in a selectable mode (activates 'select' event) // Whether the request is in a selectable mode (activates 'select' event)
saveRequest: { type: Boolean, default: false }, saveRequest: boolean
folder: { type: Object, default: () => ({}) }, folder: HoppCollection
folderIndex: { type: Number, default: null }, folderIndex: number
collectionIndex: { type: Number, default: null }, collectionIndex: number
folderPath: { type: String, default: null }, folderPath: string
isFiltered: Boolean, isFiltered: boolean
}) }>()
const emit = defineEmits([ const emit = defineEmits([
"select", "select",
@@ -229,6 +256,9 @@ const emit = defineEmits([
"add-folder", "add-folder",
"edit-folder", "edit-folder",
"duplicate-request", "duplicate-request",
"edit-properties",
"select-request",
"drop-request",
]) ])
// Template refs // Template refs
@@ -303,6 +333,11 @@ const dropEvent = ({ dataTransfer }: any) => {
dragging.value = !dragging.value dragging.value = !dragging.value
const folderPath = dataTransfer.getData("folderPath") const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex") const requestIndex = dataTransfer.getData("requestIndex")
moveGraphqlRequest(folderPath, requestIndex, props.folderPath)
emit("drop-request", {
folderPath,
requestIndex,
collectionIndex: props.folderPath,
})
} }
</script> </script>

View File

@@ -11,7 +11,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import { ImporterOrExporter } from "~/components/importExport/types" import { ImporterOrExporter } from "~/components/importExport/types"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource" import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource" import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
@@ -25,13 +25,14 @@ import { useReadonlyStream } from "~/composables/stream"
import { platform } from "~/platform" import { platform } from "~/platform"
import { import {
appendGraphqlCollections,
graphqlCollections$, graphqlCollections$,
setGraphqlCollections,
} from "~/newstore/collections" } from "~/newstore/collections"
import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql" import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql"
import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections" import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections"
import { gqlCollectionsGistExporter } from "~/helpers/import-export/export/gqlCollectionsGistExporter" import { gqlCollectionsGistExporter } from "~/helpers/import-export/export/gqlCollectionsGistExporter"
import { computed } from "vue" import { computed } from "vue"
import { hoppGQLImporter } from "~/helpers/import-export/import/hopp"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -60,15 +61,20 @@ const GqlCollectionsHoppImporter: ImporterOrExporter = {
showImportFailedError() showImportFailedError()
return return
} }
const validatedCollection = await hoppGQLImporter(
JSON.stringify(res.right)
)()
handleImportToStore(res.right) if (E.isRight(validatedCollection)) {
handleImportToStore(validatedCollection.right)
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION", type: "HOPP_IMPORT_COLLECTION",
platform: "gql", platform: "gql",
workspaceType: "personal", workspaceType: "personal",
importer: "json", importer: "json",
}) })
}
emit("hide-modal") emit("hide-modal")
}, },
@@ -214,11 +220,9 @@ const showImportFailedError = () => {
toast.error(t("import.failed")) toast.error(t("import.failed"))
} }
const handleImportToStore = async ( const handleImportToStore = async (gqlCollections: HoppCollection[]) => {
gqlCollections: HoppCollection<HoppGQLRequest>[] appendGraphqlCollections(gqlCollections)
) => { toast.success(t("state.file_imported"))
setGraphqlCollections(gqlCollections)
toast.success(t("import.success"))
} }
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -9,38 +9,41 @@
@dragend="dragging = false" @dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()" @contextmenu.prevent="options.tippy.show()"
> >
<span <div
class="flex w-16 cursor-pointer items-center justify-center truncate px-2" class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
@click="selectRequest()" @click="selectRequest()"
> >
<component <span
:is="isSelected ? IconCheckCircle : IconFile" class="pointer-events-none flex w-8 items-center justify-center truncate px-6"
class="svg-icons" >
:class="{ 'text-accent': isSelected }" <component
/> :is="isSelected ? IconCheckCircle : IconFile"
</span> class="svg-icons"
<span :class="{ 'text-accent': isSelected }"
class="flex min-w-0 flex-1 cursor-pointer items-center py-2 pr-2 transition group-hover:text-secondaryDark" />
@click="selectRequest()"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }}
</span> </span>
<span <span
v-if="isActive" class="pointer-events-none flex min-w-0 flex-1 items-center py-2 pr-2 transition group-hover:text-secondaryDark"
v-tippy="{ theme: 'tooltip' }"
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
:title="`${t('collection.request_in_use')}`"
> >
<span <span class="truncate" :class="{ 'text-accent': isSelected }">
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75" {{ request.name }}
>
</span> </span>
<span <span
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500" v-if="isActive"
></span> v-tippy="{ theme: 'tooltip' }"
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
:title="`${t('collection.request_in_use')}`"
>
<span
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
>
</span>
<span
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
></span>
</span>
</span> </span>
</span> </div>
<div class="flex"> <div class="flex">
<span> <span>
<tippy <tippy
@@ -134,8 +137,7 @@ import IconTrash2 from "~icons/lucide/trash-2"
import { PropType, computed, ref } from "vue" import { PropType, computed, ref } from "vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data" import { HoppGQLRequest } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es"
import { removeGraphqlRequest } from "~/newstore/collections" import { removeGraphqlRequest } from "~/newstore/collections"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql" import { GQLTabService } from "~/services/tab/graphql"
@@ -175,7 +177,12 @@ const isActive = computed(() => {
}) })
// TODO: Better types please // TODO: Better types please
const emit = defineEmits(["select", "edit-request", "duplicate-request"]) const emit = defineEmits([
"select",
"edit-request",
"duplicate-request",
"select-request",
])
const dragging = ref(false) const dragging = ref(false)
const confirmRemove = ref(false) const confirmRemove = ref(false)
@@ -199,36 +206,11 @@ const selectRequest = () => {
if (props.saveRequest) { if (props.saveRequest) {
pick() pick()
} else { } else {
const possibleTab = tabs.getTabRefWithSaveContext({ emit("select-request", {
originLocation: "user-collection", request: props.request,
folderPath: props.folderPath, folderPath: props.folderPath,
requestIndex: props.requestIndex, requestIndex: props.requestIndex,
}) })
// Switch to that request if that request is open
if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id)
return
}
tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
},
request: cloneDeep(
makeGQLRequest({
name: props.request.name,
url: props.request.url,
query: props.request.query,
headers: props.request.headers,
variables: props.request.variables,
auth: props.request.auth,
})
),
isDirty: false,
})
} }
} }

View File

@@ -57,7 +57,10 @@
@edit-request="editRequest($event)" @edit-request="editRequest($event)"
@duplicate-request="duplicateRequest($event)" @duplicate-request="duplicateRequest($event)"
@select-collection="$emit('use-collection', collection)" @select-collection="$emit('use-collection', collection)"
@edit-properties="editProperties($event)"
@select="$emit('select', $event)" @select="$emit('select', $event)"
@select-request="selectRequest($event)"
@drop-request="dropRequest($event)"
/> />
</div> </div>
<HoppSmartPlaceholder <HoppSmartPlaceholder
@@ -142,19 +145,27 @@
v-if="showModalImportExport" v-if="showModalImportExport"
@hide-modal="displayModalImportExport(false)" @hide-modal="displayModalImportExport(false)"
/> />
<CollectionsProperties
:show="showModalEditProperties"
:editing-properties="editingProperties"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
// TODO: TypeScript + Script Setup this :) import { nextTick, ref } from "vue"
import { defineComponent } from "vue" import { clone, cloneDeep } from "lodash-es"
import { cloneDeep, clone } from "lodash-es"
import { import {
graphqlCollections$, graphqlCollections$,
addGraphqlFolder, addGraphqlFolder,
saveGraphqlRequestAs, saveGraphqlRequestAs,
cascadeParentCollectionForHeaderAuth,
editGraphqlCollection,
editGraphqlFolder,
moveGraphqlRequest,
} from "~/newstore/collections" } from "~/newstore/collections"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down" import IconImport from "~icons/lucide/folder-down"
@@ -164,213 +175,448 @@ import { useColorMode } from "@composables/theming"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql" import { GQLTabService } from "~/services/tab/graphql"
import { computed } from "vue"
import {
HoppCollection,
HoppGQLRequest,
makeGQLRequest,
} from "@hoppscotch/data"
import { Picked } from "~/helpers/types/HoppPicked"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
import { useToast } from "~/composables/toast"
import { getRequestsByPath } from "~/helpers/collection/request"
export default defineComponent({ const t = useI18n()
props: { const toast = useToast()
// Whether to activate the ability to pick items (activates 'select' events)
saveRequest: { type: Boolean, default: false },
picked: { type: Object, default: null },
},
emits: ["select", "use-collection"],
setup() {
const collections = useReadonlyStream(graphqlCollections$, [], "deep")
const colorMode = useColorMode()
const t = useI18n()
const tabs = useService(GQLTabService)
return { defineProps<{
collections, // Whether to activate the ability to pick items (activates 'select' events)
colorMode, saveRequest: boolean
t, picked: Picked
tabs, }>()
IconPlus,
IconHelpCircle,
IconImport,
}
},
data() {
return {
showModalAdd: false,
showModalEdit: false,
showModalImportExport: false,
showModalAddRequest: false,
showModalAddFolder: false,
showModalEditFolder: false,
showModalEditRequest: false,
editingCollection: undefined,
editingCollectionIndex: undefined,
editingFolder: undefined,
editingFolderName: undefined,
editingFolderIndex: undefined,
editingFolderPath: undefined,
editingRequest: undefined,
editingRequestIndex: undefined,
filterText: "",
}
},
computed: {
filteredCollections() {
const collections = clone(this.collections)
if (!this.filterText) return collections const collections = useReadonlyStream(graphqlCollections$, [], "deep")
const colorMode = useColorMode()
const tabs = useService(GQLTabService)
const filterText = this.filterText.toLowerCase() const showModalAdd = ref(false)
const filteredCollections = [] const showModalEdit = ref(false)
const showModalImportExport = ref(false)
const showModalAddRequest = ref(false)
const showModalAddFolder = ref(false)
const showModalEditFolder = ref(false)
const showModalEditRequest = ref(false)
const showModalEditProperties = ref(false)
for (const collection of collections) { const editingCollection = ref<HoppCollection | null>(null)
const filteredRequests = [] const editingCollectionIndex = ref<number | null>(null)
const filteredFolders = [] const editingFolder = ref<HoppCollection | null>(null)
for (const request of collection.requests) { const editingFolderName = ref("")
if (request.name.toLowerCase().includes(filterText)) const editingFolderIndex = ref<number | null>(null)
filteredRequests.push(request) const editingFolderPath = ref("")
} const editingRequest = ref<HoppGQLRequest | null>(null)
for (const folder of collection.folders) { const editingRequestIndex = ref<number | null>(null)
const filteredFolderRequests = []
for (const request of folder.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredFolderRequests.push(request)
}
if (filteredFolderRequests.length > 0) {
const filteredFolder = Object.assign({}, folder)
filteredFolder.requests = filteredFolderRequests
filteredFolders.push(filteredFolder)
}
}
if (filteredRequests.length + filteredFolders.length > 0) { const editingProperties = ref<{
const filteredCollection = Object.assign({}, collection) collection: HoppCollection | null
filteredCollection.requests = filteredRequests isRootCollection: boolean
filteredCollection.folders = filteredFolders path: string
filteredCollections.push(filteredCollection) inheritedProperties?: HoppInheritedProperty
} }>({
} collection: null,
isRootCollection: false,
return filteredCollections path: "",
}, inheritedProperties: undefined,
},
methods: {
displayModalAdd(shouldDisplay) {
this.showModalAdd = shouldDisplay
},
displayModalEdit(shouldDisplay) {
this.showModalEdit = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalImportExport(shouldDisplay) {
this.showModalImportExport = shouldDisplay
},
displayModalAddRequest(shouldDisplay) {
this.showModalAddRequest = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalAddFolder(shouldDisplay) {
this.showModalAddFolder = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalEditFolder(shouldDisplay) {
this.showModalEditFolder = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalEditRequest(shouldDisplay) {
this.showModalEditRequest = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
editCollection(collection, collectionIndex) {
this.$data.editingCollection = collection
this.$data.editingCollectionIndex = collectionIndex
this.displayModalEdit(true)
},
onAddRequest({ name, path, index }) {
const newRequest = {
...this.tabs.currentActiveTab.value.document.request,
name,
}
saveGraphqlRequestAs(path, newRequest)
this.tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: path,
requestIndex: index,
},
request: newRequest,
isDirty: false,
})
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
platform: "gql",
createdNow: true,
workspaceType: "personal",
})
this.displayModalAddRequest(false)
},
addRequest(payload) {
const { path } = payload
this.$data.editingFolderPath = path
this.displayModalAddRequest(true)
},
onAddFolder({ name, path }) {
addGraphqlFolder(name, path)
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: false,
platform: "gql",
workspaceType: "personal",
})
this.displayModalAddFolder(false)
},
addFolder(payload) {
const { path } = payload
this.$data.editingFolderPath = path
this.displayModalAddFolder(true)
},
editFolder(payload) {
const { folder, folderPath } = payload
this.editingFolder = folder
this.editingFolderPath = folderPath
this.displayModalEditFolder(true)
},
editRequest(payload) {
const {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
} = payload
this.$data.editingFolderPath = folderPath
this.$data.editingCollectionIndex = collectionIndex
this.$data.editingFolderIndex = folderIndex
this.$data.editingFolderName = folderName
this.$data.editingRequest = request
this.$data.editingRequestIndex = requestIndex
this.displayModalEditRequest(true)
},
resetSelectedData() {
this.$data.editingCollection = undefined
this.$data.editingCollectionIndex = undefined
this.$data.editingFolder = undefined
this.$data.editingFolderIndex = undefined
this.$data.editingRequest = undefined
this.$data.editingRequestIndex = undefined
},
duplicateRequest({ folderPath, request }) {
saveGraphqlRequestAs(folderPath, {
...cloneDeep(request),
name: `${request.name} - ${this.t("action.duplicate")}`,
})
},
},
}) })
const filterText = ref("")
const filteredCollections = computed(() => {
const collectionsClone = clone(collections.value)
if (!filterText.value) return collectionsClone
const filterTextLower = filterText.value.toLowerCase()
const filteredCollections = []
for (const collection of collectionsClone) {
const filteredRequests = []
const filteredFolders = []
for (const request of collection.requests) {
if (request.name.toLowerCase().includes(filterTextLower))
filteredRequests.push(request)
}
for (const folder of collection.folders) {
const filteredFolderRequests = []
for (const request of folder.requests) {
if (request.name.toLowerCase().includes(filterTextLower))
filteredFolderRequests.push(request)
}
if (filteredFolderRequests.length > 0) {
const filteredFolder = { ...folder }
filteredFolder.requests = filteredFolderRequests
filteredFolders.push(filteredFolder)
}
}
if (filteredRequests.length + filteredFolders.length > 0) {
const filteredCollection = { ...collection }
filteredCollection.requests = filteredRequests
filteredCollection.folders = filteredFolders
filteredCollections.push(filteredCollection)
}
}
return filteredCollections
})
const displayModalAdd = (shouldDisplay: boolean) => {
showModalAdd.value = shouldDisplay
}
const displayModalEdit = (shouldDisplay: boolean) => {
showModalEdit.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalImportExport = (shouldDisplay: boolean) => {
showModalImportExport.value = shouldDisplay
}
const displayModalAddRequest = (shouldDisplay: boolean) => {
showModalAddRequest.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalAddFolder = (shouldDisplay: boolean) => {
showModalAddFolder.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalEditFolder = (shouldDisplay: boolean) => {
showModalEditFolder.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalEditRequest = (shouldDisplay: boolean) => {
showModalEditRequest.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalEditProperties = (show: boolean) => {
showModalEditProperties.value = show
if (!show) resetSelectedData()
}
const editCollection = (
collection: HoppCollection,
collectionIndex: number
) => {
editingCollection.value = collection
editingCollectionIndex.value = collectionIndex
displayModalEdit(true)
}
const onAddRequest = ({
name,
path,
index,
}: {
name: string
path: string
index: number
}) => {
const newRequest = {
...tabs.currentActiveTab.value.document.request,
name,
}
saveGraphqlRequestAs(path, newRequest)
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
path,
"graphql"
)
tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: path,
requestIndex: index,
},
request: newRequest,
isDirty: false,
inheritedProperties: {
auth,
headers,
},
})
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
platform: "gql",
createdNow: true,
workspaceType: "personal",
})
displayModalAddRequest(false)
}
const addRequest = (payload: { path: string }) => {
const { path } = payload
editingFolderPath.value = path
displayModalAddRequest(true)
}
const onAddFolder = ({
name,
path,
}: {
name: string
path: string | undefined
}) => {
addGraphqlFolder(name, path ?? "0")
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: false,
platform: "gql",
workspaceType: "personal",
})
displayModalAddFolder(false)
}
const addFolder = (payload: { path: string }) => {
const { path } = payload
editingFolderPath.value = path
displayModalAddFolder(true)
}
const editFolder = (payload: {
folder: HoppCollection
folderPath: string
}) => {
const { folder, folderPath } = payload
editingFolder.value = folder
editingFolderPath.value = folderPath
displayModalEditFolder(true)
}
const editRequest = (payload: {
collectionIndex: number
folderIndex: number
folderName: string
request: HoppGQLRequest
requestIndex: number
folderPath: string
}) => {
const {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
} = payload
editingFolderPath.value = folderPath
editingCollectionIndex.value = collectionIndex
editingFolderIndex.value = folderIndex
editingFolderName.value = folderName
editingRequest.value = request
editingRequestIndex.value = requestIndex
displayModalEditRequest(true)
}
const duplicateRequest = ({
folderPath,
request,
}: {
folderPath: string
request: HoppGQLRequest
}) => {
saveGraphqlRequestAs(folderPath, {
...cloneDeep(request),
name: `${request.name} - ${t("action.duplicate")}`,
})
}
const selectRequest = ({
request,
folderPath,
requestIndex,
}: {
request: HoppGQLRequest
folderPath: string
requestIndex: number
}) => {
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: folderPath,
requestIndex: requestIndex,
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath,
"graphql"
)
// Switch to that request if that request is open
if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id)
return
}
tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: folderPath,
requestIndex: requestIndex,
},
request: cloneDeep(
makeGQLRequest({
name: request.name,
url: request.url,
query: request.query,
headers: request.headers,
variables: request.variables,
auth: request.auth,
})
),
isDirty: false,
inheritedProperties: {
auth,
headers,
},
})
}
const dropRequest = ({
folderPath,
requestIndex,
collectionIndex,
}: {
folderPath: string
requestIndex: number
collectionIndex: number
}) => {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${collectionIndex}`,
"graphql"
)
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath,
requestIndex: Number(requestIndex),
})
if (possibleTab) {
possibleTab.value.document.saveContext = {
originLocation: "user-collection",
folderPath: `${collectionIndex}`,
requestIndex: getRequestsByPath(collections.value, `${collectionIndex}`)
.length,
}
possibleTab.value.document.inheritedProperties = {
auth,
headers,
}
}
moveGraphqlRequest(folderPath, requestIndex, `${collectionIndex}`)
toast.success(`${t("request.moved")}`)
}
/**
* Checks if the collection is already in the root
* @param id - path of the collection
* @returns boolean - true if the collection is already in the root
*/
const isAlreadyInRoot = (id: string) => {
const indexPath = id.split("/")
return indexPath.length === 1
}
const editProperties = ({
collectionIndex,
collection,
}: {
collectionIndex: string | null
collection: HoppCollection | null
}) => {
if (collectionIndex === null || collection === null) return
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
let inheritedProperties = {}
if (parentIndex) {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
parentIndex,
"graphql"
)
inheritedProperties = {
auth,
headers,
} as HoppInheritedProperty
}
editingProperties.value = {
collection,
isRootCollection: isAlreadyInRoot(collectionIndex),
path: collectionIndex,
inheritedProperties,
}
displayModalEditProperties(true)
}
const setCollectionProperties = (newCollection: {
collection: HoppCollection
path: string
isRootCollection: boolean
}) => {
const { collection, path, isRootCollection } = newCollection
if (isRootCollection) {
editGraphqlCollection(parseInt(path), collection)
} else {
editGraphqlFolder(path, collection)
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
path,
"graphql"
)
nextTick(() => {
updateInheritedPropertiesForAffectedRequests(
path,
{
auth,
headers,
},
"graphql"
)
})
displayModalEditProperties(false)
}
const resetSelectedData = () => {
editingCollection.value = null
editingCollectionIndex.value = null
editingFolder.value = null
editingFolderIndex.value = null
editingRequest.value = null
editingRequestIndex.value = null
}
</script> </script>

View File

@@ -38,6 +38,7 @@
@add-request="addRequest" @add-request="addRequest"
@edit-collection="editCollection" @edit-collection="editCollection"
@edit-folder="editFolder" @edit-folder="editFolder"
@edit-properties="editProperties"
@export-data="exportData" @export-data="exportData"
@remove-collection="removeCollection" @remove-collection="removeCollection"
@remove-folder="removeFolder" @remove-folder="removeFolder"
@@ -69,6 +70,7 @@
@add-folder="addFolder" @add-folder="addFolder"
@edit-collection="editCollection" @edit-collection="editCollection"
@edit-folder="editFolder" @edit-folder="editFolder"
@edit-properties="editProperties"
@export-data="exportData" @export-data="exportData"
@remove-collection="removeCollection" @remove-collection="removeCollection"
@remove-folder="removeFolder" @remove-folder="removeFolder"
@@ -151,6 +153,12 @@
:show="showTeamModalAdd" :show="showTeamModalAdd"
@hide-modal="displayTeamModalAdd(false)" @hide-modal="displayTeamModalAdd(false)"
/> />
<CollectionsProperties
:show="showModalEditProperties"
:editing-properties="editingProperties"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
/>
</div> </div>
</template> </template>
@@ -181,10 +189,13 @@ import {
moveRESTFolder, moveRESTFolder,
navigateToFolderWithIndexPath, navigateToFolderWithIndexPath,
restCollectionStore, restCollectionStore,
cascadeParentCollectionForHeaderAuth,
} from "~/newstore/collections" } from "~/newstore/collections"
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter" import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
import { import {
HoppCollection, HoppCollection,
HoppRESTAuth,
HoppRESTHeaders,
HoppRESTRequest, HoppRESTRequest,
makeCollection, makeCollection,
} from "@hoppscotch/data" } from "@hoppscotch/data"
@@ -193,10 +204,10 @@ import { GQLError } from "~/helpers/backend/GQLClient"
import { import {
createNewRootCollection, createNewRootCollection,
createChildCollection, createChildCollection,
renameCollection,
deleteCollection, deleteCollection,
moveRESTTeamCollection, moveRESTTeamCollection,
updateOrderRESTTeamCollection, updateOrderRESTTeamCollection,
updateTeamCollection,
} from "~/helpers/backend/mutations/TeamCollection" } from "~/helpers/backend/mutations/TeamCollection"
import { import {
updateTeamRequest, updateTeamRequest,
@@ -220,6 +231,7 @@ import {
getFoldersByPath, getFoldersByPath,
resolveSaveContextOnCollectionReorder, resolveSaveContextOnCollectionReorder,
updateSaveContextForAffectedRequests, updateSaveContextForAffectedRequests,
updateInheritedPropertiesForAffectedRequests,
resetTeamRequestsContext, resetTeamRequestsContext,
} from "~/helpers/collection/collection" } from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering" import { currentReorderingStatus$ } from "~/newstore/reordering"
@@ -227,6 +239,7 @@ import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -266,15 +279,11 @@ const collectionsType = ref<CollectionType>({
}) })
// Collection Data // Collection Data
const editingCollection = ref< const editingCollection = ref<HoppCollection | TeamCollection | null>(null)
HoppCollection<HoppRESTRequest> | TeamCollection | null
>(null)
const editingCollectionName = ref<string | null>(null) const editingCollectionName = ref<string | null>(null)
const editingCollectionIndex = ref<number | null>(null) const editingCollectionIndex = ref<number | null>(null)
const editingCollectionID = ref<string | null>(null) const editingCollectionID = ref<string | null>(null)
const editingFolder = ref< const editingFolder = ref<HoppCollection | TeamCollection | null>(null)
HoppCollection<HoppRESTRequest> | TeamCollection | null
>(null)
const editingFolderName = ref<string | null>(null) const editingFolderName = ref<string | null>(null)
const editingFolderPath = ref<string | null>(null) const editingFolderPath = ref<string | null>(null)
const editingRequest = ref<HoppRESTRequest | null>(null) const editingRequest = ref<HoppRESTRequest | null>(null)
@@ -282,6 +291,18 @@ const editingRequestName = ref("")
const editingRequestIndex = ref<number | null>(null) const editingRequestIndex = ref<number | null>(null)
const editingRequestID = ref<string | null>(null) const editingRequestID = ref<string | null>(null)
const editingProperties = ref<{
collection: Omit<HoppCollection, "v"> | TeamCollection | null
isRootCollection: boolean
path: string
inheritedProperties?: HoppInheritedProperty
}>({
collection: null,
isRootCollection: false,
path: "",
inheritedProperties: undefined,
})
const confirmModalTitle = ref<string | null>(null) const confirmModalTitle = ref<string | null>(null)
const filterTexts = ref("") const filterTexts = ref("")
@@ -520,6 +541,7 @@ const showModalEditCollection = ref(false)
const showModalEditFolder = ref(false) const showModalEditFolder = ref(false)
const showModalEditRequest = ref(false) const showModalEditRequest = ref(false)
const showModalImportExport = ref(false) const showModalImportExport = ref(false)
const showModalEditProperties = ref(false)
const showConfirmModal = ref(false) const showConfirmModal = ref(false)
const showTeamModalAdd = ref(false) const showTeamModalAdd = ref(false)
@@ -565,6 +587,12 @@ const displayModalImportExport = (show: boolean) => {
if (!show) resetSelectedData() if (!show) resetSelectedData()
} }
const displayModalEditProperties = (show: boolean) => {
showModalEditProperties.value = show
if (!show) resetSelectedData()
}
const displayConfirmModal = (show: boolean) => { const displayConfirmModal = (show: boolean) => {
showConfirmModal.value = show showConfirmModal.value = show
@@ -584,6 +612,11 @@ const addNewRootCollection = (name: string) => {
name, name,
folders: [], folders: [],
requests: [], requests: [],
headers: [],
auth: {
authType: "inherit",
authActive: false,
},
}) })
) )
@@ -625,7 +658,7 @@ const addNewRootCollection = (name: string) => {
const addRequest = (payload: { const addRequest = (payload: {
path: string path: string
folder: HoppCollection<HoppRESTRequest> | TeamCollection folder: HoppCollection | TeamCollection
}) => { }) => {
const { path, folder } = payload const { path, folder } = payload
editingFolder.value = folder editingFolder.value = folder
@@ -639,11 +672,13 @@ const onAddRequest = (requestName: string) => {
name: requestName, name: requestName,
} }
const path = editingFolderPath.value
if (!path) return
if (collectionsType.value.type === "my-collections") { if (collectionsType.value.type === "my-collections") {
const path = editingFolderPath.value
if (!path) return
const insertionIndex = saveRESTRequestAs(path, newRequest) const insertionIndex = saveRESTRequestAs(path, newRequest)
const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest")
tabs.createNewTab({ tabs.createNewTab({
request: newRequest, request: newRequest,
isDirty: false, isDirty: false,
@@ -652,6 +687,10 @@ const onAddRequest = (requestName: string) => {
folderPath: path, folderPath: path,
requestIndex: insertionIndex, requestIndex: insertionIndex,
}, },
inheritedProperties: {
auth,
headers,
},
}) })
platform.analytics?.logEvent({ platform.analytics?.logEvent({
@@ -692,7 +731,8 @@ const onAddRequest = (requestName: string) => {
}, },
(result) => { (result) => {
const { createRequestInCollection } = result const { createRequestInCollection } = result
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path)
tabs.createNewTab({ tabs.createNewTab({
request: newRequest, request: newRequest,
isDirty: false, isDirty: false,
@@ -702,6 +742,10 @@ const onAddRequest = (requestName: string) => {
collectionID: createRequestInCollection.collection.id, collectionID: createRequestInCollection.collection.id,
teamID: createRequestInCollection.collection.team.id, teamID: createRequestInCollection.collection.team.id,
}, },
inheritedProperties: {
auth,
headers,
},
}) })
modalLoadingState.value = false modalLoadingState.value = false
@@ -714,7 +758,7 @@ const onAddRequest = (requestName: string) => {
const addFolder = (payload: { const addFolder = (payload: {
path: string path: string
folder: HoppCollection<HoppRESTRequest> | TeamCollection folder: HoppCollection | TeamCollection
}) => { }) => {
const { path, folder } = payload const { path, folder } = payload
editingFolder.value = folder editingFolder.value = folder
@@ -773,15 +817,13 @@ const onAddFolder = (folderName: string) => {
const editCollection = (payload: { const editCollection = (payload: {
collectionIndex: string collectionIndex: string
collection: HoppCollection<HoppRESTRequest> | TeamCollection collection: HoppCollection | TeamCollection
}) => { }) => {
const { collectionIndex, collection } = payload const { collectionIndex, collection } = payload
editingCollection.value = collection editingCollection.value = collection
if (collectionsType.value.type === "my-collections") { if (collectionsType.value.type === "my-collections") {
editingCollectionIndex.value = parseInt(collectionIndex) editingCollectionIndex.value = parseInt(collectionIndex)
editingCollectionName.value = ( editingCollectionName.value = (collection as HoppCollection).name
collection as HoppCollection<HoppRESTRequest>
).name
} else { } else {
editingCollectionName.value = (collection as TeamCollection).title editingCollectionName.value = (collection as TeamCollection).title
} }
@@ -816,7 +858,7 @@ const updateEditingCollection = (newName: string) => {
modalLoadingState.value = true modalLoadingState.value = true
pipe( pipe(
renameCollection(editingCollection.value.id, newName), updateTeamCollection(editingCollection.value.id, undefined, newName),
TE.match( TE.match(
(err: GQLError<string>) => { (err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`) toast.error(`${getErrorMessage(err)}`)
@@ -834,13 +876,13 @@ const updateEditingCollection = (newName: string) => {
const editFolder = (payload: { const editFolder = (payload: {
folderPath: string | undefined folderPath: string | undefined
folder: HoppCollection<HoppRESTRequest> | TeamCollection folder: HoppCollection | TeamCollection
}) => { }) => {
const { folderPath, folder } = payload const { folderPath, folder } = payload
editingFolder.value = folder editingFolder.value = folder
if (collectionsType.value.type === "my-collections" && folderPath) { if (collectionsType.value.type === "my-collections" && folderPath) {
editingFolderPath.value = folderPath editingFolderPath.value = folderPath
editingFolderName.value = (folder as HoppCollection<HoppRESTRequest>).name editingFolderName.value = (folder as HoppCollection).name
} else { } else {
editingFolderName.value = (folder as TeamCollection).title editingFolderName.value = (folder as TeamCollection).title
} }
@@ -854,7 +896,7 @@ const updateEditingFolder = (newName: string) => {
if (!editingFolderPath.value) return if (!editingFolderPath.value) return
editRESTFolder(editingFolderPath.value, { editRESTFolder(editingFolderPath.value, {
...(editingFolder.value as HoppCollection<HoppRESTRequest>), ...(editingFolder.value as HoppCollection),
name: newName, name: newName,
}) })
displayModalEditFolder(false) displayModalEditFolder(false)
@@ -865,7 +907,7 @@ const updateEditingFolder = (newName: string) => {
/* renameCollection can be used to rename both collections and folders /* renameCollection can be used to rename both collections and folders
since folder is treated as collection in the BE. */ since folder is treated as collection in the BE. */
pipe( pipe(
renameCollection(editingFolder.value.id, newName), updateTeamCollection(editingFolder.value.id, undefined, newName),
TE.match( TE.match(
(err: GQLError<string>) => { (err: GQLError<string>) => {
if (err.error === "team_coll/short_title") { if (err.error === "team_coll/short_title") {
@@ -1279,16 +1321,18 @@ const selectPicked = (payload: Picked | null) => {
*/ */
const selectRequest = (selectedRequest: { const selectRequest = (selectedRequest: {
request: HoppRESTRequest request: HoppRESTRequest
folderPath: string | undefined folderPath: string
requestIndex: string requestIndex: string
isActive: boolean isActive: boolean
}) => { }) => {
const { request, folderPath, requestIndex } = selectedRequest const { request, folderPath, requestIndex } = selectedRequest
// If there is a request with this save context, switch into it // If there is a request with this save context, switch into it
let possibleTab = null let possibleTab = null
if (collectionsType.value.type === "team-collections") { if (collectionsType.value.type === "team-collections") {
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath)
possibleTab = tabs.getTabRefWithSaveContext({ possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID: requestIndex, requestID: requestIndex,
@@ -1302,10 +1346,19 @@ const selectRequest = (selectedRequest: {
saveContext: { saveContext: {
originLocation: "team-collection", originLocation: "team-collection",
requestID: requestIndex, requestID: requestIndex,
collectionID: folderPath,
},
inheritedProperties: {
auth,
headers,
}, },
}) })
} }
} else { } else {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath,
"rest"
)
possibleTab = tabs.getTabRefWithSaveContext({ possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
requestIndex: parseInt(requestIndex), requestIndex: parseInt(requestIndex),
@@ -1323,6 +1376,10 @@ const selectRequest = (selectedRequest: {
folderPath: folderPath!, folderPath: folderPath!,
requestIndex: parseInt(requestIndex), requestIndex: parseInt(requestIndex),
}, },
inheritedProperties: {
auth,
headers,
},
}) })
} }
} }
@@ -1349,16 +1406,17 @@ const dropRequest = (payload: {
}) => { }) => {
const { folderPath, requestIndex, destinationCollectionIndex } = payload const { folderPath, requestIndex, destinationCollectionIndex } = payload
if (!requestIndex || !destinationCollectionIndex) return if (!requestIndex || !destinationCollectionIndex || !folderPath) return
if (collectionsType.value.type === "my-collections" && folderPath) { let possibleTab = null
moveRESTRequest(
folderPath, if (collectionsType.value.type === "my-collections") {
pathToLastIndex(requestIndex), const { auth, headers } = cascadeParentCollectionForHeaderAuth(
destinationCollectionIndex destinationCollectionIndex,
"rest"
) )
const possibleTab = tabs.getTabRefWithSaveContext({ possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
folderPath, folderPath,
requestIndex: pathToLastIndex(requestIndex), requestIndex: pathToLastIndex(requestIndex),
@@ -1374,6 +1432,11 @@ const dropRequest = (payload: {
destinationCollectionIndex destinationCollectionIndex
).length, ).length,
} }
possibleTab.value.document.inheritedProperties = {
auth,
headers,
}
} }
// When it's drop it's basically getting deleted from last folder. reordering last folder accordingly // When it's drop it's basically getting deleted from last folder. reordering last folder accordingly
@@ -1383,6 +1446,11 @@ const dropRequest = (payload: {
folderPath, folderPath,
length: getRequestsByPath(myCollections.value, folderPath).length, length: getRequestsByPath(myCollections.value, folderPath).length,
}) })
moveRESTRequest(
folderPath,
pathToLastIndex(requestIndex),
destinationCollectionIndex
)
toast.success(`${t("request.moved")}`) toast.success(`${t("request.moved")}`)
draggingToRoot.value = false draggingToRoot.value = false
@@ -1406,8 +1474,12 @@ const dropRequest = (payload: {
requestMoveLoading.value.indexOf(requestIndex), requestMoveLoading.value.indexOf(requestIndex),
1 1
) )
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(
destinationCollectionIndex
)
const possibleTab = tabs.getTabRefWithSaveContext({ possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID: requestIndex, requestID: requestIndex,
}) })
@@ -1417,6 +1489,10 @@ const dropRequest = (payload: {
originLocation: "team-collection", originLocation: "team-collection",
requestID: requestIndex, requestID: requestIndex,
} }
possibleTab.value.document.inheritedProperties = {
auth,
headers,
}
} }
toast.success(`${t("request.moved")}`) toast.success(`${t("request.moved")}`)
} }
@@ -1537,6 +1613,22 @@ const dropCollection = (payload: {
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}` `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
) )
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
"rest"
)
const inheritedProperty = {
auth,
headers,
}
updateInheritedPropertiesForAffectedRequests(
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
inheritedProperty,
"rest"
)
draggingToRoot.value = false draggingToRoot.value = false
toast.success(`${t("collection.moved")}`) toast.success(`${t("collection.moved")}`)
} else if (hasTeamWriteAccess.value) { } else if (hasTeamWriteAccess.value) {
@@ -1562,6 +1654,22 @@ const dropCollection = (payload: {
collectionMoveLoading.value.indexOf(collectionIndexDragged), collectionMoveLoading.value.indexOf(collectionIndexDragged),
1 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 * Triggered by the export button in the tippy menu
* @param collection - Collection or folder to be exported * @param collection - Collection or folder to be exported
*/ */
const exportData = async ( const exportData = async (collection: HoppCollection | TeamCollection) => {
collection: HoppCollection<HoppRESTRequest> | TeamCollection
) => {
if (collectionsType.value.type === "my-collections") { if (collectionsType.value.type === "my-collections") {
const collectionJSON = JSON.stringify(collection) const collectionJSON = JSON.stringify(collection)
const name = (collection as HoppCollection<HoppRESTRequest>).name const name = (collection as HoppCollection).name
initializeDownloadCollection(collectionJSON, name) initializeDownloadCollection(collectionJSON, name)
} else { } 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<string>) => {
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) => { const resolveConfirmModal = (title: string | null) => {
if (title === `${t("confirm.remove_collection")}`) onRemoveCollection() if (title === `${t("confirm.remove_collection")}`) onRemoveCollection()
else if (title === `${t("confirm.remove_request")}`) onRemoveRequest() else if (title === `${t("confirm.remove_request")}`) onRemoveRequest()

View File

@@ -37,6 +37,18 @@
} }
" "
/> />
<HoppSmartItem
v-if="!isRootCollection"
label="Inherit"
:icon="authName === 'Inherit' ? IconCircleDot : IconCircle"
:active="authName === 'Inherit'"
@click="
() => {
auth.authType = 'inherit'
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
label="Basic Auth" label="Basic Auth"
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle" :icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
@@ -149,6 +161,17 @@
/> />
</div> </div>
</div> </div>
<div v-if="auth.authType === 'inherit'" class="p-4">
<span v-if="inheritedProperties?.auth">
Inherited
{{ getAuthName(inheritedProperties.auth.inheritedAuth.authType) }}
from Parent Collection {{ inheritedProperties?.auth.parentName }}
</span>
<span v-else>
Please save this request in any collection to inherit the
authorization
</span>
</div>
<div v-if="auth.authType === 'bearer'"> <div v-if="auth.authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
@@ -203,6 +226,8 @@ import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { onMounted } from "vue"
const t = useI18n() const t = useI18n()
@@ -210,12 +235,24 @@ const colorMode = useColorMode()
const props = defineProps<{ const props = defineProps<{
modelValue: HoppGQLAuth modelValue: HoppGQLAuth
isCollectionProperty?: boolean
isRootCollection?: boolean
inheritedProperties?: HoppInheritedProperty
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: HoppGQLAuth): void (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 = useVModel(props, "modelValue", emit)
const AUTH_KEY_NAME = { const AUTH_KEY_NAME = {
@@ -224,12 +261,20 @@ const AUTH_KEY_NAME = {
"oauth-2": "OAuth 2.0", "oauth-2": "OAuth 2.0",
"api-key": "API key", "api-key": "API key",
none: "None", none: "None",
inherit: "Inherit",
} as const } as const
const authType = pluckRef(auth, "authType") const authType = pluckRef(auth, "authType")
const authName = computed(() => const authName = computed(() =>
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None" 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 authActive = pluckRef(auth, "authActive")
const clearContent = () => { const clearContent = () => {

View File

@@ -77,22 +77,11 @@
tabindex="-1" tabindex="-1"
/> />
</span> </span>
<HoppSmartAutoComplete <SmartEnvInput
v-model="header.key"
:placeholder="`${t('count.header', { count: index + 1 })}`" :placeholder="`${t('count.header', { count: index + 1 })}`"
:source="commonHeaders" :auto-complete-source="commonHeaders"
:spellcheck="false" @change="
:value="header.key"
autofocus
styles="
bg-transparent
flex
flex-1
py-1
px-4
truncate
"
class="!flex flex-1"
@input="
updateHeader(index, { updateHeader(index, {
id: header.id, id: header.id,
key: $event, key: $event,
@@ -101,17 +90,14 @@
}) })
" "
/> />
<input <SmartEnvInput
class="flex flex-1 bg-transparent px-4 py-2" v-model="header.value"
:placeholder="`${t('count.value', { count: index + 1 })}`" :placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"
autofocus
@change=" @change="
updateHeader(index, { updateHeader(index, {
id: header.id, id: header.id,
key: header.key, key: header.key,
value: ($event!.target! as HTMLInputElement).value, value: $event,
active: header.active, active: header.active,
}) })
" "
@@ -156,6 +142,119 @@
</div> </div>
</template> </template>
</draggable> </draggable>
<draggable
v-model="computedHeaders"
item-key="id"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<span>
<HoppButtonSecondary
:icon="IconLock"
class="cursor-auto bg-divider text-secondaryLight opacity-25"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.header.key"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<SmartEnvInput
:model-value="mask(header)"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<span>
<HoppButtonSecondary
v-if="header.source === 'auth'"
v-tippy="{ theme: 'tooltip' }"
:title="t(masking ? 'state.show' : 'state.hide')"
:icon="masking ? IconEye : IconEyeOff"
@click="toggleMask()"
/>
<HoppButtonSecondary
v-else
v-tippy="{ theme: 'tooltip' }"
:icon="IconArrowUpRight"
:title="t('request.go_to_authorization_tab')"
class="cursor-auto text-primary hover:text-primary"
/>
</span>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconArrowUpRight"
:title="t('request.go_to_authorization_tab')"
/>
</span>
</div>
</template>
</draggable>
<draggable
v-model="inheritedProperties"
item-key="id"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<span>
<HoppButtonSecondary
:icon="IconLock"
class="cursor-auto bg-divider text-secondaryLight opacity-25"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.header.key"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<SmartEnvInput
:model-value="
header.source === 'auth' ? mask(header) : header.header.value
"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<HoppButtonSecondary
v-if="header.source === 'auth'"
v-tippy="{ theme: 'tooltip' }"
:title="t(masking ? 'state.show' : 'state.hide')"
:icon="masking && header.source === 'auth' ? IconEye : IconEyeOff"
@click="toggleMask()"
/>
<span v-else class="aspect-square w-[2.05rem]"></span>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconInfo"
:title="`This header is inherited from Parent Collection ${
header.inheritedFrom ?? ''
}`"
/>
</span>
</div>
</template>
</draggable>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="workingHeaders.length === 0" v-if="workingHeaders.length === 0"
:src="`/images/states/${colorMode.value}/add_category.svg`" :src="`/images/states/${colorMode.value}/add_category.svg`"
@@ -184,7 +283,12 @@ import IconCheckCircle from "~icons/lucide/check-circle"
import IconTrash from "~icons/lucide/trash" import IconTrash from "~icons/lucide/trash"
import IconCircle from "~icons/lucide/circle" import IconCircle from "~icons/lucide/circle"
import IconWrapText from "~icons/lucide/wrap-text" import IconWrapText from "~icons/lucide/wrap-text"
import { reactive, ref, watch } from "vue" import IconArrowUpRight from "~icons/lucide/arrow-up-right"
import IconLock from "~icons/lucide/lock"
import IconEye from "~icons/lucide/eye"
import IconEyeOff from "~icons/lucide/eye-off"
import IconInfo from "~icons/lucide/info"
import { computed, reactive, ref, watch } from "vue"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array" import * as A from "fp-ts/Array"
@@ -206,13 +310,20 @@ import { commonHeaders } from "~/helpers/headers"
import { useCodemirror } from "@composables/codemirror" import { useCodemirror } from "@composables/codemirror"
import { objRemoveKey } from "~/helpers/functional/object" import { objRemoveKey } from "~/helpers/functional/object"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { HoppGQLHeader } from "~/helpers/graphql"
import { throwError } from "~/helpers/functional/error"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const colorMode = useColorMode() const colorMode = useColorMode()
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
// v-model integration with props and emit // v-model integration with props and emit
const props = defineProps<{ modelValue: HoppGQLRequest }>() const props = defineProps<{
modelValue: HoppGQLRequest
isCollectionProperty?: boolean
inheritedProperties?: HoppInheritedProperty
}>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: HoppGQLRequest): void (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 = () => { const clearContent = () => {
@@ -429,4 +544,151 @@ const clearContent = () => {
bulkHeaders.value = "" 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")
// }
</script> </script>

View File

@@ -34,10 +34,16 @@
:label="`${t('tab.headers')}`" :label="`${t('tab.headers')}`"
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`" :info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
> >
<GraphqlHeaders v-model="request" /> <GraphqlHeaders
v-model="request"
:inherited-properties="inheritedProperties"
/>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`"> <HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
<GraphqlAuthorization v-model="request.auth" /> <GraphqlAuthorization
v-model="request.auth"
:inherited-properties="inheritedProperties"
/>
</HoppSmartTab> </HoppSmartTab>
</HoppSmartTabs> </HoppSmartTabs>
<CollectionsSaveRequest <CollectionsSaveRequest
@@ -69,6 +75,7 @@ import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service" import { InterceptorService } from "~/services/interceptor.service"
import { editGraphqlRequest } from "~/newstore/collections" import { editGraphqlRequest } from "~/newstore/collections"
import { GQLTabService } from "~/services/tab/graphql" import { GQLTabService } from "~/services/tab/graphql"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const VALID_GQL_OPERATIONS = [ const VALID_GQL_OPERATIONS = [
"query", "query",
@@ -93,24 +100,22 @@ const props = withDefaults(
response?: GQLResponseEvent[] | null response?: GQLResponseEvent[] | null
optionTab?: GQLOptionTabs optionTab?: GQLOptionTabs
tabId: string tabId: string
inheritedProperties?: HoppInheritedProperty
}>(), }>(),
{ {
response: null, response: null,
optionTab: "query", 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 selectedOptionTab = useVModel(props, "optionTab", emit)
const request = ref(props.modelValue) const request = useVModel(props, "modelValue", emit)
watch(
() => request.value,
(newVal) => {
emit("update:modelValue", newVal)
},
{ deep: true }
)
const url = computedWithControl( const url = computedWithControl(
() => tabs.currentActiveTab.value, () => tabs.currentActiveTab.value,
@@ -131,10 +136,30 @@ const runQuery = async (
startPageProgress() startPageProgress()
try { try {
const runURL = clone(url.value) const runURL = clone(url.value)
const runHeaders = clone(request.value.headers)
const runQuery = clone(request.value.query) const runQuery = clone(request.value.query)
const runVariables = clone(request.value.variables) 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({ await runGQLOperation({
name: request.value.name, name: request.value.name,
@@ -142,7 +167,7 @@ const runQuery = async (
headers: runHeaders, headers: runHeaders,
query: runQuery, query: runQuery,
variables: runVariables, variables: runVariables,
auth: runAuth, auth: runAuth ?? { authType: "none", authActive: false },
operationName: definition?.name?.value, operationName: definition?.name?.value,
operationType: definition?.operation ?? "query", operationType: definition?.operation ?? "query",
}) })

View File

@@ -5,6 +5,7 @@
v-model="tab.document.request" v-model="tab.document.request"
v-model:response="tab.document.response" v-model:response="tab.document.response"
v-model:option-tab="tab.document.optionTabPreference" v-model:option-tab="tab.document.optionTabPreference"
v-model:inherited-properties="tab.document.inheritedProperties"
:tab-id="tab.id" :tab-id="tab.id"
/> />
</template> </template>

View File

@@ -1,7 +1,12 @@
<template> <template>
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<div <div
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold" class="sticky z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
:class="{
'top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold':
!isCollectionProperty,
'top-propertiesPrimaryStickyFold': isCollectionProperty,
}"
> >
<span class="flex items-center"> <span class="flex items-center">
<label class="truncate font-semibold text-secondaryLight"> <label class="truncate font-semibold text-secondaryLight">
@@ -37,6 +42,18 @@
} }
" "
/> />
<HoppSmartItem
v-if="!isRootCollection"
label="Inherit"
:icon="authName === 'Inherit' ? IconCircleDot : IconCircle"
:active="authName === 'Inherit'"
@click="
() => {
auth.authType = 'inherit'
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
label="Basic Auth" label="Basic Auth"
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle" :icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
@@ -135,6 +152,17 @@
<div v-if="auth.authType === 'basic'"> <div v-if="auth.authType === 'basic'">
<HttpAuthorizationBasic v-model="auth" /> <HttpAuthorizationBasic v-model="auth" />
</div> </div>
<div v-if="auth.authType === 'inherit'" class="p-4">
<span v-if="inheritedProperties?.auth">
Inherited
{{ getAuthName(inheritedProperties.auth.inheritedAuth.authType) }}
from Parent Collection {{ inheritedProperties?.auth.parentName }}
</span>
<span v-else>
Please save this request in any collection to inherit the
authorization
</span>
</div>
<div v-if="auth.authType === 'bearer'"> <div v-if="auth.authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.token" placeholder="Token" /> <SmartEnvInput v-model="auth.token" placeholder="Token" />
@@ -181,6 +209,8 @@ import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { onMounted } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n() const t = useI18n()
@@ -188,6 +218,9 @@ const colorMode = useColorMode()
const props = defineProps<{ const props = defineProps<{
modelValue: HoppRESTAuth modelValue: HoppRESTAuth
isCollectionProperty?: boolean
isRootCollection?: boolean
inheritedProperties?: HoppInheritedProperty
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -196,18 +229,34 @@ const emit = defineEmits<{
const auth = useVModel(props, "modelValue", emit) const auth = useVModel(props, "modelValue", emit)
onMounted(() => {
if (props.isRootCollection && auth.value.authType === "inherit") {
auth.value = {
authType: "none",
authActive: true,
}
}
})
const AUTH_KEY_NAME = { const AUTH_KEY_NAME = {
basic: "Basic Auth", basic: "Basic Auth",
bearer: "Bearer", bearer: "Bearer",
"oauth-2": "OAuth 2.0", "oauth-2": "OAuth 2.0",
"api-key": "API key", "api-key": "API key",
none: "None", none: "None",
inherit: "Inherit",
} as const } as const
const authType = pluckRef(auth, "authType") const authType = pluckRef(auth, "authType")
const authName = computed(() => const authName = computed(() =>
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None" AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
) )
const getAuthName = (type: HoppRESTAuth["authType"] | undefined) => {
if (!type) return "None"
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
}
const authActive = pluckRef(auth, "authActive") const authActive = pluckRef(auth, "authActive")
const clearContent = () => { const clearContent = () => {

View File

@@ -1,7 +1,12 @@
<template> <template>
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<div <div
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold" class="sticky z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
:class="{
'top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold':
!isCollectionProperty,
'top-propertiesPrimaryStickyFold': isCollectionProperty,
}"
> >
<label class="truncate font-semibold text-secondaryLight"> <label class="truncate font-semibold text-secondaryLight">
{{ t("request.header_list") }} {{ t("request.header_list") }}
@@ -203,6 +208,61 @@
</div> </div>
</template> </template>
</draggable> </draggable>
<draggable
v-model="inheritedProperties"
item-key="id"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<span>
<HoppButtonSecondary
:icon="IconLock"
class="cursor-auto bg-divider text-secondaryLight opacity-25"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.header.key"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<SmartEnvInput
:model-value="
header.source === 'auth' ? mask(header) : header.header.value
"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<HoppButtonSecondary
v-if="header.source === 'auth'"
v-tippy="{ theme: 'tooltip' }"
:title="t(masking ? 'state.show' : 'state.hide')"
:icon="masking && header.source === 'auth' ? IconEye : IconEyeOff"
@click="toggleMask()"
/>
<span v-else class="aspect-square w-[2.05rem]"></span>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconInfo"
:title="`This header is inherited from Parent Collection ${
header.inheritedFrom ?? ''
}`"
/>
</span>
</div>
</template>
</draggable>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="workingHeaders.length === 0" v-if="workingHeaders.length === 0"
:src="`/images/states/${colorMode.value}/add_category.svg`" :src="`/images/states/${colorMode.value}/add_category.svg`"
@@ -236,6 +296,7 @@ import IconEye from "~icons/lucide/eye"
import IconEyeOff from "~icons/lucide/eye-off" import IconEyeOff from "~icons/lucide/eye-off"
import IconArrowUpRight from "~icons/lucide/arrow-up-right" import IconArrowUpRight from "~icons/lucide/arrow-up-right"
import IconWrapText from "~icons/lucide/wrap-text" import IconWrapText from "~icons/lucide/wrap-text"
import IconInfo from "~icons/lucide/info"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { computed, reactive, ref, watch } from "vue" import { computed, reactive, ref, watch } from "vue"
import { isEqual, cloneDeep } from "lodash-es" import { isEqual, cloneDeep } from "lodash-es"
@@ -264,12 +325,14 @@ import { objRemoveKey } from "~/helpers/functional/object"
import { import {
ComputedHeader, ComputedHeader,
getComputedHeaders, getComputedHeaders,
getComputedAuthHeaders,
} from "~/helpers/utils/EffectiveURL" } from "~/helpers/utils/EffectiveURL"
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments" import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection" import { InspectionService, InspectorResult } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -288,7 +351,11 @@ const linewrapEnabled = ref(true)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null) const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
// v-model integration with props and emit // v-model integration with props and emit
const props = defineProps<{ modelValue: HoppRESTRequest }>() const props = defineProps<{
modelValue: HoppRESTRequest
isCollectionProperty?: boolean
inheritedProperties?: HoppInheritedProperty
}>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "change-tab", value: RESTOptionTabs): void (e: "change-tab", value: RESTOptionTabs): void
@@ -494,6 +561,72 @@ const computedHeaders = computed(() =>
) )
) )
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(
aggregateEnvs.value,
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 masking = ref(true)
const toggleMask = () => { const toggleMask = () => {

View File

@@ -29,14 +29,21 @@
:label="`${t('tab.headers')}`" :label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`" :info="`${newActiveHeadersCount$}`"
> >
<HttpHeaders v-model="request" @change-tab="changeOptionTab" /> <HttpHeaders
v-model="request"
:inherited-properties="inheritedProperties"
@change-tab="changeOptionTab"
/>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
v-if="properties ? properties.includes('authorization') : true" v-if="properties ? properties.includes('authorization') : true"
:id="'authorization'" :id="'authorization'"
:label="`${t('tab.authorization')}`" :label="`${t('tab.authorization')}`"
> >
<HttpAuthorization v-model="request.auth" /> <HttpAuthorization
v-model="request.auth"
:inherited-properties="inheritedProperties"
/>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
v-if="properties ? properties.includes('preRequestScript') : true" v-if="properties ? properties.includes('preRequestScript') : true"
@@ -69,6 +76,7 @@ import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { computed } from "vue" import { computed } from "vue"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const VALID_OPTION_TABS = [ const VALID_OPTION_TABS = [
"params", "params",
@@ -89,6 +97,7 @@ const props = withDefaults(
modelValue: HoppRESTRequest modelValue: HoppRESTRequest
optionTab: RESTOptionTabs optionTab: RESTOptionTabs
properties?: string[] properties?: string[]
inheritedProperties?: HoppInheritedProperty
}>(), }>(),
{ {
optionTab: "params", optionTab: "params",

View File

@@ -5,6 +5,7 @@
<HttpRequestOptions <HttpRequestOptions
v-model="tab.document.request" v-model="tab.document.request"
v-model:option-tab="tab.document.optionTabPreference" v-model:option-tab="tab.document.optionTabPreference"
v-model:inherited-properties="tab.document.inheritedProperties"
/> />
</template> </template>
<template #secondary> <template #secondary>

View File

@@ -31,7 +31,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useReadonlyStream } from "~/composables/stream" import { useReadonlyStream } from "~/composables/stream"
@@ -48,7 +48,7 @@ const hasSelectedCollectionID = computed(() => {
const myCollections = useReadonlyStream(restCollections$, []) const myCollections = useReadonlyStream(restCollections$, [])
const emit = defineEmits<{ const emit = defineEmits<{
(e: "importFromMyCollection", content: HoppCollection<HoppRESTRequest>): void (e: "importFromMyCollection", content: HoppCollection): void
}>() }>()
const fetchCollectionFromMyCollections = async () => { const fetchCollectionFromMyCollections = async () => {

View File

@@ -95,13 +95,41 @@ export function runRESTRequest$(
return E.left("script_fail" as const) return E.left("script_fail" as const)
} }
const effectiveRequest = getEffectiveRESTRequest( const requestAuth =
tab.value.document.request, tab.value.document.request.auth.authType === "inherit" &&
{ tab.value.document.request.auth.authActive
name: "Env", ? tab.value.document.inheritedProperties?.auth.inheritedAuth
variables: combineEnvVariables(envs.right), : tab.value.document.request.auth
}
) let requestHeaders
const inheritedHeaders =
tab.value.document.inheritedProperties?.headers.map((header) => {
if (header.inheritedHeader) {
return header.inheritedHeader
}
return []
})
if (inheritedHeaders) {
requestHeaders = [
...inheritedHeaders,
...tab.value.document.request.headers,
]
} else {
requestHeaders = [...tab.value.document.request.headers]
}
const finalRequest = {
...tab.value.document.request,
auth: requestAuth ?? { authType: "none", authActive: false },
headers: requestHeaders,
}
const effectiveRequest = getEffectiveRESTRequest(finalRequest, {
name: "Env",
variables: combineEnvVariables(envs.right),
})
const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest) const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)
cancelFunc = cancelRun cancelFunc = cancelRun

View File

@@ -0,0 +1,15 @@
mutation UpdateTeamCollection(
$collectionID: ID!
$newTitle: String
$data: String
) {
updateTeamCollection(
collectionID: $collectionID
newTitle: $newTitle
data: $data
) {
id
title
data
}
}

View File

@@ -3,6 +3,7 @@ query GetCollectionChildren($collectionID: ID!, $cursor: ID) {
children(cursor: $cursor) { children(cursor: $cursor) {
id id
title title
data
} }
} }
} }

View File

@@ -1,5 +0,0 @@
query GetCollectionTitle($collectionID: ID!) {
collection(collectionID: $collectionID) {
title
}
}

View File

@@ -0,0 +1,6 @@
query GetCollectionTitleAndData($collectionID: ID!) {
collection(collectionID: $collectionID) {
title
data
}
}

View File

@@ -2,6 +2,7 @@ query GetSingleCollection($collectionID: ID!) {
collection(collectionID: $collectionID) { collection(collectionID: $collectionID) {
id id
title title
data
parent { parent {
id id
} }

View File

@@ -2,5 +2,6 @@ query RootCollectionsOfTeam($teamID: ID!, $cursor: ID) {
rootCollectionsOfTeam(teamID: $teamID, cursor: $cursor) { rootCollectionsOfTeam(teamID: $teamID, cursor: $cursor) {
id id
title title
data
} }
} }

View File

@@ -2,6 +2,7 @@ subscription TeamCollectionAdded($teamID: ID!) {
teamCollectionAdded(teamID: $teamID) { teamCollectionAdded(teamID: $teamID) {
id id
title title
data
parent { parent {
id id
} }

View File

@@ -2,6 +2,7 @@ subscription TeamCollectionUpdated($teamID: ID!) {
teamCollectionUpdated(teamID: $teamID) { teamCollectionUpdated(teamID: $teamID) {
id id
title title
data
parent { parent {
id id
} }

View File

@@ -4,7 +4,6 @@ import * as TE from "fp-ts/TaskEither"
import { pipe, flow } from "fp-ts/function" import { pipe, flow } from "fp-ts/function"
import { import {
HoppCollection, HoppCollection,
HoppRESTRequest,
makeCollection, makeCollection,
translateToNewRequest, translateToNewRequest,
} from "@hoppscotch/data" } from "@hoppscotch/data"
@@ -15,7 +14,7 @@ import {
ExportAsJsonDocument, ExportAsJsonDocument,
GetCollectionChildrenIDsDocument, GetCollectionChildrenIDsDocument,
GetCollectionRequestsDocument, GetCollectionRequestsDocument,
GetCollectionTitleDocument, GetCollectionTitleAndDataDocument,
} from "./graphql" } from "./graphql"
export const BACKEND_PAGE_SIZE = 10 export const BACKEND_PAGE_SIZE = 10
@@ -85,16 +84,19 @@ export const getCompleteCollectionTree = (
pipe( pipe(
TE.Do, TE.Do,
TE.bind("title", () => TE.bind("titleAndData", () =>
pipe( pipe(
() => () =>
runGQLQuery({ runGQLQuery({
query: GetCollectionTitleDocument, query: GetCollectionTitleAndDataDocument,
variables: { variables: {
collectionID: collID, collectionID: collID,
}, },
}), }),
TE.map((x) => x.collection!.title) TE.map((result) => ({
title: result.collection!.title,
data: result.collection!.data,
}))
) )
), ),
TE.bind("children", () => TE.bind("children", () =>
@@ -108,24 +110,36 @@ export const getCompleteCollectionTree = (
TE.bind("requests", () => () => getCollectionRequests(collID)), TE.bind("requests", () => () => getCollectionRequests(collID)),
TE.map( TE.map(
({ title, children, requests }) => ({ titleAndData, children, requests }) =>
<TeamCollection>{ <TeamCollection>{
id: collID, id: collID,
children, children,
requests, requests,
title, title: titleAndData.title,
data: titleAndData.data,
} }
) )
) )
export const teamCollToHoppRESTColl = ( export const teamCollToHoppRESTColl = (
coll: TeamCollection coll: TeamCollection
): HoppCollection<HoppRESTRequest> => ): HoppCollection => {
makeCollection({ const data =
coll.data && coll.data !== "null"
? JSON.parse(coll.data)
: {
auth: { authType: "inherit", authActive: true },
headers: [],
}
return makeCollection({
name: coll.title, name: coll.title,
folders: coll.children?.map(teamCollToHoppRESTColl) ?? [], folders: coll.children?.map(teamCollToHoppRESTColl) ?? [],
requests: coll.requests?.map((x) => x.request) ?? [], requests: coll.requests?.map((x) => x.request) ?? [],
auth: data.auth ?? { authType: "inherit", authActive: true },
headers: data.headers ?? [],
}) })
}
/** /**
* Get the JSON string of all the collection of the specified team * Get the JSON string of all the collection of the specified team

View File

@@ -21,6 +21,9 @@ import {
UpdateCollectionOrderDocument, UpdateCollectionOrderDocument,
UpdateCollectionOrderMutation, UpdateCollectionOrderMutation,
UpdateCollectionOrderMutationVariables, UpdateCollectionOrderMutationVariables,
UpdateTeamCollectionDocument,
UpdateTeamCollectionMutation,
UpdateTeamCollectionMutationVariables,
} from "../graphql" } from "../graphql"
type CreateNewRootCollectionError = "team_coll/short_title" type CreateNewRootCollectionError = "team_coll/short_title"
@@ -122,3 +125,18 @@ export const importJSONToTeam = (collectionJSON: string, teamID: string) =>
teamID, teamID,
} }
) )
export const updateTeamCollection = (
collectionID: string,
data?: string,
newTitle?: string
) =>
runMutation<
UpdateTeamCollectionMutation,
UpdateTeamCollectionMutationVariables,
""
>(UpdateTeamCollectionDocument, {
collectionID,
data,
newTitle,
})

View File

@@ -1,10 +1,12 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import { getAffectedIndexes } from "./affectedIndex" import { getAffectedIndexes } from "./affectedIndex"
import { GetSingleRequestDocument } from "../backend/graphql" import { GetSingleRequestDocument } from "../backend/graphql"
import { runGQLQuery } from "../backend/GQLClient" import { runGQLQuery } from "../backend/GQLClient"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { getService } from "~/modules/dioc" import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
import { GQLTabService } from "~/services/tab/graphql"
/** /**
* Resolve save context on reorder * Resolve save context on reorder
@@ -108,6 +110,135 @@ export function updateSaveContextForAffectedRequests(
} }
} }
/**
* Used to check the new folder path is close to the save context folder path or not
* @param folderPathCurrent The path saved as the inherited path in the inherited properties
* @param newFolderPath The incomming path
* @param saveContextPath The save context of the request
* @returns The path which is close to saveContext.folderPath
*/
function folderPathCloseToSaveContext(
folderPathCurrent: string | undefined,
newFolderPath: string,
saveContextPath: string
) {
if (!folderPathCurrent) return newFolderPath
const folderPathCurrentArray = folderPathCurrent.split("/")
const newFolderPathArray = newFolderPath.split("/")
const saveContextFolderPathArray = saveContextPath.split("/")
let folderPathCurrentMatch = 0
for (let i = 0; i < folderPathCurrentArray.length; i++) {
if (folderPathCurrentArray[i] === saveContextFolderPathArray[i]) {
folderPathCurrentMatch++
}
}
let newFolderPathMatch = 0
for (let i = 0; i < newFolderPathArray.length; i++) {
if (newFolderPathArray[i] === saveContextFolderPathArray[i]) {
newFolderPathMatch++
}
}
if (folderPathCurrentMatch > newFolderPathMatch) {
return folderPathCurrent
}
return newFolderPath
}
export function updateInheritedPropertiesForAffectedRequests(
path: string,
inheritedProperties: HoppInheritedProperty,
type: "rest" | "graphql",
workspace: "personal" | "team" = "personal"
) {
const tabService =
type === "rest" ? getService(RESTTabService) : getService(GQLTabService)
let tabs
if (workspace === "personal") {
tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath.startsWith(path)
)
})
} else {
tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "team-collection" &&
tab.document.saveContext.collectionID?.startsWith(path)
)
})
}
const tabsEffectedByAuth = tabs.filter((tab) => {
if (workspace === "personal") {
return (
tab.value.document.saveContext?.originLocation === "user-collection" &&
tab.value.document.saveContext.folderPath.startsWith(path) &&
path ===
folderPathCloseToSaveContext(
tab.value.document.inheritedProperties?.auth.parentID,
path,
tab.value.document.saveContext.folderPath
)
)
}
return (
tab.value.document.saveContext?.originLocation === "team-collection" &&
tab.value.document.saveContext.collectionID?.startsWith(path) &&
path ===
folderPathCloseToSaveContext(
tab.value.document.inheritedProperties?.auth.parentID,
path,
tab.value.document.saveContext.collectionID
)
)
})
const tabsEffectedByHeaders = tabs.filter((tab) => {
return (
tab.value.document.inheritedProperties &&
tab.value.document.inheritedProperties.headers.some(
(header) => header.parentID === path
)
)
})
for (const tab of tabsEffectedByAuth) {
tab.value.document.inheritedProperties = inheritedProperties
}
for (const tab of tabsEffectedByHeaders) {
const headers = tab.value.document.inheritedProperties?.headers.map(
(header) => {
if (header.parentID === path) {
return {
...header,
inheritedHeader: inheritedProperties.headers.find(
(inheritedHeader) =>
inheritedHeader.inheritedHeader?.key ===
header.inheritedHeader?.key
)?.inheritedHeader,
}
}
return header
}
)
tab.value.document.inheritedProperties = {
...tab.value.document.inheritedProperties,
headers,
}
}
}
function resetSaveContextForAffectedRequests(folderPath: string) { function resetSaveContextForAffectedRequests(folderPath: string) {
const tabService = getService(RESTTabService) const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => { const tabs = tabService.getTabsRefTo((tab) => {
@@ -152,9 +283,9 @@ export async function resetTeamRequestsContext() {
} }
export function getFoldersByPath( export function getFoldersByPath(
collections: HoppCollection<HoppRESTRequest>[], collections: HoppCollection[],
path: string path: string
): HoppCollection<HoppRESTRequest>[] { ): HoppCollection[] {
if (!path) return collections if (!path) return collections
// path will be like this "0/0/1" these are the indexes of the folders // path will be like this "0/0/1" these are the indexes of the folders

View File

@@ -1,4 +1,8 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" import {
HoppCollection,
HoppGQLRequest,
HoppRESTRequest,
} from "@hoppscotch/data"
import { getAffectedIndexes } from "./affectedIndex" import { getAffectedIndexes } from "./affectedIndex"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { getService } from "~/modules/dioc" import { getService } from "~/modules/dioc"
@@ -53,9 +57,9 @@ export function resolveSaveContextOnRequestReorder(payload: {
} }
export function getRequestsByPath( export function getRequestsByPath(
collections: HoppCollection<HoppRESTRequest>[], collections: HoppCollection[],
path: string path: string
): HoppRESTRequest[] { ): HoppRESTRequest[] | HoppGQLRequest[] {
// path will be like this "0/0/1" these are the indexes of the folders // path will be like this "0/0/1" these are the indexes of the folders
const pathArray = path.split("/").map((index) => parseInt(index)) const pathArray = path.split("/").map((index) => parseInt(index))

View File

@@ -27,7 +27,7 @@ export const getDefaultGQLRequest = (): HoppGQLRequest => ({
}`, }`,
query: DEFAULT_QUERY, query: DEFAULT_QUERY,
auth: { auth: {
authType: "none", authType: "inherit",
authActive: true, authActive: true,
}, },
}) })

View File

@@ -1,6 +1,7 @@
import { HoppGQLRequest } from "@hoppscotch/data" import { HoppGQLRequest } from "@hoppscotch/data"
import { GQLResponseEvent } from "./connection" import { GQLResponseEvent } from "./connection"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue" import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
export type HoppGQLSaveContext = export type HoppGQLSaveContext =
| { | {
@@ -73,4 +74,10 @@ export type HoppGQLDocument = {
* Options tab preference for the current tab's document * Options tab preference for the current tab's document
*/ */
optionTabPreference?: GQLOptionTabs optionTabPreference?: GQLOptionTabs
/**
* The inherited properties from the parent collection
* (if any)
*/
inheritedProperties?: HoppInheritedProperty
} }

View File

@@ -1,7 +1,5 @@
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
export const gqlCollectionsExporter = ( export const gqlCollectionsExporter = (gqlCollections: HoppCollection[]) => {
gqlCollections: HoppCollection<HoppGQLRequest>[]
) => {
return JSON.stringify(gqlCollections, null, 2) return JSON.stringify(gqlCollections, null, 2)
} }

View File

@@ -1,7 +1,5 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
export const myCollectionsExporter = ( export const myCollectionsExporter = (myCollections: HoppCollection[]) => {
myCollections: HoppCollection<HoppRESTRequest>[]
) => {
return JSON.stringify(myCollections, null, 2) return JSON.stringify(myCollections, null, 2)
} }

View File

@@ -2,15 +2,12 @@ import { pipe, flow } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray" import * as RA from "fp-ts/ReadonlyArray"
import { import { translateToNewRESTCollection, HoppCollection } from "@hoppscotch/data"
translateToNewRESTCollection,
HoppCollection,
HoppRESTRequest,
} from "@hoppscotch/data"
import { isPlainObject as _isPlainObject } from "lodash-es" import { isPlainObject as _isPlainObject } from "lodash-es"
import { IMPORTER_INVALID_FILE_FORMAT } from "." import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { safeParseJSON } from "~/helpers/functional/json" import { safeParseJSON } from "~/helpers/functional/json"
import { translateToNewGQLCollection } from "@hoppscotch/data"
export const hoppRESTImporter = (content: string) => export const hoppRESTImporter = (content: string) =>
pipe( pipe(
@@ -33,12 +30,10 @@ const isPlainObject = (value: any): value is object => _isPlainObject(value)
/** /**
* checks if a collection matches the schema for a hoppscotch collection. * checks if a collection matches the schema for a hoppscotch collection.
* as of now we are only checking if the collection has a "v" key in it. * here 2 is the latest version of the schema.
*/ */
const isValidCollection = ( const isValidCollection = (collection: unknown): collection is HoppCollection =>
collection: unknown isPlainObject(collection) && "v" in collection && collection.v === 2
): collection is HoppCollection<HoppRESTRequest> =>
isPlainObject(collection) && "v" in collection
/** /**
* checks if a collection is a valid hoppscotch collection. * checks if a collection is a valid hoppscotch collection.
@@ -56,3 +51,29 @@ const validateCollection = (collection: unknown) => {
*/ */
const makeCollectionsArray = (collections: unknown | unknown[]): unknown[] => const makeCollectionsArray = (collections: unknown | unknown[]): unknown[] =>
Array.isArray(collections) ? collections : [collections] Array.isArray(collections) ? collections : [collections]
export const hoppGQLImporter = (content: string) =>
pipe(
safeParseJSON(content),
O.chain(
flow(
makeCollectionsArray,
RA.map(validateGQLCollection),
O.sequenceArray,
O.map(RA.toArray)
)
),
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
)
/**
*
* @param collection the collection to validate
* @returns the collection if it is valid, else a translated version of the collection
*/
export const validateGQLCollection = (collection: unknown) => {
if (isValidCollection(collection)) {
return O.some(collection)
}
return O.some(translateToNewGQLCollection(collection))
}

View File

@@ -1,12 +1,12 @@
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
// TODO: add zod validation // TODO: add zod validation
export const hoppGqlCollectionsImporter = ( export const hoppGqlCollectionsImporter = (
content: string content: string
): E.Either<"INVALID_JSON", HoppCollection<HoppGQLRequest>[]> => { ): E.Either<"INVALID_JSON", HoppCollection[]> => {
return E.tryCatch( return E.tryCatch(
() => JSON.parse(content) as HoppCollection<HoppGQLRequest>[], () => JSON.parse(content) as HoppCollection[],
() => "INVALID_JSON" () => "INVALID_JSON"
) )
} }

View File

@@ -192,7 +192,7 @@ const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
const getHoppFolder = ( const getHoppFolder = (
folderRes: InsomniaFolderResource, folderRes: InsomniaFolderResource,
resources: InsomniaResource[] resources: InsomniaResource[]
): HoppCollection<HoppRESTRequest> => ): HoppCollection =>
makeCollection({ makeCollection({
name: folderRes.name ?? "", name: folderRes.name ?? "",
folders: getFoldersIn(folderRes, resources).map((f) => folders: getFoldersIn(folderRes, resources).map((f) =>

View File

@@ -12,7 +12,6 @@ import {
HoppRESTHeader, HoppRESTHeader,
HoppRESTParam, HoppRESTParam,
HoppRESTReqBody, HoppRESTReqBody,
HoppRESTRequest,
knownContentTypes, knownContentTypes,
makeRESTRequest, makeRESTRequest,
HoppCollection, HoppCollection,
@@ -581,7 +580,7 @@ const convertPathToHoppReqs = (
const convertOpenApiDocToHopp = ( const convertOpenApiDocToHopp = (
doc: OpenAPI.Document doc: OpenAPI.Document
): TE.TaskEither<never, HoppCollection<HoppRESTRequest>[]> => { ): TE.TaskEither<never, HoppCollection[]> => {
const name = doc.info.title const name = doc.info.title
const paths = Object.entries(doc.paths ?? {}) const paths = Object.entries(doc.paths ?? {})
@@ -589,7 +588,7 @@ const convertOpenApiDocToHopp = (
.flat() .flat()
return TE.of([ return TE.of([
makeCollection<HoppRESTRequest>({ makeCollection({
name, name,
folders: [], folders: [],
requests: paths, requests: paths,

View File

@@ -283,7 +283,7 @@ const getHoppRequest = (item: Item): HoppRESTRequest => {
}) })
} }
const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection<HoppRESTRequest> => const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection =>
makeCollection({ makeCollection({
name: ig.name, name: ig.name,
folders: pipe( folders: pipe(

View File

@@ -2,6 +2,7 @@ import { HoppRESTRequest } from "@hoppscotch/data"
import { HoppRESTResponse } from "../types/HoppRESTResponse" import { HoppRESTResponse } from "../types/HoppRESTResponse"
import { HoppTestResult } from "../types/HoppTestResult" import { HoppTestResult } from "../types/HoppTestResult"
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue" import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
export type HoppRESTSaveContext = export type HoppRESTSaveContext =
| { | {
@@ -80,4 +81,10 @@ export type HoppRESTDocument = {
* Options tab preference for the current tab's document * Options tab preference for the current tab's document
*/ */
optionTabPreference?: RESTOptionTabs optionTabPreference?: RESTOptionTabs
/**
* The inherited properties from the parent collection
* (if any)
*/
inheritedProperties?: HoppInheritedProperty
} }

View File

@@ -8,4 +8,5 @@ export interface TeamCollection {
title: string title: string
children: TeamCollection[] | null children: TeamCollection[] | null
requests: TeamRequest[] | null requests: TeamRequest[] | null
data: string | null
} }

View File

@@ -1,6 +1,10 @@
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { BehaviorSubject, Subscription } from "rxjs" import { BehaviorSubject, Subscription } from "rxjs"
import { translateToNewRequest } from "@hoppscotch/data" import {
HoppRESTAuth,
HoppRESTHeader,
translateToNewRequest,
} from "@hoppscotch/data"
import { pull, remove } from "lodash-es" import { pull, remove } from "lodash-es"
import { Subscription as WSubscription } from "wonka" import { Subscription as WSubscription } from "wonka"
import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient" import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
@@ -21,6 +25,7 @@ import {
TeamRequestOrderUpdatedDocument, TeamRequestOrderUpdatedDocument,
TeamCollectionOrderUpdatedDocument, TeamCollectionOrderUpdatedDocument,
} from "~/helpers/backend/graphql" } from "~/helpers/backend/graphql"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
const TEAMS_BACKEND_PAGE_SIZE = 10 const TEAMS_BACKEND_PAGE_SIZE = 10
@@ -542,6 +547,7 @@ export default class NewTeamCollectionAdapter {
children: null, children: null,
requests: null, requests: null,
title: title, title: title,
data: null,
}, },
parentID ?? null parentID ?? null
) )
@@ -693,6 +699,7 @@ export default class NewTeamCollectionAdapter {
children: null, children: null,
requests: null, requests: null,
title: result.right.teamCollectionAdded.title, title: result.right.teamCollectionAdded.title,
data: result.right.teamCollectionAdded.data ?? null,
}, },
result.right.teamCollectionAdded.parent?.id ?? null result.right.teamCollectionAdded.parent?.id ?? null
) )
@@ -715,6 +722,7 @@ export default class NewTeamCollectionAdapter {
this.updateCollection({ this.updateCollection({
id: result.right.teamCollectionUpdated.id, id: result.right.teamCollectionUpdated.id,
title: result.right.teamCollectionUpdated.title, title: result.right.teamCollectionUpdated.title,
data: result.right.teamCollectionUpdated.data,
}) })
}) })
@@ -931,6 +939,7 @@ export default class NewTeamCollectionAdapter {
<TeamCollection>{ <TeamCollection>{
id: el.id, id: el.id,
title: el.title, title: el.title,
data: el.data,
children: null, children: null,
requests: null, requests: null,
} }
@@ -1024,4 +1033,104 @@ export default class NewTeamCollectionAdapter {
) )
} }
} }
public cascadeParentCollectionForHeaderAuth(folderPath: string) {
let auth: HoppInheritedProperty["auth"] = {
parentID: folderPath ?? "",
parentName: "",
inheritedAuth: {
authType: "none",
authActive: true,
},
}
const headers: HoppInheritedProperty["headers"] = []
if (!folderPath) return { auth, headers }
const path = folderPath.split("/")
// Check if the path is empty or invalid
if (!path || path.length === 0) {
console.error("Invalid path:", folderPath)
return { auth, headers }
}
// Loop through the path and get the last parent folder with authType other than 'inherit'
for (let i = 0; i < path.length; i++) {
const parentFolder = findCollInTree(this.collections$.value, path[i])
// Check if parentFolder is undefined or null
if (!parentFolder) {
console.error("Parent folder not found for path:", path)
return { auth, headers }
}
const data: {
auth: HoppRESTAuth
headers: HoppRESTHeader[]
} = parentFolder.data
? JSON.parse(parentFolder.data)
: {
auth: null,
headers: null,
}
if (!data.auth) {
data.auth = {
authType: "inherit",
authActive: true,
}
auth.parentID = [...path.slice(0, i + 1)].join("/")
auth.parentName = parentFolder.title
}
if (!data.headers) data.headers = []
const parentFolderAuth = data.auth
const parentFolderHeaders = data.headers
if (parentFolderAuth?.authType === "inherit" && path.length === 1) {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentName: parentFolder.title,
inheritedAuth: auth.inheritedAuth,
}
}
if (parentFolderAuth?.authType !== "inherit") {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentName: parentFolder.title,
inheritedAuth: parentFolderAuth,
}
}
// Update headers, overwriting duplicates by key
if (parentFolderHeaders) {
const activeHeaders = parentFolderHeaders.filter((h) => h.active)
activeHeaders.forEach((header) => {
const index = headers.findIndex(
(h) => h.inheritedHeader?.key === header.key
)
const currentPath = [...path.slice(0, i + 1)].join("/")
if (index !== -1) {
// Replace the existing header with the same key
headers[index] = {
parentID: currentPath,
parentName: parentFolder.title,
inheritedHeader: header,
}
} else {
headers.push({
parentID: currentPath,
parentName: parentFolder.title,
inheritedHeader: header,
})
}
})
}
}
return { auth, headers }
}
} }

View File

@@ -0,0 +1,19 @@
import {
GQLHeader,
HoppGQLAuth,
HoppRESTHeader,
HoppRESTAuth,
} from "@hoppscotch/data"
export type HoppInheritedProperty = {
auth: {
parentID: string
parentName: string
inheritedAuth: HoppRESTAuth | HoppGQLAuth
}
headers: {
parentID: string
parentName: string
inheritedHeader: HoppRESTHeader | GQLHeader
}[]
}

View File

@@ -42,22 +42,26 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
* @param envVars Currently active environment variables * @param envVars Currently active environment variables
* @returns The list of headers * @returns The list of headers
*/ */
const getComputedAuthHeaders = ( export const getComputedAuthHeaders = (
req: HoppRESTRequest, envVars: Environment["variables"],
envVars: Environment["variables"] req?: HoppRESTRequest,
auth?: HoppRESTRequest["auth"]
) => { ) => {
const request = auth ? { auth: auth ?? { authActive: false } } : req
// If Authorization header is also being user-defined, that takes priority // If Authorization header is also being user-defined, that takes priority
if (req.headers.find((h) => h.key.toLowerCase() === "authorization")) if (req && req.headers.find((h) => h.key.toLowerCase() === "authorization"))
return [] return []
if (!req.auth.authActive) return [] if (!request) return []
if (!request.auth || !request.auth.authActive) return []
const headers: HoppRESTHeader[] = [] const headers: HoppRESTHeader[] = []
// TODO: Support a better b64 implementation than btoa ? // TODO: Support a better b64 implementation than btoa ?
if (req.auth.authType === "basic") { if (request.auth.authType === "basic") {
const username = parseTemplateString(req.auth.username, envVars) const username = parseTemplateString(request.auth.username, envVars)
const password = parseTemplateString(req.auth.password, envVars) const password = parseTemplateString(request.auth.password, envVars)
headers.push({ headers.push({
active: true, active: true,
@@ -65,22 +69,21 @@ const getComputedAuthHeaders = (
value: `Basic ${btoa(`${username}:${password}`)}`, value: `Basic ${btoa(`${username}:${password}`)}`,
}) })
} else if ( } else if (
req.auth.authType === "bearer" || request.auth.authType === "bearer" ||
req.auth.authType === "oauth-2" request.auth.authType === "oauth-2"
) { ) {
headers.push({ headers.push({
active: true, active: true,
key: "Authorization", key: "Authorization",
value: `Bearer ${parseTemplateString(req.auth.token, envVars)}`, value: `Bearer ${parseTemplateString(request.auth.token, envVars)}`,
}) })
} else if (req.auth.authType === "api-key") { } else if (request.auth.authType === "api-key") {
const { key, value, addTo } = req.auth const { key, addTo } = request.auth
if (addTo === "Headers" && key) {
if (addTo === "Headers") {
headers.push({ headers.push({
active: true, active: true,
key: parseTemplateString(key, envVars), key: parseTemplateString(key, envVars),
value: parseTemplateString(value, envVars), value: parseTemplateString(request.auth.value ?? "", envVars),
}) })
} }
} }
@@ -131,16 +134,18 @@ export type ComputedHeader = {
export const getComputedHeaders = ( export const getComputedHeaders = (
req: HoppRESTRequest, req: HoppRESTRequest,
envVars: Environment["variables"] envVars: Environment["variables"]
): ComputedHeader[] => [ ): ComputedHeader[] => {
...getComputedAuthHeaders(req, envVars).map((header) => ({ return [
source: "auth" as const, ...getComputedAuthHeaders(envVars, req).map((header) => ({
header, source: "auth" as const,
})), header,
...getComputedBodyHeaders(req).map((header) => ({ })),
source: "body" as const, ...getComputedBodyHeaders(req).map((header) => ({
header, source: "body" as const,
})), header,
] })),
]
}
export type ComputedParam = { export type ComputedParam = {
source: "auth" source: "auth"
@@ -160,7 +165,7 @@ export const getComputedParams = (
): ComputedParam[] => { ): ComputedParam[] => {
// When this gets complex, its best to split this function off (like with getComputedHeaders) // When this gets complex, its best to split this function off (like with getComputedHeaders)
// API-key auth can be added to query params // API-key auth can be added to query params
if (!req.auth.authActive) return [] if (!req.auth || !req.auth.authActive) return []
if (req.auth.authType !== "api-key") return [] if (req.auth.authType !== "api-key") return []
if (req.auth.addTo !== "Query params") return [] if (req.auth.addTo !== "Query params") return []

View File

@@ -10,23 +10,34 @@ import { cloneDeep } from "lodash-es"
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request" import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
import { getService } from "~/modules/dioc" import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const defaultRESTCollectionState = { const defaultRESTCollectionState = {
state: [ state: [
makeCollection<HoppRESTRequest>({ makeCollection({
name: "My Collection", name: "My Collection",
folders: [], folders: [],
requests: [], requests: [],
auth: {
authType: "inherit",
authActive: false,
},
headers: [],
}), }),
], ],
} }
const defaultGraphqlCollectionState = { const defaultGraphqlCollectionState = {
state: [ state: [
makeCollection<HoppGQLRequest>({ makeCollection({
name: "My GraphQL Collection", name: "My GraphQL Collection",
folders: [], folders: [],
requests: [], requests: [],
auth: {
authType: "inherit",
authActive: false,
},
headers: [],
}), }),
], ],
} }
@@ -39,7 +50,7 @@ type GraphqlCollectionStoreType = typeof defaultGraphqlCollectionState
* Not removing this behaviour because i'm not sure if we utilize this behaviour anywhere and i found this on a tight time crunch. * Not removing this behaviour because i'm not sure if we utilize this behaviour anywhere and i found this on a tight time crunch.
*/ */
export function navigateToFolderWithIndexPath( export function navigateToFolderWithIndexPath(
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[], collections: HoppCollection[],
indexPaths: number[] indexPaths: number[]
) { ) {
if (indexPaths.length === 0) return null if (indexPaths.length === 0) return null
@@ -52,6 +63,94 @@ export function navigateToFolderWithIndexPath(
return target !== undefined ? target : null return target !== undefined ? target : null
} }
export function cascadeParentCollectionForHeaderAuth(
folderPath: string | undefined,
type: "rest" | "graphql"
) {
const collectionStore =
type === "rest" ? restCollectionStore : graphqlCollectionStore
let auth: HoppInheritedProperty["auth"] = {
parentID: folderPath ?? "",
parentName: "",
inheritedAuth: {
authType: "none",
authActive: true,
},
}
const headers: HoppInheritedProperty["headers"] = []
if (!folderPath) return { auth, headers }
const path = folderPath.split("/").map((i) => parseInt(i))
// Check if the path is empty or invalid
if (!path || path.length === 0) {
console.error("Invalid path:", folderPath)
return { auth, headers }
}
// Loop through the path and get the last parent folder with authType other than 'inherit'
for (let i = 0; i < path.length; i++) {
const parentFolder = navigateToFolderWithIndexPath(
collectionStore.value.state,
[...path.slice(0, i + 1)] // Create a copy of the path array
)
// Check if parentFolder is undefined or null
if (!parentFolder) {
console.error("Parent folder not found for path:", path)
return { auth, headers }
}
const parentFolderAuth = parentFolder.auth
const parentFolderHeaders = parentFolder.headers
// check if the parent folder has authType 'inherit' and if it is the root folder
if (parentFolderAuth?.authType === "inherit" && path.length === 1) {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentName: parentFolder.name,
inheritedAuth: auth.inheritedAuth,
}
}
if (parentFolderAuth?.authType !== "inherit") {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentName: parentFolder.name,
inheritedAuth: parentFolderAuth,
}
}
// Update headers, overwriting duplicates by key
if (parentFolderHeaders) {
const activeHeaders = parentFolderHeaders.filter((h) => h.active)
activeHeaders.forEach((header) => {
const index = headers.findIndex(
(h) => h.inheritedHeader?.key === header.key
)
const currentPath = [...path.slice(0, i + 1)].join("/")
if (index !== -1) {
// Replace the existing header with the same key
headers[index] = {
parentID: currentPath,
parentName: parentFolder.name,
inheritedHeader: header,
}
} else {
headers.push({
parentID: currentPath,
parentName: parentFolder.name,
inheritedHeader: header,
})
}
})
}
}
return { auth, headers }
}
function reorderItems(array: unknown[], from: number, to: number) { function reorderItems(array: unknown[], from: number, to: number) {
const item = array.splice(from, 1)[0] const item = array.splice(from, 1)[0]
if (from < to) { if (from < to) {
@@ -64,7 +163,7 @@ function reorderItems(array: unknown[], from: number, to: number) {
const restCollectionDispatchers = defineDispatchers({ const restCollectionDispatchers = defineDispatchers({
setCollections( setCollections(
_: RESTCollectionStoreType, _: RESTCollectionStoreType,
{ entries }: { entries: HoppCollection<HoppRESTRequest>[] } { entries }: { entries: HoppCollection[] }
) { ) {
return { return {
state: entries, state: entries,
@@ -73,7 +172,7 @@ const restCollectionDispatchers = defineDispatchers({
appendCollections( appendCollections(
{ state }: RESTCollectionStoreType, { state }: RESTCollectionStoreType,
{ entries }: { entries: HoppCollection<HoppRESTRequest>[] } { entries }: { entries: HoppCollection[] }
) { ) {
return { return {
state: [...state, ...entries], state: [...state, ...entries],
@@ -82,7 +181,7 @@ const restCollectionDispatchers = defineDispatchers({
addCollection( addCollection(
{ state }: RESTCollectionStoreType, { state }: RESTCollectionStoreType,
{ collection }: { collection: HoppCollection<any> } { collection }: { collection: HoppCollection }
) { ) {
return { return {
state: [...state, collection], state: [...state, collection],
@@ -112,7 +211,7 @@ const restCollectionDispatchers = defineDispatchers({
partialCollection, partialCollection,
}: { }: {
collectionIndex: number collectionIndex: number
partialCollection: Partial<HoppCollection<any>> partialCollection: Partial<HoppCollection>
} }
) { ) {
return { return {
@@ -128,10 +227,15 @@ const restCollectionDispatchers = defineDispatchers({
{ state }: RESTCollectionStoreType, { state }: RESTCollectionStoreType,
{ name, path }: { name: string; path: string } { name, path }: { name: string; path: string }
) { ) {
const newFolder: HoppCollection<HoppRESTRequest> = makeCollection({ const newFolder: HoppCollection = makeCollection({
name, name,
folders: [], folders: [],
requests: [], requests: [],
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
}) })
const newState = state const newState = state
@@ -158,7 +262,7 @@ const restCollectionDispatchers = defineDispatchers({
folder, folder,
}: { }: {
path: string path: string
folder: Partial<HoppCollection<HoppRESTRequest>> folder: Partial<HoppCollection>
} }
) { ) {
const newState = state const newState = state
@@ -249,7 +353,7 @@ const restCollectionDispatchers = defineDispatchers({
} }
const theFolder = containingFolder.folders.splice(folderIndex, 1) const theFolder = containingFolder.folders.splice(folderIndex, 1)
newState.push(theFolder[0] as HoppCollection<HoppRESTRequest>) newState.push(theFolder[0] as HoppCollection)
return { return {
state: newState, state: newState,
@@ -612,7 +716,7 @@ const restCollectionDispatchers = defineDispatchers({
type: "collection" | "request" type: "collection" | "request"
} }
) { ) {
const after = removeDuplicateCollectionsFromPath<HoppRESTRequest>( const after = removeDuplicateCollectionsFromPath(
id, id,
collectionPath, collectionPath,
state, state,
@@ -628,7 +732,7 @@ const restCollectionDispatchers = defineDispatchers({
const gqlCollectionDispatchers = defineDispatchers({ const gqlCollectionDispatchers = defineDispatchers({
setCollections( setCollections(
_: GraphqlCollectionStoreType, _: GraphqlCollectionStoreType,
{ entries }: { entries: HoppCollection<any>[] } { entries }: { entries: HoppCollection[] }
) { ) {
return { return {
state: entries, state: entries,
@@ -637,7 +741,7 @@ const gqlCollectionDispatchers = defineDispatchers({
appendCollections( appendCollections(
{ state }: GraphqlCollectionStoreType, { state }: GraphqlCollectionStoreType,
{ entries }: { entries: HoppCollection<any>[] } { entries }: { entries: HoppCollection[] }
) { ) {
return { return {
state: [...state, ...entries], state: [...state, ...entries],
@@ -646,7 +750,7 @@ const gqlCollectionDispatchers = defineDispatchers({
addCollection( addCollection(
{ state }: GraphqlCollectionStoreType, { state }: GraphqlCollectionStoreType,
{ collection }: { collection: HoppCollection<any> } { collection }: { collection: HoppCollection }
) { ) {
return { return {
state: [...state, collection], state: [...state, collection],
@@ -673,7 +777,7 @@ const gqlCollectionDispatchers = defineDispatchers({
{ {
collectionIndex, collectionIndex,
collection, collection,
}: { collectionIndex: number; collection: Partial<HoppCollection<any>> } }: { collectionIndex: number; collection: Partial<HoppCollection> }
) { ) {
return { return {
state: state.map((col, index) => state: state.map((col, index) =>
@@ -686,12 +790,16 @@ const gqlCollectionDispatchers = defineDispatchers({
{ state }: GraphqlCollectionStoreType, { state }: GraphqlCollectionStoreType,
{ name, path }: { name: string; path: string } { name, path }: { name: string; path: string }
) { ) {
const newFolder: HoppCollection<HoppGQLRequest> = makeCollection({ const newFolder: HoppCollection = makeCollection({
name, name,
folders: [], folders: [],
requests: [], requests: [],
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
}) })
const newState = state const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x)) const indexPaths = path.split("/").map((x) => parseInt(x))
@@ -711,10 +819,7 @@ const gqlCollectionDispatchers = defineDispatchers({
editFolder( editFolder(
{ state }: GraphqlCollectionStoreType, { state }: GraphqlCollectionStoreType,
{ { path, folder }: { path: string; folder: Partial<HoppCollection> }
path,
folder,
}: { path: string; folder: Partial<HoppCollection<HoppGQLRequest>> }
) { ) {
const newState = state const newState = state
@@ -913,7 +1018,7 @@ const gqlCollectionDispatchers = defineDispatchers({
type: "collection" | "request" type: "collection" | "request"
} }
) { ) {
const after = removeDuplicateCollectionsFromPath<HoppGQLRequest>( const after = removeDuplicateCollectionsFromPath(
id, id,
collectionPath, collectionPath,
state, state,
@@ -936,7 +1041,7 @@ export const graphqlCollectionStore = new DispatchingStore(
gqlCollectionDispatchers gqlCollectionDispatchers
) )
export function setRESTCollections(entries: HoppCollection<HoppRESTRequest>[]) { export function setRESTCollections(entries: HoppCollection[]) {
restCollectionStore.dispatch({ restCollectionStore.dispatch({
dispatcher: "setCollections", dispatcher: "setCollections",
payload: { payload: {
@@ -953,9 +1058,7 @@ export const graphqlCollections$ = graphqlCollectionStore.subject$.pipe(
pluck("state") pluck("state")
) )
export function appendRESTCollections( export function appendRESTCollections(entries: HoppCollection[]) {
entries: HoppCollection<HoppRESTRequest>[]
) {
restCollectionStore.dispatch({ restCollectionStore.dispatch({
dispatcher: "appendCollections", dispatcher: "appendCollections",
payload: { payload: {
@@ -964,7 +1067,7 @@ export function appendRESTCollections(
}) })
} }
export function addRESTCollection(collection: HoppCollection<HoppRESTRequest>) { export function addRESTCollection(collection: HoppCollection) {
restCollectionStore.dispatch({ restCollectionStore.dispatch({
dispatcher: "addCollection", dispatcher: "addCollection",
payload: { payload: {
@@ -992,7 +1095,7 @@ export function getRESTCollection(collectionIndex: number) {
export function editRESTCollection( export function editRESTCollection(
collectionIndex: number, collectionIndex: number,
partialCollection: Partial<HoppCollection<HoppRESTRequest>> partialCollection: Partial<HoppCollection>
) { ) {
restCollectionStore.dispatch({ restCollectionStore.dispatch({
dispatcher: "editCollection", dispatcher: "editCollection",
@@ -1013,10 +1116,7 @@ export function addRESTFolder(name: string, path: string) {
}) })
} }
export function editRESTFolder( export function editRESTFolder(path: string, folder: Partial<HoppCollection>) {
path: string,
folder: Partial<HoppCollection<HoppRESTRequest>>
) {
restCollectionStore.dispatch({ restCollectionStore.dispatch({
dispatcher: "editFolder", dispatcher: "editFolder",
payload: { payload: {
@@ -1160,9 +1260,7 @@ export function updateRESTCollectionOrder(
}) })
} }
export function setGraphqlCollections( export function setGraphqlCollections(entries: HoppCollection[]) {
entries: HoppCollection<HoppGQLRequest>[]
) {
graphqlCollectionStore.dispatch({ graphqlCollectionStore.dispatch({
dispatcher: "setCollections", dispatcher: "setCollections",
payload: { payload: {
@@ -1171,9 +1269,7 @@ export function setGraphqlCollections(
}) })
} }
export function appendGraphqlCollections( export function appendGraphqlCollections(entries: HoppCollection[]) {
entries: HoppCollection<HoppGQLRequest>[]
) {
graphqlCollectionStore.dispatch({ graphqlCollectionStore.dispatch({
dispatcher: "appendCollections", dispatcher: "appendCollections",
payload: { payload: {
@@ -1182,9 +1278,7 @@ export function appendGraphqlCollections(
}) })
} }
export function addGraphqlCollection( export function addGraphqlCollection(collection: HoppCollection) {
collection: HoppCollection<HoppGQLRequest>
) {
graphqlCollectionStore.dispatch({ graphqlCollectionStore.dispatch({
dispatcher: "addCollection", dispatcher: "addCollection",
payload: { payload: {
@@ -1208,7 +1302,7 @@ export function removeGraphqlCollection(
export function editGraphqlCollection( export function editGraphqlCollection(
collectionIndex: number, collectionIndex: number,
collection: Partial<HoppCollection<HoppGQLRequest>> collection: Partial<HoppCollection>
) { ) {
graphqlCollectionStore.dispatch({ graphqlCollectionStore.dispatch({
dispatcher: "editCollection", dispatcher: "editCollection",
@@ -1231,7 +1325,7 @@ export function addGraphqlFolder(name: string, path: string) {
export function editGraphqlFolder( export function editGraphqlFolder(
path: string, path: string,
folder: Partial<HoppCollection<HoppGQLRequest>> folder: Partial<HoppCollection>
) { ) {
graphqlCollectionStore.dispatch({ graphqlCollectionStore.dispatch({
dispatcher: "editFolder", dispatcher: "editFolder",
@@ -1322,14 +1416,12 @@ export function moveGraphqlRequest(
}) })
} }
function removeDuplicateCollectionsFromPath< function removeDuplicateCollectionsFromPath(
T extends HoppRESTRequest | HoppGQLRequest,
>(
idToRemove: string, idToRemove: string,
collectionPath: string | null, collectionPath: string | null,
collections: HoppCollection<T>[], collections: HoppCollection[],
type: "collection" | "request" type: "collection" | "request"
): HoppCollection<T>[] { ): HoppCollection[] {
const indexes = collectionPath?.split("/").map((x) => parseInt(x)) const indexes = collectionPath?.split("/").map((x) => parseInt(x))
indexes && indexes.pop() indexes && indexes.pop()
const parentPath = indexes?.join("/") const parentPath = indexes?.join("/")

View File

@@ -12,7 +12,7 @@ import { onMounted } from "vue"
import { useRoute, useRouter } from "vue-router" import { useRoute, useRouter } from "vue-router"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import { appendRESTCollections } from "~/newstore/collections" import { appendRESTCollections } from "~/newstore/collections"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
@@ -110,9 +110,7 @@ const handleImportFailure = (error: ImportCollectionsError) => {
toast.error(t(IMPORT_ERROR_MAP[error]).toString()) toast.error(t(IMPORT_ERROR_MAP[error]).toString())
} }
const handleImportSuccess = ( const handleImportSuccess = (collections: HoppCollection[]) => {
collections: HoppCollection<HoppRESTRequest>[]
) => {
appendRESTCollections(collections) appendRESTCollections(collections)
toast.success(t("import.import_from_url_success").toString()) toast.success(t("import.import_from_url_success").toString())
} }

View File

@@ -1,9 +1,4 @@
import { import { Environment, HoppCollection } from "@hoppscotch/data"
Environment,
HoppCollection,
HoppGQLRequest,
HoppRESTRequest,
} from "@hoppscotch/data"
import { HoppGQLDocument } from "~/helpers/graphql/document" import { HoppGQLDocument } from "~/helpers/graphql/document"
import { HoppRESTDocument } from "~/helpers/rest/document" import { HoppRESTDocument } from "~/helpers/rest/document"
@@ -14,15 +9,15 @@ import { PersistableTabState } from "~/services/tab"
type VUEX_DATA = { type VUEX_DATA = {
postwoman: { postwoman: {
settings?: SettingsDef settings?: SettingsDef
collections?: HoppCollection<HoppRESTRequest>[] collections?: HoppCollection[]
collectionsGraphql?: HoppCollection<HoppGQLRequest>[] collectionsGraphql?: HoppCollection[]
environments?: Environment[] environments?: Environment[]
} }
} }
const DEFAULT_SETTINGS = getDefaultSettings() const DEFAULT_SETTINGS = getDefaultSettings()
export const REST_COLLECTIONS_MOCK: HoppCollection<HoppRESTRequest>[] = [ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
{ {
v: 1, v: 1,
name: "Echo", name: "Echo",
@@ -44,7 +39,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection<HoppRESTRequest>[] = [
}, },
] ]
export const GQL_COLLECTIONS_MOCK: HoppCollection<HoppGQLRequest>[] = [ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
{ {
v: 1, v: 1,
name: "Echo", name: "Echo",

View File

@@ -3,7 +3,9 @@ import {
GQLHeader, GQLHeader,
HoppGQLAuth, HoppGQLAuth,
HoppGQLRequest, HoppGQLRequest,
HoppRESTAuth,
HoppRESTRequest, HoppRESTRequest,
HoppRESTHeaders,
} from "@hoppscotch/data" } from "@hoppscotch/data"
import { entityReference } from "verzod" import { entityReference } from "verzod"
import { z } from "zod" import { z } from "zod"
@@ -62,12 +64,18 @@ const HoppGQLRequestSchema = entityReference(HoppGQLRequest)
const HoppRESTCollectionSchema = HoppCollectionSchemaCommonProps.extend({ const HoppRESTCollectionSchema = HoppCollectionSchemaCommonProps.extend({
folders: z.array(z.lazy(() => HoppRESTCollectionSchema)), folders: z.array(z.lazy(() => HoppRESTCollectionSchema)),
requests: z.optional(z.array(HoppRESTRequestSchema)), requests: z.optional(z.array(HoppRESTRequestSchema)),
auth: z.optional(HoppRESTAuth),
headers: z.optional(HoppRESTHeaders),
}).strict() }).strict()
// @ts-expect-error recursive schema // @ts-expect-error recursive schema
const HoppGQLCollectionSchema = HoppCollectionSchemaCommonProps.extend({ const HoppGQLCollectionSchema = HoppCollectionSchemaCommonProps.extend({
folders: z.array(z.lazy(() => HoppGQLCollectionSchema)), folders: z.array(z.lazy(() => HoppGQLCollectionSchema)),
requests: z.optional(z.array(HoppGQLRequestSchema)), requests: z.optional(z.array(HoppGQLRequestSchema)),
auth: z.optional(HoppGQLAuth),
headers: z.optional(z.array(GQLHeader)),
}).strict() }).strict()
export const VUEX_SCHEMA = z.object({ export const VUEX_SCHEMA = z.object({
@@ -276,6 +284,23 @@ const validGqlOperations = [
"authorization", "authorization",
] as const ] as const
const HoppInheritedPropertySchema = z
.object({
auth: z.object({
parentID: z.string(),
parentName: z.string(),
inheritedAuth: z.union([HoppRESTAuth, HoppGQLAuth]),
}),
headers: z.array(
z.object({
parentID: z.string(),
parentName: z.string(),
inheritedHeader: z.union([HoppRESTHeaders, GQLHeader]),
})
),
})
.strict()
export const GQL_TAB_STATE_SCHEMA = z export const GQL_TAB_STATE_SCHEMA = z
.object({ .object({
lastActiveTabID: z.string(), lastActiveTabID: z.string(),
@@ -291,6 +316,7 @@ export const GQL_TAB_STATE_SCHEMA = z
response: z.optional(z.nullable(GQLResponseEventSchema)), response: z.optional(z.nullable(GQLResponseEventSchema)),
responseTabPreference: z.optional(z.string()), responseTabPreference: z.optional(z.string()),
optionTabPreference: z.optional(z.enum(validGqlOperations)), optionTabPreference: z.optional(z.enum(validGqlOperations)),
inheritedProperties: z.optional(HoppInheritedPropertySchema),
}) })
.strict(), .strict(),
}) })
@@ -462,6 +488,7 @@ export const REST_TAB_STATE_SCHEMA = z
testResults: z.optional(z.nullable(HoppTestResultSchema)), testResults: z.optional(z.nullable(HoppTestResultSchema)),
responseTabPreference: z.optional(z.string()), responseTabPreference: z.optional(z.string()),
optionTabPreference: z.optional(z.enum(validRestOperations)), optionTabPreference: z.optional(z.enum(validRestOperations)),
inheritedProperties: z.optional(HoppInheritedPropertySchema),
}) })
.strict(), .strict(),
}) })

View File

@@ -10,6 +10,7 @@ import { Ref, computed, effectScope, markRaw, ref, watch } from "vue"
import { getI18n } from "~/modules/i18n" import { getI18n } from "~/modules/i18n"
import MiniSearch from "minisearch" import MiniSearch from "minisearch"
import { import {
cascadeParentCollectionForHeaderAuth,
graphqlCollectionStore, graphqlCollectionStore,
restCollectionStore, restCollectionStore,
} from "~/newstore/collections" } from "~/newstore/collections"
@@ -229,7 +230,7 @@ export class CollectionsSpotlightSearcherService
private getRESTFolderFromFolderPath( private getRESTFolderFromFolderPath(
folderPath: string folderPath: string
): HoppCollection<HoppRESTRequest> | undefined { ): HoppCollection | undefined {
try { try {
const folderIndicies = folderPath.split("/").map((x) => parseInt(x)) const folderIndicies = folderPath.split("/").map((x) => parseInt(x))
@@ -253,7 +254,7 @@ export class CollectionsSpotlightSearcherService
private getGQLFolderFromFolderPath( private getGQLFolderFromFolderPath(
folderPath: string folderPath: string
): HoppCollection<HoppGQLRequest> | undefined { ): HoppCollection | undefined {
try { try {
const folderIndicies = folderPath.split("/").map((x) => parseInt(x)) const folderIndicies = folderPath.split("/").map((x) => parseInt(x))
@@ -300,10 +301,15 @@ export class CollectionsSpotlightSearcherService
this.restTab.setActiveTab(possibleTab.value.id) this.restTab.setActiveTab(possibleTab.value.id)
} else { } else {
const req = this.getRESTFolderFromFolderPath(folderPath.join("/")) const req = this.getRESTFolderFromFolderPath(folderPath.join("/"))
?.requests[reqIndex] ?.requests[reqIndex] as HoppRESTRequest
if (!req) return if (!req) return
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath.join("/"),
"rest"
)
this.restTab.createNewTab( this.restTab.createNewTab(
{ {
request: req, request: req,
@@ -313,6 +319,10 @@ export class CollectionsSpotlightSearcherService
folderPath: folderPath.join("/"), folderPath: folderPath.join("/"),
requestIndex: reqIndex, requestIndex: reqIndex,
}, },
inheritedProperties: {
auth,
headers,
},
}, },
true true
) )
@@ -322,10 +332,14 @@ export class CollectionsSpotlightSearcherService
const reqIndex = folderPath.pop()! const reqIndex = folderPath.pop()!
const req = this.getGQLFolderFromFolderPath(folderPath.join("/")) const req = this.getGQLFolderFromFolderPath(folderPath.join("/"))
?.requests[reqIndex] ?.requests[reqIndex] as HoppGQLRequest
if (!req) return if (!req) return
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath.join("/"),
"graphql"
)
this.gqlTab.createNewTab({ this.gqlTab.createNewTab({
saveContext: { saveContext: {
originLocation: "user-collection", originLocation: "user-collection",
@@ -334,6 +348,10 @@ export class CollectionsSpotlightSearcherService
}, },
request: req, request: req,
isDirty: false, isDirty: false,
inheritedProperties: {
auth,
headers,
},
}) })
} }
} }

View File

@@ -1,33 +1,48 @@
import { GQL_REQ_SCHEMA_VERSION, HoppGQLRequest, translateToGQLRequest } from "../graphql"; import { InferredEntity, createVersionedEntity } from "verzod"
import { HoppRESTRequest, translateToNewRequest } from "../rest";
const CURRENT_COLL_SCHEMA_VER = 1 import V1_VERSION from "./v/1"
import V2_VERSION from "./v/2"
type SupportedReqTypes = import { z } from "zod"
| HoppRESTRequest import { translateToNewRequest } from "../rest"
| HoppGQLRequest import { translateToGQLRequest } from "../graphql"
export type HoppCollection<T extends SupportedReqTypes> = { const versionedObject = z.object({
v: number // v is a stringified number
name: string v: z.string().regex(/^\d+$/).transform(Number),
folders: HoppCollection<T>[] })
requests: T[]
id?: string // For Firestore ID data export const HoppCollection = createVersionedEntity({
} latestVersion: 2,
versionMap: {
1: V1_VERSION,
2: V2_VERSION,
},
getVersion(data) {
const versionCheck = versionedObject.safeParse(data)
if (versionCheck.success) return versionCheck.data.v
// For V1 we have to check the schema
const result = V1_VERSION.schema.safeParse(data)
return result.success ? 0 : null
},
})
export type HoppCollection = InferredEntity<typeof HoppCollection>
export const CollectionSchemaVersion = 2
/** /**
* Generates a Collection object. This ignores the version number object * Generates a Collection object. This ignores the version number object
* so it can be incremented independently without updating it everywhere
* @param x The Collection Data * @param x The Collection Data
* @returns The final collection * @returns The final collection
*/ */
export function makeCollection<T extends SupportedReqTypes>( export function makeCollection(x: Omit<HoppCollection, "v">): HoppCollection {
x: Omit<HoppCollection<T>, "v">
): HoppCollection<T> {
return { return {
v: CURRENT_COLL_SCHEMA_VER, v: CollectionSchemaVersion,
...x ...x,
} }
} }
@@ -36,20 +51,23 @@ export function makeCollection<T extends SupportedReqTypes>(
* @param x The collection object to load * @param x The collection object to load
* @returns The proper new collection format * @returns The proper new collection format
*/ */
export function translateToNewRESTCollection( export function translateToNewRESTCollection(x: any): HoppCollection {
x: any if (x.v && x.v === CollectionSchemaVersion) return x
): HoppCollection<HoppRESTRequest> {
if (x.v && x.v === 1) return x
// Legacy // Legacy
const name = x.name ?? "Untitled" const name = x.name ?? "Untitled"
const folders = (x.folders ?? []).map(translateToNewRESTCollection) const folders = (x.folders ?? []).map(translateToNewRESTCollection)
const requests = (x.requests ?? []).map(translateToNewRequest) const requests = (x.requests ?? []).map(translateToNewRequest)
const obj = makeCollection<HoppRESTRequest>({ const auth = x.auth ?? { authType: "inherit", authActive: true }
const headers = x.headers ?? []
const obj = makeCollection({
name, name,
folders, folders,
requests, requests,
auth,
headers,
}) })
if (x.id) obj.id = x.id if (x.id) obj.id = x.id
@@ -62,24 +80,26 @@ export function translateToNewRESTCollection(
* @param x The collection object to load * @param x The collection object to load
* @returns The proper new collection format * @returns The proper new collection format
*/ */
export function translateToNewGQLCollection( export function translateToNewGQLCollection(x: any): HoppCollection {
x: any if (x.v && x.v === CollectionSchemaVersion) return x
): HoppCollection<HoppGQLRequest> {
if (x.v && x.v === GQL_REQ_SCHEMA_VERSION) return x
// Legacy // Legacy
const name = x.name ?? "Untitled" const name = x.name ?? "Untitled"
const folders = (x.folders ?? []).map(translateToNewGQLCollection) const folders = (x.folders ?? []).map(translateToNewGQLCollection)
const requests = (x.requests ?? []).map(translateToGQLRequest) const requests = (x.requests ?? []).map(translateToGQLRequest)
const obj = makeCollection<HoppGQLRequest>({ const auth = x.auth ?? { authType: "inherit", authActive: true }
const headers = x.headers ?? []
const obj = makeCollection({
name, name,
folders, folders,
requests, requests,
auth,
headers,
}) })
if (x.id) obj.id = x.id if (x.id) obj.id = x.id
return obj return obj
} }

View File

@@ -0,0 +1,36 @@
import { defineVersion, entityReference } from "verzod"
import { z } from "zod"
import { HoppRESTRequest } from "../../rest"
import { HoppGQLRequest } from "../../graphql"
const baseCollectionSchema = z.object({
v: z.literal(1),
id: z.optional(z.string()), // For Firestore ID data
name: z.string(),
requests: z.array(
z.lazy(() =>
z.union([
entityReference(HoppRESTRequest),
entityReference(HoppGQLRequest),
])
)
),
})
type Input = z.input<typeof baseCollectionSchema> & {
folders: Input[]
}
type Output = z.output<typeof baseCollectionSchema> & {
folders: Output[]
}
export const V1_SCHEMA: z.ZodType<Output, z.ZodTypeDef, Input> = baseCollectionSchema.extend({
folders: z.lazy(() => z.array(V1_SCHEMA)),
})
export default defineVersion({
initial: true,
schema: V1_SCHEMA,
})

View File

@@ -0,0 +1,57 @@
import { defineVersion, entityReference } from "verzod"
import { z } from "zod"
import { HoppRESTRequest, HoppRESTAuth } from "../../rest"
import { HoppGQLRequest, HoppGQLAuth, GQLHeader } from "../../graphql"
import { V1_SCHEMA } from "./1"
import { HoppRESTHeaders } from "../../rest/v/1"
const baseCollectionSchema = z.object({
v: z.literal(2),
id: z.optional(z.string()), // For Firestore ID data
name: z.string(),
requests: z.array(
z.lazy(() =>
z.union([
entityReference(HoppRESTRequest),
entityReference(HoppGQLRequest),
])
)
),
auth: z.union([HoppRESTAuth, HoppGQLAuth]),
headers: z.union([HoppRESTHeaders, z.array(GQLHeader)]),
})
type Input = z.input<typeof baseCollectionSchema> & {
folders: Input[]
}
type Output = z.output<typeof baseCollectionSchema> & {
folders: Output[]
}
export const V2_SCHEMA: z.ZodType<Output, z.ZodTypeDef, Input> = baseCollectionSchema.extend({
folders: z.lazy(() => z.array(V2_SCHEMA)),
})
export default defineVersion({
initial: false,
schema: V2_SCHEMA,
up(old: z.infer<typeof V1_SCHEMA>) {
// @ts-expect-error
const result: z.infer<typeof V2_SCHEMA> = {
...old,
v: 2,
auth: {
authActive: true,
authType: "inherit",
},
headers: [],
}
if (old.id) result.id = old.id
return result
},
})

View File

@@ -11,6 +11,7 @@ export {
HoppGQLAuthBearer, HoppGQLAuthBearer,
HoppGQLAuthNone, HoppGQLAuthNone,
HoppGQLAuthOAuth2, HoppGQLAuthOAuth2,
HoppGQLAuthInherit,
} from "./v/2" } from "./v/2"
export const GQL_REQ_SCHEMA_VERSION = 2 export const GQL_REQ_SCHEMA_VERSION = 2

View File

@@ -3,7 +3,7 @@ import { defineVersion } from "verzod"
import { GQLHeader, V1_SCHEMA } from "./1" import { GQLHeader, V1_SCHEMA } from "./1"
export const HoppGQLAuthNone = z.object({ export const HoppGQLAuthNone = z.object({
authType: z.literal("none") authType: z.literal("none"),
}) })
export type HoppGQLAuthNone = z.infer<typeof HoppGQLAuthNone> export type HoppGQLAuthNone = z.infer<typeof HoppGQLAuthNone>
@@ -12,7 +12,7 @@ export const HoppGQLAuthBasic = z.object({
authType: z.literal("basic"), authType: z.literal("basic"),
username: z.string().catch(""), username: z.string().catch(""),
password: z.string().catch("") password: z.string().catch(""),
}) })
export type HoppGQLAuthBasic = z.infer<typeof HoppGQLAuthBasic> export type HoppGQLAuthBasic = z.infer<typeof HoppGQLAuthBasic>
@@ -20,7 +20,7 @@ export type HoppGQLAuthBasic = z.infer<typeof HoppGQLAuthBasic>
export const HoppGQLAuthBearer = z.object({ export const HoppGQLAuthBearer = z.object({
authType: z.literal("bearer"), authType: z.literal("bearer"),
token: z.string().catch("") token: z.string().catch(""),
}) })
export type HoppGQLAuthBearer = z.infer<typeof HoppGQLAuthBearer> export type HoppGQLAuthBearer = z.infer<typeof HoppGQLAuthBearer>
@@ -33,7 +33,7 @@ export const HoppGQLAuthOAuth2 = z.object({
authURL: z.string().catch(""), authURL: z.string().catch(""),
accessTokenURL: z.string().catch(""), accessTokenURL: z.string().catch(""),
clientID: z.string().catch(""), clientID: z.string().catch(""),
scope: z.string().catch("") scope: z.string().catch(""),
}) })
export type HoppGQLAuthOAuth2 = z.infer<typeof HoppGQLAuthOAuth2> export type HoppGQLAuthOAuth2 = z.infer<typeof HoppGQLAuthOAuth2>
@@ -43,22 +43,31 @@ export const HoppGQLAuthAPIKey = z.object({
key: z.string().catch(""), key: z.string().catch(""),
value: z.string().catch(""), value: z.string().catch(""),
addTo: z.string().catch("Headers") addTo: z.string().catch("Headers"),
}) })
export type HoppGQLAuthAPIKey = z.infer<typeof HoppGQLAuthAPIKey> export type HoppGQLAuthAPIKey = z.infer<typeof HoppGQLAuthAPIKey>
export const HoppGQLAuth = z.discriminatedUnion("authType", [ export const HoppGQLAuthInherit = z.object({
HoppGQLAuthNone, authType: z.literal("inherit"),
HoppGQLAuthBasic, })
HoppGQLAuthBearer,
HoppGQLAuthOAuth2, export type HoppGQLAuthInherit = z.infer<typeof HoppGQLAuthInherit>
HoppGQLAuthAPIKey
]).and( export const HoppGQLAuth = z
z.object({ .discriminatedUnion("authType", [
authActive: z.boolean() HoppGQLAuthNone,
}) HoppGQLAuthBasic,
) HoppGQLAuthBearer,
HoppGQLAuthOAuth2,
HoppGQLAuthAPIKey,
HoppGQLAuthInherit,
])
.and(
z.object({
authActive: z.boolean(),
})
)
export type HoppGQLAuth = z.infer<typeof HoppGQLAuth> export type HoppGQLAuth = z.infer<typeof HoppGQLAuth>
@@ -72,7 +81,7 @@ const V2_SCHEMA = z.object({
query: z.string(), query: z.string(),
variables: z.string(), variables: z.string(),
auth: HoppGQLAuth auth: HoppGQLAuth,
}) })
export default defineVersion({ export default defineVersion({
@@ -85,7 +94,7 @@ export default defineVersion({
auth: { auth: {
authActive: true, authActive: true,
authType: "none", authType: "none",
} },
} }
} },
}) })

View File

@@ -20,10 +20,12 @@ export {
HoppRESTAuth, HoppRESTAuth,
HoppRESTAuthAPIKey, HoppRESTAuthAPIKey,
HoppRESTAuthBasic, HoppRESTAuthBasic,
HoppRESTAuthInherit,
HoppRESTAuthBearer, HoppRESTAuthBearer,
HoppRESTAuthNone, HoppRESTAuthNone,
HoppRESTAuthOAuth2, HoppRESTAuthOAuth2,
HoppRESTReqBody, HoppRESTReqBody,
HoppRESTHeaders,
} from "./v/1" } from "./v/1"
const versionedObject = z.object({ const versionedObject = z.object({

View File

@@ -3,27 +3,29 @@ import { z } from "zod"
import { V0_SCHEMA } from "./0" import { V0_SCHEMA } from "./0"
export const FormDataKeyValue = z.object({ export const FormDataKeyValue = z
key: z.string(), .object({
active: z.boolean() key: z.string(),
}).and( active: z.boolean(),
z.union([ })
z.object({ .and(
isFile: z.literal(true), z.union([
value: z.array(z.instanceof(Blob).nullable()) z.object({
}), isFile: z.literal(true),
z.object({ value: z.array(z.instanceof(Blob).nullable()),
isFile: z.literal(false), }),
value: z.string() z.object({
}) isFile: z.literal(false),
]) value: z.string(),
) }),
])
)
export type FormDataKeyValue = z.infer<typeof FormDataKeyValue> export type FormDataKeyValue = z.infer<typeof FormDataKeyValue>
export const HoppRESTReqBodyFormData = z.object({ export const HoppRESTReqBodyFormData = z.object({
contentType: z.literal("multipart/form-data"), contentType: z.literal("multipart/form-data"),
body: z.array(FormDataKeyValue) body: z.array(FormDataKeyValue),
}) })
export type HoppRESTReqBodyFormData = z.infer<typeof HoppRESTReqBodyFormData> export type HoppRESTReqBodyFormData = z.infer<typeof HoppRESTReqBodyFormData>
@@ -31,11 +33,11 @@ export type HoppRESTReqBodyFormData = z.infer<typeof HoppRESTReqBodyFormData>
export const HoppRESTReqBody = z.union([ export const HoppRESTReqBody = z.union([
z.object({ z.object({
contentType: z.literal(null), contentType: z.literal(null),
body: z.literal(null).catch(null) body: z.literal(null).catch(null),
}), }),
z.object({ z.object({
contentType: z.literal("multipart/form-data"), contentType: z.literal("multipart/form-data"),
body: z.array(FormDataKeyValue).catch([]) body: z.array(FormDataKeyValue).catch([]),
}), }),
z.object({ z.object({
contentType: z.union([ contentType: z.union([
@@ -48,14 +50,14 @@ export const HoppRESTReqBody = z.union([
z.literal("text/html"), z.literal("text/html"),
z.literal("text/plain"), z.literal("text/plain"),
]), ]),
body: z.string().catch("") body: z.string().catch(""),
}) }),
]) ])
export type HoppRESTReqBody = z.infer<typeof HoppRESTReqBody> export type HoppRESTReqBody = z.infer<typeof HoppRESTReqBody>
export const HoppRESTAuthNone = z.object({ export const HoppRESTAuthNone = z.object({
authType: z.literal("none") authType: z.literal("none"),
}) })
export type HoppRESTAuthNone = z.infer<typeof HoppRESTAuthNone> export type HoppRESTAuthNone = z.infer<typeof HoppRESTAuthNone>
@@ -96,17 +98,26 @@ export const HoppRESTAuthAPIKey = z.object({
export type HoppRESTAuthAPIKey = z.infer<typeof HoppRESTAuthAPIKey> export type HoppRESTAuthAPIKey = z.infer<typeof HoppRESTAuthAPIKey>
export const HoppRESTAuth = z.discriminatedUnion("authType", [ export const HoppRESTAuthInherit = z.object({
HoppRESTAuthNone, authType: z.literal("inherit"),
HoppRESTAuthBasic, })
HoppRESTAuthBearer,
HoppRESTAuthOAuth2, export type HoppRESTAuthInherit = z.infer<typeof HoppRESTAuthInherit>
HoppRESTAuthAPIKey
]).and( export const HoppRESTAuth = z
z.object({ .discriminatedUnion("authType", [
authActive: z.boolean(), HoppRESTAuthNone,
}) HoppRESTAuthInherit,
) HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey,
])
.and(
z.object({
authActive: z.boolean(),
})
)
export type HoppRESTAuth = z.infer<typeof HoppRESTAuth> export type HoppRESTAuth = z.infer<typeof HoppRESTAuth>
@@ -114,7 +125,7 @@ export const HoppRESTParams = z.array(
z.object({ z.object({
key: z.string().catch(""), key: z.string().catch(""),
value: z.string().catch(""), value: z.string().catch(""),
active: z.boolean().catch(true) active: z.boolean().catch(true),
}) })
) )
@@ -124,7 +135,7 @@ export const HoppRESTHeaders = z.array(
z.object({ z.object({
key: z.string().catch(""), key: z.string().catch(""),
value: z.string().catch(""), value: z.string().catch(""),
active: z.boolean().catch(true) active: z.boolean().catch(true),
}) })
) )
@@ -144,17 +155,21 @@ const V1_SCHEMA = z.object({
auth: HoppRESTAuth, auth: HoppRESTAuth,
body: HoppRESTReqBody body: HoppRESTReqBody,
}) })
function parseRequestBody(x: z.infer<typeof V0_SCHEMA>): z.infer<typeof V1_SCHEMA>["body"] { function parseRequestBody(
x: z.infer<typeof V0_SCHEMA>
): z.infer<typeof V1_SCHEMA>["body"] {
return { return {
contentType: "application/json", contentType: "application/json",
body: x.contentType === "application/json" ? x.rawParams ?? "" : "", body: x.contentType === "application/json" ? x.rawParams ?? "" : "",
} }
} }
export function parseOldAuth(x: z.infer<typeof V0_SCHEMA>): z.infer<typeof V1_SCHEMA>["auth"] { export function parseOldAuth(
x: z.infer<typeof V0_SCHEMA>
): z.infer<typeof V1_SCHEMA>["auth"] {
if (!x.auth || x.auth === "None") if (!x.auth || x.auth === "None")
return { return {
authType: "none", authType: "none",
@@ -183,7 +198,16 @@ export default defineVersion({
initial: false, initial: false,
schema: V1_SCHEMA, schema: V1_SCHEMA,
up(old: z.infer<typeof V0_SCHEMA>) { up(old: z.infer<typeof V0_SCHEMA>) {
const { url, path, headers, params, name, method, preRequestScript, testScript } = old const {
url,
path,
headers,
params,
name,
method,
preRequestScript,
testScript,
} = old
const endpoint = `${url}${path}` const endpoint = `${url}${path}`
const body = parseRequestBody(old) const body = parseRequestBody(old)

View File

@@ -13,5 +13,5 @@
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"declarationDir": "./dist" "declarationDir": "./dist"
}, },
"include": ["src/*.ts"] "include": ["src/**/*.ts"]
} }

View File

@@ -10,5 +10,5 @@
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": ["src/*.ts"] "include": ["src/**/*.ts"]
} }

View File

@@ -101,7 +101,7 @@ type ExportedUserCollectionGQL = {
function exportedCollectionToHoppCollection( function exportedCollectionToHoppCollection(
collection: ExportedUserCollectionREST | ExportedUserCollectionGQL, collection: ExportedUserCollectionREST | ExportedUserCollectionGQL,
collectionType: "REST" | "GQL" collectionType: "REST" | "GQL"
): HoppCollection<HoppRESTRequest | HoppGQLRequest> { ): HoppCollection {
if (collectionType == "REST") { if (collectionType == "REST") {
const restCollection = collection as ExportedUserCollectionREST const restCollection = collection as ExportedUserCollectionREST
@@ -186,7 +186,7 @@ async function loadUserCollections(collectionType: "REST" | "GQL") {
exportedCollectionToHoppCollection( exportedCollectionToHoppCollection(
collection, collection,
"REST" "REST"
) as HoppCollection<HoppRESTRequest> ) as HoppCollection
) )
) )
: setGraphqlCollections( : setGraphqlCollections(
@@ -195,7 +195,7 @@ async function loadUserCollections(collectionType: "REST" | "GQL") {
exportedCollectionToHoppCollection( exportedCollectionToHoppCollection(
collection, collection,
"GQL" "GQL"
) as HoppCollection<HoppGQLRequest> ) as HoppCollection
) )
) )
}) })
@@ -718,7 +718,7 @@ export const def: CollectionsPlatformDef = {
function getCollectionPathFromCollectionID( function getCollectionPathFromCollectionID(
collectionID: string, collectionID: string,
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[], collections: HoppCollection[],
parentPath?: string parentPath?: string
): string | null { ): string | null {
for (const collectionIndex in collections) { for (const collectionIndex in collections) {
@@ -742,7 +742,7 @@ function getCollectionPathFromCollectionID(
function getRequestPathFromRequestID( function getRequestPathFromRequestID(
requestID: string, requestID: string,
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[], collections: HoppCollection[],
parentPath?: string parentPath?: string
): { collectionPath: string; requestIndex: number } | null { ): { collectionPath: string; requestIndex: number } | null {
for (const collectionIndex in collections) { for (const collectionIndex in collections) {
@@ -774,7 +774,7 @@ function getRequestPathFromRequestID(
function getRequestIndex( function getRequestIndex(
requestID: string, requestID: string,
parentCollectionPath: string, parentCollectionPath: string,
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[] collections: HoppCollection[]
) { ) {
const collection = navigateToFolderWithIndexPath( const collection = navigateToFolderWithIndexPath(
collections, collections,

View File

@@ -39,7 +39,7 @@ export const restRequestsMapper = createMapper<string, string>()
// temp implementation untill the backend implements an endpoint that accepts an entire collection // temp implementation untill the backend implements an endpoint that accepts an entire collection
// TODO: use importCollectionsJSON to do this // TODO: use importCollectionsJSON to do this
const recursivelySyncCollections = async ( const recursivelySyncCollections = async (
collection: HoppCollection<HoppRESTRequest>, collection: HoppCollection,
collectionPath: string, collectionPath: string,
parentUserCollectionID?: string parentUserCollectionID?: string
) => { ) => {

View File

@@ -36,7 +36,7 @@ export const gqlRequestsMapper = createMapper<string, string>()
// temp implementation untill the backend implements an endpoint that accepts an entire collection // temp implementation untill the backend implements an endpoint that accepts an entire collection
// TODO: use importCollectionsJSON to do this // TODO: use importCollectionsJSON to do this
const recursivelySyncCollections = async ( const recursivelySyncCollections = async (
collection: HoppCollection<HoppRESTRequest>, collection: HoppCollection,
collectionPath: string, collectionPath: string,
parentUserCollectionID?: string parentUserCollectionID?: string
) => { ) => {

View File

@@ -1,11 +1,14 @@
mutation CreateGQLChildUserCollection( mutation CreateGQLChildUserCollection(
$title: String! $title: String!
$parentUserCollectionID: ID! $parentUserCollectionID: ID!
$data: String
) { ) {
createGQLChildUserCollection( createGQLChildUserCollection(
title: $title title: $title
parentUserCollectionID: $parentUserCollectionID parentUserCollectionID: $parentUserCollectionID
data: $data
) { ) {
id id
data
} }
} }

View File

@@ -1,5 +1,6 @@
mutation CreateGQLRootUserCollection($title: String!) { mutation CreateGQLRootUserCollection($title: String!, $data: String) {
createGQLRootUserCollection(title: $title) { createGQLRootUserCollection(title: $title, data: $data) {
id id
data
} }
} }

View File

@@ -1,11 +1,14 @@
mutation CreateRESTChildUserCollection( mutation CreateRESTChildUserCollection(
$title: String! $title: String!
$parentUserCollectionID: ID! $parentUserCollectionID: ID!
$data: String
) { ) {
createRESTChildUserCollection( createRESTChildUserCollection(
title: $title title: $title
parentUserCollectionID: $parentUserCollectionID parentUserCollectionID: $parentUserCollectionID
data: $data
) { ) {
id id
data
} }
} }

View File

@@ -1,5 +1,6 @@
mutation CreateRESTRootUserCollection($title: String!) { mutation CreateRESTRootUserCollection($title: String!, $data: String) {
createRESTRootUserCollection(title: $title) { createRESTRootUserCollection(title: $title, data: $data) {
id id
data
} }
} }

View File

@@ -0,0 +1,15 @@
mutation UpdateUserCollection(
$userCollectionID: ID!
$newTitle: String
$data: String
) {
updateUserCollection(
userCollectionID: $userCollectionID
newTitle: $newTitle
data: $data
) {
id
title
data
}
}

View File

@@ -4,10 +4,12 @@ query GetGQLRootUserCollections {
id id
title title
type type
data
childrenGQL { childrenGQL {
id id
title title
type type
data
} }
} }
} }

View File

@@ -4,10 +4,12 @@ query GetUserRootCollections {
id id
title title
type type
data
childrenREST { childrenREST {
id id
title title
type type
data
} }
} }
} }

View File

@@ -6,5 +6,6 @@ subscription UserCollectionCreated {
id id
title title
type type
data
} }
} }

View File

@@ -3,6 +3,7 @@ subscription userCollectionUpdated {
id id
title title
type type
data
parent { parent {
id id
} }

View File

@@ -65,24 +65,29 @@ import {
GetGqlRootUserCollectionsQueryVariables, GetGqlRootUserCollectionsQueryVariables,
GetGqlRootUserCollectionsDocument, GetGqlRootUserCollectionsDocument,
ReqType, ReqType,
UpdateUserCollectionMutation,
UpdateUserCollectionMutationVariables,
UpdateUserCollectionDocument,
} from "../../api/generated/graphql" } from "../../api/generated/graphql"
export const createRESTRootUserCollection = (title: string) => export const createRESTRootUserCollection = (title: string, data?: string) =>
runMutation< runMutation<
CreateRestRootUserCollectionMutation, CreateRestRootUserCollectionMutation,
CreateRestRootUserCollectionMutationVariables, CreateRestRootUserCollectionMutationVariables,
"" ""
>(CreateRestRootUserCollectionDocument, { >(CreateRestRootUserCollectionDocument, {
title, title,
data,
})() })()
export const createGQLRootUserCollection = (title: string) => export const createGQLRootUserCollection = (title: string, data?: string) =>
runMutation< runMutation<
CreateGqlRootUserCollectionMutation, CreateGqlRootUserCollectionMutation,
CreateGqlRootUserCollectionMutationVariables, CreateGqlRootUserCollectionMutationVariables,
"" ""
>(CreateGqlRootUserCollectionDocument, { >(CreateGqlRootUserCollectionDocument, {
title, title,
data,
})() })()
export const createRESTUserRequest = ( export const createRESTUserRequest = (
@@ -117,7 +122,8 @@ export const createGQLUserRequest = (
export const createRESTChildUserCollection = ( export const createRESTChildUserCollection = (
title: string, title: string,
parentUserCollectionID: string parentUserCollectionID: string,
data?: string
) => ) =>
runMutation< runMutation<
CreateRestChildUserCollectionMutation, CreateRestChildUserCollectionMutation,
@@ -126,11 +132,13 @@ export const createRESTChildUserCollection = (
>(CreateRestChildUserCollectionDocument, { >(CreateRestChildUserCollectionDocument, {
title, title,
parentUserCollectionID, parentUserCollectionID,
data,
})() })()
export const createGQLChildUserCollection = ( export const createGQLChildUserCollection = (
title: string, title: string,
parentUserCollectionID: string parentUserCollectionID: string,
data?: string
) => ) =>
runMutation< runMutation<
CreateGqlChildUserCollectionMutation, CreateGqlChildUserCollectionMutation,
@@ -139,6 +147,7 @@ export const createGQLChildUserCollection = (
>(CreateGqlChildUserCollectionDocument, { >(CreateGqlChildUserCollectionDocument, {
title, title,
parentUserCollectionID, parentUserCollectionID,
data,
})() })()
export const deleteUserCollection = (userCollectionID: string) => export const deleteUserCollection = (userCollectionID: string) =>
@@ -160,6 +169,17 @@ export const renameUserCollection = (
"" ""
>(RenameUserCollectionDocument, { userCollectionID, newTitle })() >(RenameUserCollectionDocument, { userCollectionID, newTitle })()
export const updateUserCollection = (
userCollectionID: string,
newTitle?: string,
data?: string
) =>
runMutation<
UpdateUserCollectionMutation,
UpdateUserCollectionMutationVariables,
""
>(UpdateUserCollectionDocument, { userCollectionID, newTitle, data })()
export const moveUserCollection = ( export const moveUserCollection = (
sourceCollectionID: string, sourceCollectionID: string,
destinationCollectionID?: string destinationCollectionID?: string

View File

@@ -89,6 +89,7 @@ type ExportedUserCollectionREST = {
folders: ExportedUserCollectionREST[] folders: ExportedUserCollectionREST[]
requests: Array<HoppRESTRequest & { id: string }> requests: Array<HoppRESTRequest & { id: string }>
name: string name: string
data: string
} }
type ExportedUserCollectionGQL = { type ExportedUserCollectionGQL = {
@@ -96,18 +97,25 @@ type ExportedUserCollectionGQL = {
folders: ExportedUserCollectionGQL[] folders: ExportedUserCollectionGQL[]
requests: Array<HoppGQLRequest & { id: string }> requests: Array<HoppGQLRequest & { id: string }>
name: string name: string
data: string
} }
function exportedCollectionToHoppCollection( function exportedCollectionToHoppCollection(
collection: ExportedUserCollectionREST | ExportedUserCollectionGQL, collection: ExportedUserCollectionREST | ExportedUserCollectionGQL,
collectionType: "REST" | "GQL" collectionType: "REST" | "GQL"
): HoppCollection<HoppRESTRequest | HoppGQLRequest> { ): HoppCollection {
if (collectionType == "REST") { if (collectionType == "REST") {
const restCollection = collection as ExportedUserCollectionREST const restCollection = collection as ExportedUserCollectionREST
const data =
restCollection.data && restCollection.data !== "null"
? JSON.parse(restCollection.data)
: {
auth: { authType: "inherit", authActive: false },
headers: [],
}
return { return {
id: restCollection.id, id: restCollection.id,
v: 1, v: 2,
name: restCollection.name, name: restCollection.name,
folders: restCollection.folders.map((folder) => folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType) exportedCollectionToHoppCollection(folder, collectionType)
@@ -139,26 +147,40 @@ function exportedCollectionToHoppCollection(
testScript, testScript,
}) })
), ),
auth: data.auth,
headers: data.headers,
} }
} else { } else {
const gqlCollection = collection as ExportedUserCollectionGQL const gqlCollection = collection as ExportedUserCollectionGQL
const data =
gqlCollection.data && gqlCollection.data !== "null"
? JSON.parse(gqlCollection.data)
: {
auth: { authType: "inherit", authActive: false },
headers: [],
}
return { return {
id: gqlCollection.id, id: gqlCollection.id,
v: 1, v: 2,
name: gqlCollection.name, name: gqlCollection.name,
folders: gqlCollection.folders.map((folder) => folders: gqlCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType) exportedCollectionToHoppCollection(folder, collectionType)
), ),
requests: gqlCollection.requests.map( requests: gqlCollection.requests.map(
({ v, auth, headers, name, id }) => ({ ({ v, auth, headers, name, id, query, url, variables }) => ({
id, id,
v, v,
auth, auth,
headers, headers,
name, name,
query,
url,
variables,
}) })
) as HoppGQLRequest[], ) as HoppGQLRequest[],
auth: data.auth,
headers: data.headers,
} }
} }
} }
@@ -168,7 +190,6 @@ async function loadUserCollections(collectionType: "REST" | "GQL") {
undefined, undefined,
collectionType == "REST" ? ReqType.Rest : ReqType.Gql collectionType == "REST" ? ReqType.Rest : ReqType.Gql
) )
if (E.isRight(res)) { if (E.isRight(res)) {
const collectionsJSONString = const collectionsJSONString =
res.right.exportUserCollectionsToJSON.exportedCollection res.right.exportUserCollectionsToJSON.exportedCollection
@@ -177,7 +198,6 @@ async function loadUserCollections(collectionType: "REST" | "GQL") {
ExportedUserCollectionGQL | ExportedUserCollectionREST ExportedUserCollectionGQL | ExportedUserCollectionREST
> >
).map((collection) => ({ v: 1, ...collection })) ).map((collection) => ({ v: 1, ...collection }))
runDispatchWithOutSyncing(() => { runDispatchWithOutSyncing(() => {
collectionType == "REST" collectionType == "REST"
? setRESTCollections( ? setRESTCollections(
@@ -186,7 +206,7 @@ async function loadUserCollections(collectionType: "REST" | "GQL") {
exportedCollectionToHoppCollection( exportedCollectionToHoppCollection(
collection, collection,
"REST" "REST"
) as HoppCollection<HoppRESTRequest> ) as HoppCollection
) )
) )
: setGraphqlCollections( : setGraphqlCollections(
@@ -195,7 +215,7 @@ async function loadUserCollections(collectionType: "REST" | "GQL") {
exportedCollectionToHoppCollection( exportedCollectionToHoppCollection(
collection, collection,
"GQL" "GQL"
) as HoppCollection<HoppGQLRequest> ) as HoppCollection
) )
) )
}) })
@@ -292,19 +312,32 @@ function setupUserCollectionCreatedSubscription() {
}) })
} else { } else {
// root collections won't have parentCollectionID // root collections won't have parentCollectionID
const data =
res.right.userCollectionCreated.data &&
res.right.userCollectionCreated.data != "null"
? JSON.parse(res.right.userCollectionCreated.data)
: {
auth: { authType: "inherit", authActive: false },
headers: [],
}
runDispatchWithOutSyncing(() => { runDispatchWithOutSyncing(() => {
collectionType == "GQL" collectionType == "GQL"
? addGraphqlCollection({ ? addGraphqlCollection({
name: res.right.userCollectionCreated.title, name: res.right.userCollectionCreated.title,
folders: [], folders: [],
requests: [], requests: [],
v: 1, v: 2,
auth: data.auth,
headers: data.headers,
}) })
: addRESTCollection({ : addRESTCollection({
name: res.right.userCollectionCreated.title, name: res.right.userCollectionCreated.title,
folders: [], folders: [],
requests: [], requests: [],
v: 1, v: 2,
auth: data.auth,
headers: data?.headers,
}) })
const localIndex = collectionStore.value.state.length - 1 const localIndex = collectionStore.value.state.length - 1
@@ -718,7 +751,7 @@ export const def: CollectionsPlatformDef = {
function getCollectionPathFromCollectionID( function getCollectionPathFromCollectionID(
collectionID: string, collectionID: string,
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[], collections: HoppCollection[],
parentPath?: string parentPath?: string
): string | null { ): string | null {
for (const collectionIndex in collections) { for (const collectionIndex in collections) {
@@ -742,7 +775,7 @@ function getCollectionPathFromCollectionID(
function getRequestPathFromRequestID( function getRequestPathFromRequestID(
requestID: string, requestID: string,
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[], collections: HoppCollection[],
parentPath?: string parentPath?: string
): { collectionPath: string; requestIndex: number } | null { ): { collectionPath: string; requestIndex: number } | null {
for (const collectionIndex in collections) { for (const collectionIndex in collections) {
@@ -774,7 +807,7 @@ function getRequestPathFromRequestID(
function getRequestIndex( function getRequestIndex(
requestID: string, requestID: string,
parentCollectionPath: string, parentCollectionPath: string,
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[] collections: HoppCollection[]
) { ) {
const collection = navigateToFolderWithIndexPath( const collection = navigateToFolderWithIndexPath(
collections, collections,

View File

@@ -24,7 +24,7 @@ import {
editUserRequest, editUserRequest,
moveUserCollection, moveUserCollection,
moveUserRequest, moveUserRequest,
renameUserCollection, updateUserCollection,
updateUserCollectionOrder, updateUserCollectionOrder,
} from "./collections.api" } from "./collections.api"
@@ -39,7 +39,7 @@ export const restRequestsMapper = createMapper<string, string>()
// temp implementation untill the backend implements an endpoint that accepts an entire collection // temp implementation untill the backend implements an endpoint that accepts an entire collection
// TODO: use importCollectionsJSON to do this // TODO: use importCollectionsJSON to do this
const recursivelySyncCollections = async ( const recursivelySyncCollections = async (
collection: HoppCollection<HoppRESTRequest>, collection: HoppCollection,
collectionPath: string, collectionPath: string,
parentUserCollectionID?: string parentUserCollectionID?: string
) => { ) => {
@@ -47,27 +47,69 @@ const recursivelySyncCollections = async (
// if parentUserCollectionID does not exist, create the collection as a root collection // if parentUserCollectionID does not exist, create the collection as a root collection
if (!parentUserCollectionID) { if (!parentUserCollectionID) {
const res = await createRESTRootUserCollection(collection.name) const data = {
auth: collection.auth ?? {
authType: "inherit",
authActive: true,
},
headers: collection.headers ?? [],
}
const res = await createRESTRootUserCollection(
collection.name,
JSON.stringify(data)
)
if (E.isRight(res)) { if (E.isRight(res)) {
parentCollectionID = res.right.createRESTRootUserCollection.id parentCollectionID = res.right.createRESTRootUserCollection.id
const returnedData = res.right.createRESTRootUserCollection.data
? JSON.parse(res.right.createRESTRootUserCollection.data)
: {
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
}
collection.id = parentCollectionID collection.id = parentCollectionID
collection.auth = returnedData.auth
collection.headers = returnedData.headers
removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath) removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath)
} else { } else {
parentCollectionID = undefined parentCollectionID = undefined
} }
} else { } else {
// if parentUserCollectionID exists, create the collection as a child collection // if parentUserCollectionID exists, create the collection as a child collection
const data = {
auth: collection.auth ?? {
authType: "inherit",
authActive: true,
},
headers: collection.headers ?? [],
}
const res = await createRESTChildUserCollection( const res = await createRESTChildUserCollection(
collection.name, collection.name,
parentUserCollectionID parentUserCollectionID,
JSON.stringify(data)
) )
if (E.isRight(res)) { if (E.isRight(res)) {
const childCollectionId = res.right.createRESTChildUserCollection.id const childCollectionId = res.right.createRESTChildUserCollection.id
const returnedData = res.right.createRESTChildUserCollection.data
? JSON.parse(res.right.createRESTChildUserCollection.data)
: {
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
}
collection.id = childCollectionId collection.id = childCollectionId
collection.auth = returnedData.auth
collection.headers = returnedData.headers
removeDuplicateRESTCollectionOrFolder( removeDuplicateRESTCollectionOrFolder(
childCollectionId, childCollectionId,
@@ -155,8 +197,13 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
[collectionIndex] [collectionIndex]
)?.id )?.id
if (collectionID && collection.name) { const data = {
renameUserCollection(collectionID, collection.name) auth: collection.auth,
headers: collection.headers,
}
if (collectionID) {
updateUserCollection(collectionID, collection.name, JSON.stringify(data))
} }
}, },
async addFolder({ name, path }) { async addFolder({ name, path }) {
@@ -195,9 +242,12 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
)?.id )?.id
const folderName = folder.name const folderName = folder.name
const data = {
if (folderID && folderName) { auth: folder.auth,
renameUserCollection(folderID, folderName) headers: folder.headers,
}
if (folderID) {
updateUserCollection(folderID, folderName, JSON.stringify(data))
} }
}, },
async removeFolder({ folderID }) { async removeFolder({ folderID }) {

View File

@@ -21,7 +21,7 @@ import {
deleteUserCollection, deleteUserCollection,
deleteUserRequest, deleteUserRequest,
editGQLUserRequest, editGQLUserRequest,
renameUserCollection, updateUserCollection,
} from "./collections.api" } from "./collections.api"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
@@ -36,7 +36,7 @@ export const gqlRequestsMapper = createMapper<string, string>()
// temp implementation untill the backend implements an endpoint that accepts an entire collection // temp implementation untill the backend implements an endpoint that accepts an entire collection
// TODO: use importCollectionsJSON to do this // TODO: use importCollectionsJSON to do this
const recursivelySyncCollections = async ( const recursivelySyncCollections = async (
collection: HoppCollection<HoppRESTRequest>, collection: HoppCollection,
collectionPath: string, collectionPath: string,
parentUserCollectionID?: string parentUserCollectionID?: string
) => { ) => {
@@ -44,12 +44,35 @@ const recursivelySyncCollections = async (
// if parentUserCollectionID does not exist, create the collection as a root collection // if parentUserCollectionID does not exist, create the collection as a root collection
if (!parentUserCollectionID) { if (!parentUserCollectionID) {
const res = await createGQLRootUserCollection(collection.name) const data = {
auth: collection.auth ?? {
authType: "inherit",
authActive: true,
},
headers: collection.headers ?? [],
}
const res = await createGQLRootUserCollection(
collection.name,
JSON.stringify(data)
)
if (E.isRight(res)) { if (E.isRight(res)) {
parentCollectionID = res.right.createGQLRootUserCollection.id parentCollectionID = res.right.createGQLRootUserCollection.id
const returnedData = res.right.createGQLRootUserCollection.data
? JSON.parse(res.right.createGQLRootUserCollection.data)
: {
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
}
collection.id = parentCollectionID collection.id = parentCollectionID
collection.auth = returnedData.auth
collection.headers = returnedData.headers
removeDuplicateGraphqlCollectionOrFolder( removeDuplicateGraphqlCollectionOrFolder(
parentCollectionID, parentCollectionID,
collectionPath collectionPath
@@ -59,15 +82,37 @@ const recursivelySyncCollections = async (
} }
} else { } else {
// if parentUserCollectionID exists, create the collection as a child collection // if parentUserCollectionID exists, create the collection as a child collection
const data = {
auth: collection.auth ?? {
authType: "inherit",
authActive: true,
},
headers: collection.headers ?? [],
}
const res = await createGQLChildUserCollection( const res = await createGQLChildUserCollection(
collection.name, collection.name,
parentUserCollectionID parentUserCollectionID,
JSON.stringify(data)
) )
if (E.isRight(res)) { if (E.isRight(res)) {
const childCollectionId = res.right.createGQLChildUserCollection.id const childCollectionId = res.right.createGQLChildUserCollection.id
const returnedData = res.right.createGQLChildUserCollection.data
? JSON.parse(res.right.createGQLChildUserCollection.data)
: {
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
}
collection.id = childCollectionId collection.id = childCollectionId
collection.auth = returnedData.auth
collection.headers = returnedData.headers
removeDuplicateGraphqlCollectionOrFolder( removeDuplicateGraphqlCollectionOrFolder(
childCollectionId, childCollectionId,
@@ -158,8 +203,13 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
[collectionIndex] [collectionIndex]
)?.id )?.id
if (collectionID && collection.name) { const data = {
renameUserCollection(collectionID, collection.name) auth: collection.auth,
headers: collection.headers,
}
if (collectionID) {
updateUserCollection(collectionID, collection.name, JSON.stringify(data))
} }
}, },
async addFolder({ name, path }) { async addFolder({ name, path }) {
@@ -197,8 +247,13 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
path.split("/").map((index) => parseInt(index)) path.split("/").map((index) => parseInt(index))
)?.id )?.id
if (folderBackendId && folder.name) { const data = {
renameUserCollection(folderBackendId, folder.name) auth: folder.auth,
headers: folder.headers,
}
if (folderBackendId) {
updateUserCollection(folderBackendId, folder.name, JSON.stringify(data))
} }
}, },
async removeFolder({ folderID }) { async removeFolder({ folderID }) {

View File

@@ -22,6 +22,7 @@
--lower-tertiary-sticky-fold: 7.125rem; --lower-tertiary-sticky-fold: 7.125rem;
--lower-fourth-sticky-fold: 9.188rem; --lower-fourth-sticky-fold: 9.188rem;
--sidebar-primary-sticky-fold: 2rem; --sidebar-primary-sticky-fold: 2rem;
--properties-primary-sticky-fold: 2.05rem;
} }
@mixin dark-theme { @mixin dark-theme {

View File

@@ -59,7 +59,7 @@
</div> </div>
<div <div
class="flex flex-col overflow-y-auto max-h-[55vh]" class="flex flex-col overflow-y-auto max-h-[55vh]"
:class="{ 'p-4': !fullWidth }" :class="{ 'p-4': !fullWidth && !fullWidthBody }"
> >
<slot name="body"></slot> <slot name="body"></slot>
</div> </div>
@@ -117,6 +117,7 @@ withDefaults(
dimissible: boolean dimissible: boolean
placement: string placement: string
fullWidth: boolean fullWidth: boolean
fullWidthBody: boolean
styles: string styles: string
closeText: string | null closeText: string | null
}>(), }>(),
@@ -126,6 +127,7 @@ withDefaults(
dimissible: true, dimissible: true,
placement: "top", placement: "top",
fullWidth: false, fullWidth: false,
fullWidthBody: false,
styles: "sm:max-w-lg", styles: "sm:max-w-lg",
closeText: null, closeText: null,
} }