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";
export type CollectionRunnerParam = {
collections: HoppCollection<HoppRESTRequest>[];
collections: HoppCollection[];
envs: HoppEnvs;
delay?: number;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,6 +33,7 @@
"open_workspace": "Open workspace",
"paste": "Paste",
"prettify": "Prettify",
"properties":"Properties",
"remove": "Remove",
"rename": "Rename",
"restore": "Restore",
@@ -172,6 +173,8 @@
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "New Collection",
"order_changed": "Collection Order Updated",
"properties":"Colection Properties",
"properties_updated": "Collection Properties Updated",
"renamed": "Collection renamed",
"request_in_use": "Request in use",
"save_as": "Save as",
@@ -354,6 +357,8 @@
"offline_short": "You're using Hoppscotch offline.",
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
"collection_properties_header": "This header will be set for every request in this collection.",
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
"tests": "Write a test script to automate debugging."

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -71,6 +71,13 @@
collection: node.data.data.data,
})
"
@edit-properties="
node.data.type === 'collections' &&
emit('edit-properties', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data="
node.data.type === 'collections' &&
emit('export-data', node.data.data.data)
@@ -139,6 +146,13 @@
folder: node.data.data.data,
})
"
@edit-properties="
node.data.type === 'folders' &&
emit('edit-properties', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data="
node.data.type === 'folders' &&
emit('export-data', node.data.data.data)
@@ -344,7 +358,7 @@ export type Collection = {
isLastItem: boolean
data: {
parentIndex: null
data: HoppCollection<HoppRESTRequest>
data: HoppCollection
}
}
@@ -353,7 +367,7 @@ type Folder = {
isLastItem: boolean
data: {
parentIndex: string
data: HoppCollection<HoppRESTRequest>
data: HoppCollection
}
}
@@ -380,7 +394,7 @@ type CollectionType =
const props = defineProps({
filteredCollections: {
type: Array as PropType<HoppCollection<HoppRESTRequest>[]>,
type: Array as PropType<HoppCollection[]>,
default: () => [],
required: true,
},
@@ -412,28 +426,35 @@ const emit = defineEmits<{
event: "add-request",
payload: {
path: string
folder: HoppCollection<HoppRESTRequest>
folder: HoppCollection
}
): void
(
event: "add-folder",
payload: {
path: string
folder: HoppCollection<HoppRESTRequest>
folder: HoppCollection
}
): void
(
event: "edit-collection",
payload: {
collectionIndex: string
collection: HoppCollection<HoppRESTRequest>
collection: HoppCollection
}
): void
(
event: "edit-folder",
payload: {
folderPath: string
folder: HoppCollection<HoppRESTRequest>
folder: HoppCollection
}
): void
(
event: "edit-properties",
payload: {
collectionIndex: string
collection: HoppCollection
}
): void
(
@@ -451,7 +472,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
}
): void
(event: "export-data", payload: HoppCollection<HoppRESTRequest>): void
(event: "export-data", payload: HoppCollection): void
(event: "remove-collection", payload: string): void
(event: "remove-folder", payload: string): void
(
@@ -665,10 +686,10 @@ const updateCollectionOrder = (
type MyCollectionNode = Collection | Folder | Requests
class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
constructor(public data: Ref<HoppCollection<HoppRESTRequest>[]>) {}
constructor(public data: Ref<HoppCollection[]>) {}
navigateToFolderWithIndexPath(
collections: HoppCollection<HoppRESTRequest>[],
collections: HoppCollection[],
indexPaths: number[]
) {
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 { useToast } from "@composables/toast"
import {
cascadeParentCollectionForHeaderAuth,
editGraphqlRequest,
editRESTRequest,
saveGraphqlRequestAs,
@@ -239,6 +240,16 @@ const saveRequestAs = async () => {
},
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${picked.value.collectionIndex}`,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
@@ -266,6 +277,16 @@ const saveRequestAs = async () => {
},
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
@@ -294,6 +315,16 @@ const saveRequestAs = async () => {
},
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
@@ -378,6 +409,16 @@ const saveRequestAs = async () => {
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved()
} else if (picked.value.pickedType === "gql-my-folder") {
// TODO: Check for GQL request ?
@@ -393,6 +434,16 @@ const saveRequestAs = async () => {
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved()
} else if (picked.value.pickedType === "gql-my-collection") {
// TODO: Check for GQL request ?
@@ -408,6 +459,16 @@ const saveRequestAs = async () => {
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${picked.value.collectionIndex}`,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved()
}
}

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
v-if="show"
dialog
:title="t('folder.new')"
@close="$emit('hide-modal')"
@close="hideModal"
>
<template #body>
<HoppSmartInput
@@ -32,47 +32,49 @@
</HoppSmartModal>
</template>
<script lang="ts">
<script setup lang="ts">
import { ref } from "vue"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { defineComponent } from "vue"
export default defineComponent({
props: {
show: Boolean,
folderPath: { type: String, default: null },
collectionIndex: { type: Number, default: null },
},
emits: ["hide-modal", "add-folder"],
setup() {
return {
toast: useToast(),
t: useI18n(),
const t = useI18n()
const toast = useToast()
const props = defineProps<{
show: boolean
folderPath?: string
collectionIndex: number
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
(
e: "add-folder",
v: {
name: string
path: string | undefined
}
},
data() {
return {
name: null,
}
},
methods: {
addFolder() {
if (!this.name) {
this.toast.error(`${this.t("folder.name_length_insufficient")}`)
return
}
): void
}>()
this.$emit("add-folder", {
name: this.name,
path: this.folderPath || `${this.collectionIndex}`,
})
const name = ref<string | null>(null)
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
const addFolder = () => {
if (!name.value) {
toast.error(`${t("folder.name_length_insufficient")}`)
return
}
emit("add-folder", {
name: name.value,
path: props.folderPath || `${props.collectionIndex}`,
})
hideModal()
}
const hideModal = () => {
name.value = null
emit("hide-modal")
}
</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>
</template>
</tippy>
@@ -155,7 +170,15 @@
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@edit-properties="
$emit('edit-properties', {
collectionIndex: `${collectionIndex}/${String(index)}`,
collection: folder,
})
"
@select="$emit('select', $event)"
@select-request="$emit('select-request', $event)"
@drop-request="$emit('drop-request', $event)"
/>
<CollectionsGraphqlRequest
v-for="(request, index) in collection.requests"
@@ -171,6 +194,7 @@
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@select="$emit('select', $event)"
@select-request="$emit('select-request', $event)"
/>
<HoppSmartPlaceholder
v-if="
@@ -214,25 +238,24 @@ import IconFolderPlus from "~icons/lucide/folder-plus"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconEdit from "~icons/lucide/edit"
import IconTrash2 from "~icons/lucide/trash-2"
import IconSettings2 from "~icons/lucide/settings-2"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import {
removeGraphqlCollection,
moveGraphqlRequest,
} from "~/newstore/collections"
import { removeGraphqlCollection } from "~/newstore/collections"
import { Picked } from "~/helpers/types/HoppPicked"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
import { HoppCollection } from "@hoppscotch/data"
const props = defineProps({
picked: { type: Object, default: null },
const props = defineProps<{
picked: Picked | null
// Whether the viewing context is related to picking (activates 'select' events)
saveRequest: { type: Boolean, default: false },
collectionIndex: { type: Number, default: null },
collection: { type: Object, default: () => ({}) },
isFiltered: Boolean,
})
saveRequest: boolean
collectionIndex: number | null
collection: HoppCollection
isFiltered: boolean
}>()
const colorMode = useColorMode()
const toast = useToast()
@@ -248,7 +271,23 @@ const emit = defineEmits<{
(e: "add-request", i: any): void
(e: "add-folder", i: any): void
(e: "edit-folder", i: any): void
(
e: "edit-properties",
payload: {
collectionIndex: string | null
collection: HoppCollection
}
): void
(e: "edit-collection"): void
(e: "select-request", i: any): void
(
e: "drop-request",
payload: {
folderPath: string
requestIndex: string
collectionIndex: number | null
}
): void
}>()
// Template refs
@@ -324,6 +363,10 @@ const dropEvent = ({ dataTransfer }: any) => {
dragging.value = !dragging.value
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
moveGraphqlRequest(folderPath, requestIndex, `${props.collectionIndex}`)
emit("drop-request", {
folderPath,
requestIndex,
collectionIndex: props.collectionIndex,
})
}
</script>

View File

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

View File

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

View File

@@ -32,61 +32,55 @@
</HoppSmartModal>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue"
<script setup lang="ts">
import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { HoppGQLRequest } from "@hoppscotch/data"
import { editGraphqlRequest } from "~/newstore/collections"
export default defineComponent({
props: {
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
}
const t = useI18n()
const toast = useToast()
// TODO: Type safety goes brrrr. Proper typing plz
const requestUpdated = {
...this.$props.request,
name: this.$data.requestUpdateData.name || this.$props.request.name,
}
const props = defineProps<{
show: boolean
folderPath?: string
requestIndex: number | null
request: HoppGQLRequest | null
editingRequestName: string
}>()
editGraphqlRequest(this.folderPath, this.requestIndex, requestUpdated)
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
this.hideModal()
},
hideModal() {
this.requestUpdateData = { name: null }
this.$emit("hide-modal")
},
},
})
const requestUpdateData = ref({ name: null as string | null })
watch(
() => props.editingRequestName,
(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>

View File

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

View File

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

View File

@@ -9,38 +9,41 @@
@dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()"
>
<span
class="flex w-16 cursor-pointer items-center justify-center truncate px-2"
<div
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
@click="selectRequest()"
>
<component
:is="isSelected ? IconCheckCircle : IconFile"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
</span>
<span
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
class="pointer-events-none flex w-8 items-center justify-center truncate px-6"
>
<component
:is="isSelected ? IconCheckCircle : IconFile"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
</span>
<span
v-if="isActive"
v-tippy="{ theme: 'tooltip' }"
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
:title="`${t('collection.request_in_use')}`"
class="pointer-events-none flex min-w-0 flex-1 items-center py-2 pr-2 transition group-hover:text-secondaryDark"
>
<span
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }}
</span>
<span
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
></span>
v-if="isActive"
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>
</div>
<div class="flex">
<span>
<tippy
@@ -134,8 +137,7 @@ import IconTrash2 from "~icons/lucide/trash-2"
import { PropType, computed, ref } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es"
import { HoppGQLRequest } from "@hoppscotch/data"
import { removeGraphqlRequest } from "~/newstore/collections"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
@@ -175,7 +177,12 @@ const isActive = computed(() => {
})
// 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 confirmRemove = ref(false)
@@ -199,36 +206,11 @@ const selectRequest = () => {
if (props.saveRequest) {
pick()
} else {
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
emit("select-request", {
request: props.request,
folderPath: props.folderPath,
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)"
@duplicate-request="duplicateRequest($event)"
@select-collection="$emit('use-collection', collection)"
@edit-properties="editProperties($event)"
@select="$emit('select', $event)"
@select-request="selectRequest($event)"
@drop-request="dropRequest($event)"
/>
</div>
<HoppSmartPlaceholder
@@ -142,19 +145,27 @@
v-if="showModalImportExport"
@hide-modal="displayModalImportExport(false)"
/>
<CollectionsProperties
:show="showModalEditProperties"
:editing-properties="editingProperties"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
/>
</div>
</template>
<script lang="ts">
// TODO: TypeScript + Script Setup this :)
import { defineComponent } from "vue"
import { cloneDeep, clone } from "lodash-es"
<script setup lang="ts">
import { nextTick, ref } from "vue"
import { clone, cloneDeep } from "lodash-es"
import {
graphqlCollections$,
addGraphqlFolder,
saveGraphqlRequestAs,
cascadeParentCollectionForHeaderAuth,
editGraphqlCollection,
editGraphqlFolder,
moveGraphqlRequest,
} from "~/newstore/collections"
import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down"
@@ -164,213 +175,448 @@ import { useColorMode } from "@composables/theming"
import { platform } from "~/platform"
import { useService } from "dioc/vue"
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({
props: {
// 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)
const t = useI18n()
const toast = useToast()
return {
collections,
colorMode,
t,
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)
defineProps<{
// Whether to activate the ability to pick items (activates 'select' events)
saveRequest: boolean
picked: Picked
}>()
if (!this.filterText) return collections
const collections = useReadonlyStream(graphqlCollections$, [], "deep")
const colorMode = useColorMode()
const tabs = useService(GQLTabService)
const filterText = this.filterText.toLowerCase()
const filteredCollections = []
const showModalAdd = ref(false)
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 filteredRequests = []
const filteredFolders = []
for (const request of collection.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredRequests.push(request)
}
for (const folder of collection.folders) {
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)
}
}
const editingCollection = ref<HoppCollection | null>(null)
const editingCollectionIndex = ref<number | null>(null)
const editingFolder = ref<HoppCollection | null>(null)
const editingFolderName = ref("")
const editingFolderIndex = ref<number | null>(null)
const editingFolderPath = ref("")
const editingRequest = ref<HoppGQLRequest | null>(null)
const editingRequestIndex = ref<number | null>(null)
if (filteredRequests.length + filteredFolders.length > 0) {
const filteredCollection = Object.assign({}, collection)
filteredCollection.requests = filteredRequests
filteredCollection.folders = filteredFolders
filteredCollections.push(filteredCollection)
}
}
return filteredCollections
},
},
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 editingProperties = ref<{
collection: HoppCollection | null
isRootCollection: boolean
path: string
inheritedProperties?: HoppInheritedProperty
}>({
collection: null,
isRootCollection: false,
path: "",
inheritedProperties: undefined,
})
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>

View File

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

View File

@@ -77,22 +77,11 @@
tabindex="-1"
/>
</span>
<HoppSmartAutoComplete
<SmartEnvInput
v-model="header.key"
:placeholder="`${t('count.header', { count: index + 1 })}`"
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
styles="
bg-transparent
flex
flex-1
py-1
px-4
truncate
"
class="!flex flex-1"
@input="
:auto-complete-source="commonHeaders"
@change="
updateHeader(index, {
id: header.id,
key: $event,
@@ -101,17 +90,14 @@
})
"
/>
<input
class="flex flex-1 bg-transparent px-4 py-2"
<SmartEnvInput
v-model="header.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"
autofocus
@change="
updateHeader(index, {
id: header.id,
key: header.key,
value: ($event!.target! as HTMLInputElement).value,
value: $event,
active: header.active,
})
"
@@ -156,6 +142,119 @@
</div>
</template>
</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
v-if="workingHeaders.length === 0"
: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 IconCircle from "~icons/lucide/circle"
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 O from "fp-ts/Option"
import * as A from "fp-ts/Array"
@@ -206,13 +310,20 @@ import { commonHeaders } from "~/helpers/headers"
import { useCodemirror } from "@composables/codemirror"
import { objRemoveKey } from "~/helpers/functional/object"
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 t = useI18n()
const toast = useToast()
// v-model integration with props and emit
const props = defineProps<{ modelValue: HoppGQLRequest }>()
const props = defineProps<{
modelValue: HoppGQLRequest
isCollectionProperty?: boolean
inheritedProperties?: HoppInheritedProperty
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: HoppGQLRequest): void
@@ -413,7 +524,11 @@ const deleteHeader = (index: number) => {
})
}
workingHeaders.value.splice(index, 1)
workingHeaders.value = pipe(
workingHeaders.value,
A.deleteAt(index),
O.getOrElseW(() => throwError("Working Headers Deletion Out of Bounds"))
)
}
const clearContent = () => {
@@ -429,4 +544,151 @@ const clearContent = () => {
bulkHeaders.value = ""
}
const getComputedAuthHeaders = (
req?: HoppGQLRequest,
auth?: HoppGQLRequest["auth"]
) => {
const request = auth ? { auth: auth ?? { authActive: false } } : req
// If Authorization header is also being user-defined, that takes priority
if (req && req.headers.find((h) => h.key.toLowerCase() === "authorization"))
return []
if (!request) return []
if (!request.auth || !request.auth.authActive) return []
const headers: HoppGQLHeader[] = []
// TODO: Support a better b64 implementation than btoa ?
if (request.auth.authType === "basic") {
const username = request.auth.username
const password = request.auth.password
headers.push({
active: true,
key: "Authorization",
value: `Basic ${btoa(`${username}:${password}`)}`,
})
} else if (
request.auth.authType === "bearer" ||
request.auth.authType === "oauth-2"
) {
headers.push({
active: true,
key: "Authorization",
value: `Bearer ${request.auth.token}`,
})
} else if (request.auth.authType === "api-key") {
const { key, addTo } = request.auth
if (addTo === "Headers" && key) {
headers.push({
active: true,
key,
value: request.auth.value ?? "",
})
}
}
return headers
}
const getComputedHeaders = (req: HoppGQLRequest) => {
return [
...getComputedAuthHeaders(req).map((header) => ({
source: "auth" as const,
header,
})),
]
}
const computedHeaders = computed(() =>
getComputedHeaders(request.value).map((header, index) => ({
id: `header-${index}`,
...header,
}))
)
const inheritedProperties = computed(() => {
if (!props.inheritedProperties?.auth || !props.inheritedProperties.headers)
return []
//filter out headers that are already in the request headers
const inheritedHeaders = props.inheritedProperties.headers.filter(
(header) =>
!request.value.headers.some(
(requestHeader) => requestHeader.key === header.inheritedHeader?.key
)
)
const headers = inheritedHeaders
.filter(
(header) =>
header.inheritedHeader !== null &&
header.inheritedHeader !== undefined &&
header.inheritedHeader.active
)
.map((header, index) => ({
inheritedFrom: props.inheritedProperties?.headers[index].parentName,
source: "headers",
id: `header-${index}`,
header: {
key: header.inheritedHeader?.key,
value: header.inheritedHeader?.value,
active: header.inheritedHeader?.active,
},
}))
let auth = [] as {
inheritedFrom: string
source: "auth"
id: string
header: {
key: string
value: string
active: boolean
}
}[]
const computedAuthHeader = getComputedAuthHeaders(
request.value,
props.inheritedProperties.auth.inheritedAuth
)[0]
if (
computedAuthHeader &&
request.value.auth.authType === "inherit" &&
request.value.auth.authActive
) {
auth = [
{
inheritedFrom: props.inheritedProperties?.auth.parentName,
source: "auth",
id: `header-auth`,
header: computedAuthHeader,
},
]
}
return [...headers, ...auth]
})
const masking = ref(true)
const toggleMask = () => {
masking.value = !masking.value
}
const mask = (header: any) => {
if (header.source === "auth" && masking.value)
return header.header.value.replace(/\S/gi, "*")
return header.header.value
}
// const changeTab = (tab: ComputedHeader["source"]) => {
// if (tab === "auth") emit("change-tab", "authorization")
// else emit("change-tab", "bodyParams")
// }
</script>

View File

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

View File

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

View File

@@ -1,7 +1,12 @@
<template>
<div class="flex flex-1 flex-col">
<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">
<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
label="Basic Auth"
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
@@ -135,6 +152,17 @@
<div v-if="auth.authType === 'basic'">
<HttpAuthorizationBasic v-model="auth" />
</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 class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.token" placeholder="Token" />
@@ -181,6 +209,8 @@ import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
import { onMounted } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n()
@@ -188,6 +218,9 @@ const colorMode = useColorMode()
const props = defineProps<{
modelValue: HoppRESTAuth
isCollectionProperty?: boolean
isRootCollection?: boolean
inheritedProperties?: HoppInheritedProperty
}>()
const emit = defineEmits<{
@@ -196,18 +229,34 @@ const emit = defineEmits<{
const auth = useVModel(props, "modelValue", emit)
onMounted(() => {
if (props.isRootCollection && auth.value.authType === "inherit") {
auth.value = {
authType: "none",
authActive: true,
}
}
})
const AUTH_KEY_NAME = {
basic: "Basic Auth",
bearer: "Bearer",
"oauth-2": "OAuth 2.0",
"api-key": "API key",
none: "None",
inherit: "Inherit",
} as const
const authType = pluckRef(auth, "authType")
const authName = computed(() =>
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
)
const getAuthName = (type: HoppRESTAuth["authType"] | undefined) => {
if (!type) return "None"
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
}
const authActive = pluckRef(auth, "authActive")
const clearContent = () => {

View File

@@ -1,7 +1,12 @@
<template>
<div class="flex flex-1 flex-col">
<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">
{{ t("request.header_list") }}
@@ -203,6 +208,61 @@
</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
v-if="workingHeaders.length === 0"
: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 IconArrowUpRight from "~icons/lucide/arrow-up-right"
import IconWrapText from "~icons/lucide/wrap-text"
import IconInfo from "~icons/lucide/info"
import { useColorMode } from "@composables/theming"
import { computed, reactive, ref, watch } from "vue"
import { isEqual, cloneDeep } from "lodash-es"
@@ -264,12 +325,14 @@ import { objRemoveKey } from "~/helpers/functional/object"
import {
ComputedHeader,
getComputedHeaders,
getComputedAuthHeaders,
} from "~/helpers/utils/EffectiveURL"
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n()
const toast = useToast()
@@ -288,7 +351,11 @@ const linewrapEnabled = ref(true)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
// v-model integration with props and emit
const props = defineProps<{ modelValue: HoppRESTRequest }>()
const props = defineProps<{
modelValue: HoppRESTRequest
isCollectionProperty?: boolean
inheritedProperties?: HoppInheritedProperty
}>()
const emit = defineEmits<{
(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 toggleMask = () => {

View File

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

View File

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

View File

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

View File

@@ -95,13 +95,41 @@ export function runRESTRequest$(
return E.left("script_fail" as const)
}
const effectiveRequest = getEffectiveRESTRequest(
tab.value.document.request,
{
name: "Env",
variables: combineEnvVariables(envs.right),
}
)
const requestAuth =
tab.value.document.request.auth.authType === "inherit" &&
tab.value.document.request.auth.authActive
? tab.value.document.inheritedProperties?.auth.inheritedAuth
: 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)
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) {
id
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) {
id
title
data
parent {
id
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,9 @@ import {
UpdateCollectionOrderDocument,
UpdateCollectionOrderMutation,
UpdateCollectionOrderMutationVariables,
UpdateTeamCollectionDocument,
UpdateTeamCollectionMutation,
UpdateTeamCollectionMutationVariables,
} from "../graphql"
type CreateNewRootCollectionError = "team_coll/short_title"
@@ -122,3 +125,18 @@ export const importJSONToTeam = (collectionJSON: string, teamID: string) =>
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 { GetSingleRequestDocument } from "../backend/graphql"
import { runGQLQuery } from "../backend/GQLClient"
import * as E from "fp-ts/Either"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
import { GQLTabService } from "~/services/tab/graphql"
/**
* 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) {
const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
@@ -152,9 +283,9 @@ export async function resetTeamRequestsContext() {
}
export function getFoldersByPath(
collections: HoppCollection<HoppRESTRequest>[],
collections: HoppCollection[],
path: string
): HoppCollection<HoppRESTRequest>[] {
): HoppCollection[] {
if (!path) return collections
// 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 { RESTTabService } from "~/services/tab/rest"
import { getService } from "~/modules/dioc"
@@ -53,9 +57,9 @@ export function resolveSaveContextOnRequestReorder(payload: {
}
export function getRequestsByPath(
collections: HoppCollection<HoppRESTRequest>[],
collections: HoppCollection[],
path: string
): HoppRESTRequest[] {
): HoppRESTRequest[] | HoppGQLRequest[] {
// path will be like this "0/0/1" these are the indexes of the folders
const pathArray = path.split("/").map((index) => parseInt(index))

View File

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

View File

@@ -1,6 +1,7 @@
import { HoppGQLRequest } from "@hoppscotch/data"
import { GQLResponseEvent } from "./connection"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
export type HoppGQLSaveContext =
| {
@@ -73,4 +74,10 @@ export type HoppGQLDocument = {
* Options tab preference for the current tab's document
*/
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 = (
gqlCollections: HoppCollection<HoppGQLRequest>[]
) => {
export const gqlCollectionsExporter = (gqlCollections: HoppCollection[]) => {
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 = (
myCollections: HoppCollection<HoppRESTRequest>[]
) => {
export const myCollectionsExporter = (myCollections: HoppCollection[]) => {
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 O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import {
translateToNewRESTCollection,
HoppCollection,
HoppRESTRequest,
} from "@hoppscotch/data"
import { translateToNewRESTCollection, HoppCollection } from "@hoppscotch/data"
import { isPlainObject as _isPlainObject } from "lodash-es"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { safeParseJSON } from "~/helpers/functional/json"
import { translateToNewGQLCollection } from "@hoppscotch/data"
export const hoppRESTImporter = (content: string) =>
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.
* 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 = (
collection: unknown
): collection is HoppCollection<HoppRESTRequest> =>
isPlainObject(collection) && "v" in collection
const isValidCollection = (collection: unknown): collection is HoppCollection =>
isPlainObject(collection) && "v" in collection && collection.v === 2
/**
* checks if a collection is a valid hoppscotch collection.
@@ -56,3 +51,29 @@ const validateCollection = (collection: unknown) => {
*/
const makeCollectionsArray = (collections: unknown | unknown[]): unknown[] =>
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"
// TODO: add zod validation
export const hoppGqlCollectionsImporter = (
content: string
): E.Either<"INVALID_JSON", HoppCollection<HoppGQLRequest>[]> => {
): E.Either<"INVALID_JSON", HoppCollection[]> => {
return E.tryCatch(
() => JSON.parse(content) as HoppCollection<HoppGQLRequest>[],
() => JSON.parse(content) as HoppCollection[],
() => "INVALID_JSON"
)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,10 @@
import * as E from "fp-ts/Either"
import { BehaviorSubject, Subscription } from "rxjs"
import { translateToNewRequest } from "@hoppscotch/data"
import {
HoppRESTAuth,
HoppRESTHeader,
translateToNewRequest,
} from "@hoppscotch/data"
import { pull, remove } from "lodash-es"
import { Subscription as WSubscription } from "wonka"
import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
@@ -21,6 +25,7 @@ import {
TeamRequestOrderUpdatedDocument,
TeamCollectionOrderUpdatedDocument,
} from "~/helpers/backend/graphql"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
const TEAMS_BACKEND_PAGE_SIZE = 10
@@ -542,6 +547,7 @@ export default class NewTeamCollectionAdapter {
children: null,
requests: null,
title: title,
data: null,
},
parentID ?? null
)
@@ -693,6 +699,7 @@ export default class NewTeamCollectionAdapter {
children: null,
requests: null,
title: result.right.teamCollectionAdded.title,
data: result.right.teamCollectionAdded.data ?? null,
},
result.right.teamCollectionAdded.parent?.id ?? null
)
@@ -715,6 +722,7 @@ export default class NewTeamCollectionAdapter {
this.updateCollection({
id: result.right.teamCollectionUpdated.id,
title: result.right.teamCollectionUpdated.title,
data: result.right.teamCollectionUpdated.data,
})
})
@@ -931,6 +939,7 @@ export default class NewTeamCollectionAdapter {
<TeamCollection>{
id: el.id,
title: el.title,
data: el.data,
children: 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
* @returns The list of headers
*/
const getComputedAuthHeaders = (
req: HoppRESTRequest,
envVars: Environment["variables"]
export const getComputedAuthHeaders = (
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 (req.headers.find((h) => h.key.toLowerCase() === "authorization"))
if (req && req.headers.find((h) => h.key.toLowerCase() === "authorization"))
return []
if (!req.auth.authActive) return []
if (!request) return []
if (!request.auth || !request.auth.authActive) return []
const headers: HoppRESTHeader[] = []
// TODO: Support a better b64 implementation than btoa ?
if (req.auth.authType === "basic") {
const username = parseTemplateString(req.auth.username, envVars)
const password = parseTemplateString(req.auth.password, envVars)
if (request.auth.authType === "basic") {
const username = parseTemplateString(request.auth.username, envVars)
const password = parseTemplateString(request.auth.password, envVars)
headers.push({
active: true,
@@ -65,22 +69,21 @@ const getComputedAuthHeaders = (
value: `Basic ${btoa(`${username}:${password}`)}`,
})
} else if (
req.auth.authType === "bearer" ||
req.auth.authType === "oauth-2"
request.auth.authType === "bearer" ||
request.auth.authType === "oauth-2"
) {
headers.push({
active: true,
key: "Authorization",
value: `Bearer ${parseTemplateString(req.auth.token, envVars)}`,
value: `Bearer ${parseTemplateString(request.auth.token, envVars)}`,
})
} else if (req.auth.authType === "api-key") {
const { key, value, addTo } = req.auth
if (addTo === "Headers") {
} else if (request.auth.authType === "api-key") {
const { key, addTo } = request.auth
if (addTo === "Headers" && key) {
headers.push({
active: true,
key: parseTemplateString(key, envVars),
value: parseTemplateString(value, envVars),
value: parseTemplateString(request.auth.value ?? "", envVars),
})
}
}
@@ -131,16 +134,18 @@ export type ComputedHeader = {
export const getComputedHeaders = (
req: HoppRESTRequest,
envVars: Environment["variables"]
): ComputedHeader[] => [
...getComputedAuthHeaders(req, envVars).map((header) => ({
source: "auth" as const,
header,
})),
...getComputedBodyHeaders(req).map((header) => ({
source: "body" as const,
header,
})),
]
): ComputedHeader[] => {
return [
...getComputedAuthHeaders(envVars, req).map((header) => ({
source: "auth" as const,
header,
})),
...getComputedBodyHeaders(req).map((header) => ({
source: "body" as const,
header,
})),
]
}
export type ComputedParam = {
source: "auth"
@@ -160,7 +165,7 @@ export const getComputedParams = (
): ComputedParam[] => {
// When this gets complex, its best to split this function off (like with getComputedHeaders)
// 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.addTo !== "Query params") return []

View File

@@ -10,23 +10,34 @@ import { cloneDeep } from "lodash-es"
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const defaultRESTCollectionState = {
state: [
makeCollection<HoppRESTRequest>({
makeCollection({
name: "My Collection",
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: false,
},
headers: [],
}),
],
}
const defaultGraphqlCollectionState = {
state: [
makeCollection<HoppGQLRequest>({
makeCollection({
name: "My GraphQL Collection",
folders: [],
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.
*/
export function navigateToFolderWithIndexPath(
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[],
collections: HoppCollection[],
indexPaths: number[]
) {
if (indexPaths.length === 0) return null
@@ -52,6 +63,94 @@ export function navigateToFolderWithIndexPath(
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) {
const item = array.splice(from, 1)[0]
if (from < to) {
@@ -64,7 +163,7 @@ function reorderItems(array: unknown[], from: number, to: number) {
const restCollectionDispatchers = defineDispatchers({
setCollections(
_: RESTCollectionStoreType,
{ entries }: { entries: HoppCollection<HoppRESTRequest>[] }
{ entries }: { entries: HoppCollection[] }
) {
return {
state: entries,
@@ -73,7 +172,7 @@ const restCollectionDispatchers = defineDispatchers({
appendCollections(
{ state }: RESTCollectionStoreType,
{ entries }: { entries: HoppCollection<HoppRESTRequest>[] }
{ entries }: { entries: HoppCollection[] }
) {
return {
state: [...state, ...entries],
@@ -82,7 +181,7 @@ const restCollectionDispatchers = defineDispatchers({
addCollection(
{ state }: RESTCollectionStoreType,
{ collection }: { collection: HoppCollection<any> }
{ collection }: { collection: HoppCollection }
) {
return {
state: [...state, collection],
@@ -112,7 +211,7 @@ const restCollectionDispatchers = defineDispatchers({
partialCollection,
}: {
collectionIndex: number
partialCollection: Partial<HoppCollection<any>>
partialCollection: Partial<HoppCollection>
}
) {
return {
@@ -128,10 +227,15 @@ const restCollectionDispatchers = defineDispatchers({
{ state }: RESTCollectionStoreType,
{ name, path }: { name: string; path: string }
) {
const newFolder: HoppCollection<HoppRESTRequest> = makeCollection({
const newFolder: HoppCollection = makeCollection({
name,
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
})
const newState = state
@@ -158,7 +262,7 @@ const restCollectionDispatchers = defineDispatchers({
folder,
}: {
path: string
folder: Partial<HoppCollection<HoppRESTRequest>>
folder: Partial<HoppCollection>
}
) {
const newState = state
@@ -249,7 +353,7 @@ const restCollectionDispatchers = defineDispatchers({
}
const theFolder = containingFolder.folders.splice(folderIndex, 1)
newState.push(theFolder[0] as HoppCollection<HoppRESTRequest>)
newState.push(theFolder[0] as HoppCollection)
return {
state: newState,
@@ -612,7 +716,7 @@ const restCollectionDispatchers = defineDispatchers({
type: "collection" | "request"
}
) {
const after = removeDuplicateCollectionsFromPath<HoppRESTRequest>(
const after = removeDuplicateCollectionsFromPath(
id,
collectionPath,
state,
@@ -628,7 +732,7 @@ const restCollectionDispatchers = defineDispatchers({
const gqlCollectionDispatchers = defineDispatchers({
setCollections(
_: GraphqlCollectionStoreType,
{ entries }: { entries: HoppCollection<any>[] }
{ entries }: { entries: HoppCollection[] }
) {
return {
state: entries,
@@ -637,7 +741,7 @@ const gqlCollectionDispatchers = defineDispatchers({
appendCollections(
{ state }: GraphqlCollectionStoreType,
{ entries }: { entries: HoppCollection<any>[] }
{ entries }: { entries: HoppCollection[] }
) {
return {
state: [...state, ...entries],
@@ -646,7 +750,7 @@ const gqlCollectionDispatchers = defineDispatchers({
addCollection(
{ state }: GraphqlCollectionStoreType,
{ collection }: { collection: HoppCollection<any> }
{ collection }: { collection: HoppCollection }
) {
return {
state: [...state, collection],
@@ -673,7 +777,7 @@ const gqlCollectionDispatchers = defineDispatchers({
{
collectionIndex,
collection,
}: { collectionIndex: number; collection: Partial<HoppCollection<any>> }
}: { collectionIndex: number; collection: Partial<HoppCollection> }
) {
return {
state: state.map((col, index) =>
@@ -686,12 +790,16 @@ const gqlCollectionDispatchers = defineDispatchers({
{ state }: GraphqlCollectionStoreType,
{ name, path }: { name: string; path: string }
) {
const newFolder: HoppCollection<HoppGQLRequest> = makeCollection({
const newFolder: HoppCollection = makeCollection({
name,
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
})
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
@@ -711,10 +819,7 @@ const gqlCollectionDispatchers = defineDispatchers({
editFolder(
{ state }: GraphqlCollectionStoreType,
{
path,
folder,
}: { path: string; folder: Partial<HoppCollection<HoppGQLRequest>> }
{ path, folder }: { path: string; folder: Partial<HoppCollection> }
) {
const newState = state
@@ -913,7 +1018,7 @@ const gqlCollectionDispatchers = defineDispatchers({
type: "collection" | "request"
}
) {
const after = removeDuplicateCollectionsFromPath<HoppGQLRequest>(
const after = removeDuplicateCollectionsFromPath(
id,
collectionPath,
state,
@@ -936,7 +1041,7 @@ export const graphqlCollectionStore = new DispatchingStore(
gqlCollectionDispatchers
)
export function setRESTCollections(entries: HoppCollection<HoppRESTRequest>[]) {
export function setRESTCollections(entries: HoppCollection[]) {
restCollectionStore.dispatch({
dispatcher: "setCollections",
payload: {
@@ -953,9 +1058,7 @@ export const graphqlCollections$ = graphqlCollectionStore.subject$.pipe(
pluck("state")
)
export function appendRESTCollections(
entries: HoppCollection<HoppRESTRequest>[]
) {
export function appendRESTCollections(entries: HoppCollection[]) {
restCollectionStore.dispatch({
dispatcher: "appendCollections",
payload: {
@@ -964,7 +1067,7 @@ export function appendRESTCollections(
})
}
export function addRESTCollection(collection: HoppCollection<HoppRESTRequest>) {
export function addRESTCollection(collection: HoppCollection) {
restCollectionStore.dispatch({
dispatcher: "addCollection",
payload: {
@@ -992,7 +1095,7 @@ export function getRESTCollection(collectionIndex: number) {
export function editRESTCollection(
collectionIndex: number,
partialCollection: Partial<HoppCollection<HoppRESTRequest>>
partialCollection: Partial<HoppCollection>
) {
restCollectionStore.dispatch({
dispatcher: "editCollection",
@@ -1013,10 +1116,7 @@ export function addRESTFolder(name: string, path: string) {
})
}
export function editRESTFolder(
path: string,
folder: Partial<HoppCollection<HoppRESTRequest>>
) {
export function editRESTFolder(path: string, folder: Partial<HoppCollection>) {
restCollectionStore.dispatch({
dispatcher: "editFolder",
payload: {
@@ -1160,9 +1260,7 @@ export function updateRESTCollectionOrder(
})
}
export function setGraphqlCollections(
entries: HoppCollection<HoppGQLRequest>[]
) {
export function setGraphqlCollections(entries: HoppCollection[]) {
graphqlCollectionStore.dispatch({
dispatcher: "setCollections",
payload: {
@@ -1171,9 +1269,7 @@ export function setGraphqlCollections(
})
}
export function appendGraphqlCollections(
entries: HoppCollection<HoppGQLRequest>[]
) {
export function appendGraphqlCollections(entries: HoppCollection[]) {
graphqlCollectionStore.dispatch({
dispatcher: "appendCollections",
payload: {
@@ -1182,9 +1278,7 @@ export function appendGraphqlCollections(
})
}
export function addGraphqlCollection(
collection: HoppCollection<HoppGQLRequest>
) {
export function addGraphqlCollection(collection: HoppCollection) {
graphqlCollectionStore.dispatch({
dispatcher: "addCollection",
payload: {
@@ -1208,7 +1302,7 @@ export function removeGraphqlCollection(
export function editGraphqlCollection(
collectionIndex: number,
collection: Partial<HoppCollection<HoppGQLRequest>>
collection: Partial<HoppCollection>
) {
graphqlCollectionStore.dispatch({
dispatcher: "editCollection",
@@ -1231,7 +1325,7 @@ export function addGraphqlFolder(name: string, path: string) {
export function editGraphqlFolder(
path: string,
folder: Partial<HoppCollection<HoppGQLRequest>>
folder: Partial<HoppCollection>
) {
graphqlCollectionStore.dispatch({
dispatcher: "editFolder",
@@ -1322,14 +1416,12 @@ export function moveGraphqlRequest(
})
}
function removeDuplicateCollectionsFromPath<
T extends HoppRESTRequest | HoppGQLRequest,
>(
function removeDuplicateCollectionsFromPath(
idToRemove: string,
collectionPath: string | null,
collections: HoppCollection<T>[],
collections: HoppCollection[],
type: "collection" | "request"
): HoppCollection<T>[] {
): HoppCollection[] {
const indexes = collectionPath?.split("/").map((x) => parseInt(x))
indexes && indexes.pop()
const parentPath = indexes?.join("/")

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,48 @@
import { GQL_REQ_SCHEMA_VERSION, HoppGQLRequest, translateToGQLRequest } from "../graphql";
import { HoppRESTRequest, translateToNewRequest } from "../rest";
import { InferredEntity, createVersionedEntity } from "verzod"
const CURRENT_COLL_SCHEMA_VER = 1
import V1_VERSION from "./v/1"
import V2_VERSION from "./v/2"
type SupportedReqTypes =
| HoppRESTRequest
| HoppGQLRequest
import { z } from "zod"
import { translateToNewRequest } from "../rest"
import { translateToGQLRequest } from "../graphql"
export type HoppCollection<T extends SupportedReqTypes> = {
v: number
name: string
folders: HoppCollection<T>[]
requests: T[]
const versionedObject = z.object({
// v is a stringified number
v: z.string().regex(/^\d+$/).transform(Number),
})
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
* so it can be incremented independently without updating it everywhere
* @param x The Collection Data
* @returns The final collection
*/
export function makeCollection<T extends SupportedReqTypes>(
x: Omit<HoppCollection<T>, "v">
): HoppCollection<T> {
export function makeCollection(x: Omit<HoppCollection, "v">): HoppCollection {
return {
v: CURRENT_COLL_SCHEMA_VER,
...x
v: CollectionSchemaVersion,
...x,
}
}
@@ -36,20 +51,23 @@ export function makeCollection<T extends SupportedReqTypes>(
* @param x The collection object to load
* @returns The proper new collection format
*/
export function translateToNewRESTCollection(
x: any
): HoppCollection<HoppRESTRequest> {
if (x.v && x.v === 1) return x
export function translateToNewRESTCollection(x: any): HoppCollection {
if (x.v && x.v === CollectionSchemaVersion) return x
// Legacy
const name = x.name ?? "Untitled"
const folders = (x.folders ?? []).map(translateToNewRESTCollection)
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,
folders,
requests,
auth,
headers,
})
if (x.id) obj.id = x.id
@@ -62,24 +80,26 @@ export function translateToNewRESTCollection(
* @param x The collection object to load
* @returns The proper new collection format
*/
export function translateToNewGQLCollection(
x: any
): HoppCollection<HoppGQLRequest> {
if (x.v && x.v === GQL_REQ_SCHEMA_VERSION) return x
export function translateToNewGQLCollection(x: any): HoppCollection {
if (x.v && x.v === CollectionSchemaVersion) return x
// Legacy
const name = x.name ?? "Untitled"
const folders = (x.folders ?? []).map(translateToNewGQLCollection)
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,
folders,
requests,
auth,
headers,
})
if (x.id) obj.id = x.id
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,
HoppGQLAuthNone,
HoppGQLAuthOAuth2,
HoppGQLAuthInherit,
} from "./v/2"
export const GQL_REQ_SCHEMA_VERSION = 2

View File

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

View File

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

View File

@@ -3,27 +3,29 @@ import { z } from "zod"
import { V0_SCHEMA } from "./0"
export const FormDataKeyValue = z.object({
key: z.string(),
active: z.boolean()
}).and(
z.union([
z.object({
isFile: z.literal(true),
value: z.array(z.instanceof(Blob).nullable())
}),
z.object({
isFile: z.literal(false),
value: z.string()
})
])
)
export const FormDataKeyValue = z
.object({
key: z.string(),
active: z.boolean(),
})
.and(
z.union([
z.object({
isFile: z.literal(true),
value: z.array(z.instanceof(Blob).nullable()),
}),
z.object({
isFile: z.literal(false),
value: z.string(),
}),
])
)
export type FormDataKeyValue = z.infer<typeof FormDataKeyValue>
export const HoppRESTReqBodyFormData = z.object({
contentType: z.literal("multipart/form-data"),
body: z.array(FormDataKeyValue)
body: z.array(FormDataKeyValue),
})
export type HoppRESTReqBodyFormData = z.infer<typeof HoppRESTReqBodyFormData>
@@ -31,11 +33,11 @@ export type HoppRESTReqBodyFormData = z.infer<typeof HoppRESTReqBodyFormData>
export const HoppRESTReqBody = z.union([
z.object({
contentType: z.literal(null),
body: z.literal(null).catch(null)
body: z.literal(null).catch(null),
}),
z.object({
contentType: z.literal("multipart/form-data"),
body: z.array(FormDataKeyValue).catch([])
body: z.array(FormDataKeyValue).catch([]),
}),
z.object({
contentType: z.union([
@@ -48,14 +50,14 @@ export const HoppRESTReqBody = z.union([
z.literal("text/html"),
z.literal("text/plain"),
]),
body: z.string().catch("")
})
body: z.string().catch(""),
}),
])
export type HoppRESTReqBody = z.infer<typeof HoppRESTReqBody>
export const HoppRESTAuthNone = z.object({
authType: z.literal("none")
authType: z.literal("none"),
})
export type HoppRESTAuthNone = z.infer<typeof HoppRESTAuthNone>
@@ -96,17 +98,26 @@ export const HoppRESTAuthAPIKey = z.object({
export type HoppRESTAuthAPIKey = z.infer<typeof HoppRESTAuthAPIKey>
export const HoppRESTAuth = z.discriminatedUnion("authType", [
HoppRESTAuthNone,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey
]).and(
z.object({
authActive: z.boolean(),
})
)
export const HoppRESTAuthInherit = z.object({
authType: z.literal("inherit"),
})
export type HoppRESTAuthInherit = z.infer<typeof HoppRESTAuthInherit>
export const HoppRESTAuth = z
.discriminatedUnion("authType", [
HoppRESTAuthNone,
HoppRESTAuthInherit,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey,
])
.and(
z.object({
authActive: z.boolean(),
})
)
export type HoppRESTAuth = z.infer<typeof HoppRESTAuth>
@@ -114,7 +125,7 @@ export const HoppRESTParams = z.array(
z.object({
key: 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({
key: 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,
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 {
contentType: "application/json",
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")
return {
authType: "none",
@@ -183,7 +198,16 @@ export default defineVersion({
initial: false,
schema: V1_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 body = parseRequestBody(old)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
mutation CreateRESTRootUserCollection($title: String!) {
createRESTRootUserCollection(title: $title) {
mutation CreateRESTRootUserCollection($title: String!, $data: String) {
createRESTRootUserCollection(title: $title, data: $data) {
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
title
type
data
childrenGQL {
id
title
type
data
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ import {
editUserRequest,
moveUserCollection,
moveUserRequest,
renameUserCollection,
updateUserCollection,
updateUserCollectionOrder,
} 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
// TODO: use importCollectionsJSON to do this
const recursivelySyncCollections = async (
collection: HoppCollection<HoppRESTRequest>,
collection: HoppCollection,
collectionPath: string,
parentUserCollectionID?: string
) => {
@@ -47,27 +47,69 @@ const recursivelySyncCollections = async (
// if parentUserCollectionID does not exist, create the collection as a root collection
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)) {
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.auth = returnedData.auth
collection.headers = returnedData.headers
removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath)
} else {
parentCollectionID = undefined
}
} else {
// 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(
collection.name,
parentUserCollectionID
parentUserCollectionID,
JSON.stringify(data)
)
if (E.isRight(res)) {
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.auth = returnedData.auth
collection.headers = returnedData.headers
removeDuplicateRESTCollectionOrFolder(
childCollectionId,
@@ -155,8 +197,13 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
[collectionIndex]
)?.id
if (collectionID && collection.name) {
renameUserCollection(collectionID, collection.name)
const data = {
auth: collection.auth,
headers: collection.headers,
}
if (collectionID) {
updateUserCollection(collectionID, collection.name, JSON.stringify(data))
}
},
async addFolder({ name, path }) {
@@ -195,9 +242,12 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
)?.id
const folderName = folder.name
if (folderID && folderName) {
renameUserCollection(folderID, folderName)
const data = {
auth: folder.auth,
headers: folder.headers,
}
if (folderID) {
updateUserCollection(folderID, folderName, JSON.stringify(data))
}
},
async removeFolder({ folderID }) {

View File

@@ -21,7 +21,7 @@ import {
deleteUserCollection,
deleteUserRequest,
editGQLUserRequest,
renameUserCollection,
updateUserCollection,
} from "./collections.api"
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
// TODO: use importCollectionsJSON to do this
const recursivelySyncCollections = async (
collection: HoppCollection<HoppRESTRequest>,
collection: HoppCollection,
collectionPath: string,
parentUserCollectionID?: string
) => {
@@ -44,12 +44,35 @@ const recursivelySyncCollections = async (
// if parentUserCollectionID does not exist, create the collection as a root collection
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)) {
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.auth = returnedData.auth
collection.headers = returnedData.headers
removeDuplicateGraphqlCollectionOrFolder(
parentCollectionID,
collectionPath
@@ -59,15 +82,37 @@ const recursivelySyncCollections = async (
}
} else {
// 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(
collection.name,
parentUserCollectionID
parentUserCollectionID,
JSON.stringify(data)
)
if (E.isRight(res)) {
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.auth = returnedData.auth
collection.headers = returnedData.headers
removeDuplicateGraphqlCollectionOrFolder(
childCollectionId,
@@ -158,8 +203,13 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
[collectionIndex]
)?.id
if (collectionID && collection.name) {
renameUserCollection(collectionID, collection.name)
const data = {
auth: collection.auth,
headers: collection.headers,
}
if (collectionID) {
updateUserCollection(collectionID, collection.name, JSON.stringify(data))
}
},
async addFolder({ name, path }) {
@@ -197,8 +247,13 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
path.split("/").map((index) => parseInt(index))
)?.id
if (folderBackendId && folder.name) {
renameUserCollection(folderBackendId, folder.name)
const data = {
auth: folder.auth,
headers: folder.headers,
}
if (folderBackendId) {
updateUserCollection(folderBackendId, folder.name, JSON.stringify(data))
}
},
async removeFolder({ folderID }) {

View File

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

View File

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