api-client/packages/hoppscotch-common/src/newstore/collections.ts
Anwarul Islam 088ea9f4dc
feat(common): remove functions to utilize ref_id when IDs are not provided (#5898)
Co-authored-by: nivedin <nivedinp@gmail.com>
2026-03-27 18:22:31 +05:30

1943 lines
46 KiB
TypeScript

import {
generateUniqueRefId,
HoppCollection,
HoppCollectionVariable,
HoppGQLAuth,
HoppGQLRequest,
HoppRESTAuth,
HoppRESTHeaders,
HoppRESTRequest,
makeCollection,
GQLHeader,
} from "@hoppscotch/data"
import { cloneDeep } from "lodash-es"
import { pluck } from "rxjs/operators"
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { getService } from "~/modules/dioc"
import { getI18n } from "~/modules/i18n"
import { RESTTabService } from "~/services/tab/rest"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { CurrentValueService } from "~/services/current-environment-value.service"
//collection variables current value and secret value
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
const defaultRESTCollectionState = {
state: [
makeCollection({
name: "My Collection",
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: false,
},
headers: [],
variables: [],
description: null,
}),
],
}
const defaultGraphqlCollectionState = {
state: [
makeCollection({
name: "My GraphQL Collection",
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: false,
},
headers: [],
variables: [],
description: null,
}),
],
}
type RESTCollectionStoreType = typeof defaultRESTCollectionState
type GraphqlCollectionStoreType = typeof defaultGraphqlCollectionState
/**
* NOTE: this function is not pure. It mutates the indexPaths inplace
* 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[],
indexPaths: number[]
) {
if (indexPaths.length === 0) return null
let target = collections[indexPaths.shift() as number]
while (indexPaths.length > 0 && target)
target = target.folders[indexPaths.shift() as number]
return target !== undefined ? target : null
}
const getCurrentValue = (
env: HoppCollectionVariable,
varIndex: number,
collectionID: string,
showSecret: boolean
) => {
if (env && env.secret && showSecret) {
return secretEnvironmentService.getSecretEnvironmentVariable(
collectionID,
varIndex
)?.value
}
return currentEnvironmentValueService.getEnvironmentVariable(
collectionID,
varIndex
)?.currentValue
}
/**
* This function populates the values of the variables with the current values or secrets.
* @param variables Variables to populate
* @returns Populated variables with current values or secrets
*/
function populateValues(
variables: HoppCollectionVariable[],
parentID: string,
showSecret: boolean
) {
return variables.map((v, index) => ({
...v,
currentValue:
getCurrentValue(v, index, parentID, showSecret) ?? v.currentValue,
}))
}
/**
* Used to obtain the inherited auth and headers for a given folder path, used for both REST and GraphQL personal collections
* @param folderPath the path of the folder to cascade the auth from
* @param type the type of collection
* @param showSecret whether to show secret values in the collection variables
* @returns the inherited auth and headers for the given folder path
*/
export function cascadeParentCollectionForProperties(
folderPath: string | undefined,
type: "rest" | "graphql",
showSecret: boolean = false
) {
const collectionStore =
type === "rest" ? restCollectionStore : graphqlCollectionStore
let auth: HoppInheritedProperty["auth"] = {
parentID: folderPath ?? "",
parentName: "",
inheritedAuth: {
authType: "none",
authActive: true,
},
}
const headers: HoppInheritedProperty["headers"] = []
const variables: HoppInheritedProperty["variables"] = []
if (!folderPath) return { auth, headers, variables }
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, variables }
}
// 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, variables }
}
const parentFolderAuth = parentFolder.auth as HoppRESTAuth | HoppGQLAuth
const parentFolderHeaders = parentFolder.headers as
| HoppRESTHeaders
| GQLHeader[]
const parentFolderVariables =
parentFolder.variables as HoppCollectionVariable[]
// check if the parent folder has authType 'inherit' and if it is the root folder
if (
parentFolderAuth?.authType === "inherit" &&
[...path.slice(0, i + 1)].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 idx = headers.findIndex(
(h) => h.inheritedHeader?.key === header.key
)
const currentPath = [...path.slice(0, i + 1)].join("/")
const headerObj = {
parentID: currentPath,
parentName: parentFolder.name,
inheritedHeader: header,
}
if (idx !== -1) headers[idx] = headerObj
else headers.push(headerObj)
})
}
if (parentFolderVariables) {
const currentPath = [...path.slice(0, i + 1)].join("/")
variables.push({
parentPath: currentPath,
parentID: parentFolder._ref_id ?? parentFolder.id ?? currentPath,
parentName: parentFolder.name,
inheritedVariables: populateValues(
parentFolderVariables,
parentFolder.id ?? currentPath,
showSecret
),
})
}
}
return { auth, headers, variables }
}
function reorderItems(array: unknown[], from: number, to: number) {
const item = array.splice(from, 1)[0]
if (from < to) {
array.splice(to - 1, 0, item)
} else {
array.splice(to, 0, item)
}
}
function createComparator<T>(
key: keyof T,
sortOrder: "asc" | "desc" = "asc"
): (a: T, b: T) => number {
return (a, b) => {
const aVal = a[key]
const bVal = b[key]
if (typeof aVal === "string" && typeof bVal === "string") {
return sortOrder === "asc"
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal)
}
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1
return 0
}
}
const restCollectionDispatchers = defineDispatchers({
setCollections(
_: RESTCollectionStoreType,
{ entries }: { entries: HoppCollection[] }
) {
return {
state: entries,
}
},
appendCollections(
{ state }: RESTCollectionStoreType,
{ entries }: { entries: HoppCollection[] }
) {
return {
state: [...state, ...entries],
}
},
addCollection(
{ state }: RESTCollectionStoreType,
{ collection }: { collection: HoppCollection }
) {
return {
state: [...state, collection],
}
},
removeCollection(
{ state }: RESTCollectionStoreType,
{
collectionIndex,
// this collectionID is used to sync the collection removal
// eslint-disable-next-line @typescript-eslint/no-unused-vars
collectionID,
}: { collectionIndex: number; collectionID?: string }
) {
return {
state: (state as any).filter(
(_: any, i: number) => i !== collectionIndex
),
}
},
editCollection(
{ state }: RESTCollectionStoreType,
{
collectionIndex,
partialCollection,
}: {
collectionIndex: number
partialCollection: Partial<HoppCollection>
}
) {
return {
state: state.map((col, index) =>
index === collectionIndex
? { ...col, ...cloneDeep(partialCollection) }
: col
),
}
},
sortRESTCollection(
{ state }: RESTCollectionStoreType,
{
collectionPath,
sortOrder,
}: { collectionPath: number | null; sortOrder: "asc" | "desc" }
) {
const newState = state
// If collectionPath is null, we are sorting the root collections
if (collectionPath === null || isNaN(collectionPath)) {
return {
state: newState.sort(createComparator("name", sortOrder)),
}
}
const collection = newState.find((_, index) => index === collectionPath)
if (!collection) {
console.error(`Collection not found.`)
return {}
}
collection.requests.sort(createComparator("name", sortOrder))
collection.folders.sort(createComparator("name", sortOrder))
return {
state: newState,
}
},
addFolder(
{ state }: RESTCollectionStoreType,
{ name, path }: { name: string; path: string }
) {
const newFolder: HoppCollection = makeCollection({
name,
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
variables: [],
description: null,
})
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const target = navigateToFolderWithIndexPath(newState, indexPaths)
if (target === null) {
console.log(`Could not parse path '${path}'. Ignoring add folder request`)
return {}
}
target.folders.push(newFolder)
return {
state: newState,
}
},
editFolder(
{ state }: RESTCollectionStoreType,
{
path,
folder,
}: {
path: string
folder: Partial<HoppCollection>
}
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const target = navigateToFolderWithIndexPath(newState, indexPaths)
if (target === null) {
console.log(
`Could not parse path '${path}'. Ignoring edit folder request`
)
return {}
}
Object.assign(target, {
...target,
...cloneDeep(folder),
})
return {
state: newState,
}
},
removeFolder(
{ state }: RESTCollectionStoreType,
// folderID is used to sync the folder removal in collections.sync.ts
// eslint-disable-next-line @typescript-eslint/no-unused-vars
{ path, folderID }: { path: string; folderID?: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
if (indexPaths.length === 0) {
console.log(
"Given path too short. If this is a collection, use removeCollection dispatcher instead. Skipping request."
)
return {}
}
// We get the index path to the folder itself,
// we have to find the folder containing the target folder,
// so we pop the last path index
const folderIndex = indexPaths.pop() as number
const containingFolder = navigateToFolderWithIndexPath(newState, indexPaths)
if (containingFolder === null) {
console.log(
`Could not resolve path '${path}'. Skipping removeFolder dispatch.`
)
return {}
}
containingFolder.folders.splice(folderIndex, 1)
return {
state: newState,
}
},
moveFolder(
{ state }: RESTCollectionStoreType,
{ path, destinationPath }: { path: string; destinationPath: string | null }
) {
const newState = state
// Move the folder to the root
if (destinationPath === null) {
const indexPaths = path.split("/").map((x) => parseInt(x))
if (indexPaths.length === 0) {
console.log("Given path too short. Skipping request.")
return {}
}
const folderIndex = indexPaths.pop() as number
const containingFolder = navigateToFolderWithIndexPath(
newState,
indexPaths
)
if (containingFolder === null) {
console.error(
`The folder to move is already in the root. Skipping request to move folder.`
)
return {}
}
const theFolder = containingFolder.folders.splice(folderIndex, 1)
newState.push(theFolder[0] as HoppCollection)
return {
state: newState,
}
}
const indexPaths = path.split("/").map((x) => parseInt(x))
const destinationIndexPaths = destinationPath
.split("/")
.map((x) => parseInt(x))
if (indexPaths.length === 0 || destinationIndexPaths.length === 0) {
console.error(
`Given path is too short. Skipping request to move folder '${path}' to destination '${destinationPath}'.`
)
return {}
}
const target = navigateToFolderWithIndexPath(
newState,
destinationIndexPaths
)
if (target === null) {
console.error(
`Could not resolve destination path '${destinationPath}'. Skipping moveFolder dispatch.`
)
return {}
}
const folderIndex = indexPaths.pop() as number
const containingFolder = navigateToFolderWithIndexPath(newState, indexPaths)
// We are moving a folder from the root
if (containingFolder === null) {
const theFolder = newState.splice(folderIndex, 1)
target.folders.push(theFolder[0])
} else {
const theFolder = containingFolder.folders.splice(folderIndex, 1)
target.folders.push(theFolder[0])
}
return { state: newState }
},
sortRESTFolder(
{ state }: RESTCollectionStoreType,
{
path,
sortOrder,
}: {
path: string
sortOrder: "asc" | "desc"
}
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
if (indexPaths.length === 0) {
console.log("Given path too short. Skipping request.")
return {}
}
const target = navigateToFolderWithIndexPath(newState, indexPaths)
if (target === null) {
console.log(
`Could not resolve path '${path}'. Ignoring sortRESTFolder dispatch.`
)
return {}
}
target.requests.sort(createComparator("name", sortOrder))
target.folders.sort(createComparator("name", sortOrder))
return {
state: newState,
}
},
updateCollectionOrder(
{ state }: RESTCollectionStoreType,
{
collectionIndex,
destinationCollectionIndex,
}: {
collectionIndex: string
destinationCollectionIndex: string | null
}
) {
const newState = state
const indexPaths = collectionIndex.split("/").map((x) => parseInt(x))
if (indexPaths.length === 0) {
console.log("Given path too short. Skipping request.")
return {}
}
// Reordering the collection to the last position
if (destinationCollectionIndex === null) {
const folderIndex = indexPaths.pop() as number
const containingFolder = navigateToFolderWithIndexPath(
newState,
indexPaths
)
if (containingFolder === null) {
newState.push(newState.splice(folderIndex, 1)[0])
return {
state: newState,
}
}
// Pushing the folder to the end of the array (last position)
containingFolder.folders.push(
containingFolder.folders.splice(folderIndex, 1)[0]
)
return {
state: newState,
}
}
const destinationIndexPaths = destinationCollectionIndex
.split("/")
.map((x) => parseInt(x))
if (destinationIndexPaths.length === 0) {
console.log("Given path too short. Skipping request.")
return {}
}
const folderIndex = indexPaths.pop() as number
const destinationFolderIndex = destinationIndexPaths.pop() as number
const containingFolder = navigateToFolderWithIndexPath(
newState,
destinationIndexPaths
)
if (containingFolder === null) {
reorderItems(newState, folderIndex, destinationFolderIndex)
return {
state: newState,
}
}
reorderItems(containingFolder.folders, folderIndex, destinationFolderIndex)
return {
state: newState,
}
},
duplicateCollection(
{ state }: RESTCollectionStoreType,
// `collectionSyncID` is used to sync the duplicated collection in `collections.sync.ts`
// eslint-disable-next-line @typescript-eslint/no-unused-vars
{ path, collectionSyncID }: { path: string; collectionSyncID?: string }
) {
const t = getI18n()
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const isRootCollection = indexPaths.length === 1
const collection = navigateToFolderWithIndexPath(state, [...indexPaths])
if (collection) {
const name = `${collection.name} - ${t("action.duplicate")}`
function recursiveChangeRefIdToAvoidConflicts(
collection: HoppCollection
): HoppCollection {
const newCollection = {
...collection,
_ref_id: generateUniqueRefId("coll"),
}
newCollection.folders = newCollection.folders.map((folder) =>
recursiveChangeRefIdToAvoidConflicts(folder)
)
return newCollection
}
const duplicatedCollection = {
...cloneDeep(collection),
name,
...(collection.id
? { id: `${collection.id}-duplicate-collection` }
: {}),
}
const duplicatedCollectionWithNewRefId =
recursiveChangeRefIdToAvoidConflicts(duplicatedCollection)
if (isRootCollection) {
newState.push(duplicatedCollectionWithNewRefId)
} else {
const parentCollectionIndexPath = indexPaths.slice(0, -1)
const parentCollection = navigateToFolderWithIndexPath(state, [
...parentCollectionIndexPath,
])
parentCollection?.folders.push(duplicatedCollectionWithNewRefId)
}
}
return {
state: newState,
}
},
editRequest(
{ state }: RESTCollectionStoreType,
{
path,
requestIndex,
requestNew,
}: { path: string; requestIndex: number; requestNew: any }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring editRequest dispatch.`
)
return {}
}
targetLocation.requests = targetLocation.requests.map((req, index) =>
index !== requestIndex ? req : requestNew
)
return {
state: newState,
}
},
saveRequestAs(
{ state }: RESTCollectionStoreType,
{ path, request }: { path: string; request: any }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring saveRequestAs dispatch.`
)
return {}
}
targetLocation.requests.push(request)
return {
state: newState,
}
},
removeRequest(
{ state }: RESTCollectionStoreType,
{
path,
requestIndex,
// this requestID is used to sync the request removal
// eslint-disable-next-line @typescript-eslint/no-unused-vars
requestID,
}: { path: string; requestIndex: number; requestID?: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring removeRequest dispatch.`
)
return {}
}
targetLocation.requests.splice(requestIndex, 1)
// Deal with situations where a tab with the given thing is deleted
// We are just going to dissociate the save context of the tab and mark it dirty
const tabService = getService(RESTTabService)
const tab = tabService.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: path,
requestIndex: requestIndex,
})
if (tab) {
tab.value.document.saveContext = undefined
tab.value.document.isDirty = true
}
return {
state: newState,
}
},
moveRequest(
{ state }: RESTCollectionStoreType,
{
path,
requestIndex,
destinationPath,
}: { path: string; requestIndex: number; destinationPath: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
if (indexPaths.length === 0) {
console.log("Given path too short. Skipping request.")
return {}
}
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve source path '${path}'. Skipping moveRequest dispatch.`
)
return {}
}
const req = targetLocation.requests[requestIndex]
const destIndexPaths = destinationPath.split("/").map((x) => parseInt(x))
const destLocation = navigateToFolderWithIndexPath(newState, destIndexPaths)
if (destLocation === null) {
console.log(
`Could not resolve destination path '${destinationPath}'. Skipping moveRequest dispatch.`
)
return {}
}
destLocation.requests.push(req)
targetLocation.requests.splice(requestIndex, 1)
const tabService = getService(RESTTabService)
const possibleTab = tabService.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: path,
requestIndex,
})
if (possibleTab) {
possibleTab.value.document.saveContext = {
originLocation: "user-collection",
folderPath: destinationPath,
requestIndex: destLocation.requests.length - 1,
}
}
return {
state: newState,
}
},
updateRequestOrder(
{ state }: RESTCollectionStoreType,
{
requestIndex,
destinationRequestIndex,
destinationCollectionPath,
}: {
requestIndex: number
destinationRequestIndex: number | null
destinationCollectionPath: string
}
) {
const newState = state
const indexPaths = destinationCollectionPath
.split("/")
.map((x) => parseInt(x))
if (indexPaths.length === 0) {
console.log("Given path too short. Skipping request.")
return {}
}
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${destinationCollectionPath}'. Ignoring reorderRequest dispatch.`
)
return {}
}
// if the destination is null, we are moving to the end of the list
if (destinationRequestIndex === null) {
// move to the end of the list
targetLocation.requests.push(
targetLocation.requests.splice(requestIndex, 1)[0]
)
resolveSaveContextOnRequestReorder({
lastIndex: requestIndex,
newIndex: targetLocation.requests.length,
folderPath: destinationCollectionPath,
})
return {
state: newState,
}
}
reorderItems(targetLocation.requests, requestIndex, destinationRequestIndex)
resolveSaveContextOnRequestReorder({
lastIndex: requestIndex,
newIndex: destinationRequestIndex,
folderPath: destinationCollectionPath,
})
return {
state: newState,
}
},
// only used for collections.sync.ts to prevent double insertion of collections from storeSync and Subscriptions
removeDuplicateCollectionOrFolder(
{ state },
{
id,
collectionPath,
type,
}: {
id: string
collectionPath: string
type: "collection" | "request"
}
) {
const after = removeDuplicateCollectionsFromPath(
id,
collectionPath,
state,
type ?? "collection"
)
return {
state: after,
}
},
})
const gqlCollectionDispatchers = defineDispatchers({
setCollections(
_: GraphqlCollectionStoreType,
{ entries }: { entries: HoppCollection[] }
) {
return {
state: entries,
}
},
appendCollections(
{ state }: GraphqlCollectionStoreType,
{ entries }: { entries: HoppCollection[] }
) {
return {
state: [...state, ...entries],
}
},
addCollection(
{ state }: GraphqlCollectionStoreType,
{ collection }: { collection: HoppCollection }
) {
return {
state: [...state, collection],
}
},
removeCollection(
{ state }: GraphqlCollectionStoreType,
{
collectionIndex, // this collectionID is used to sync the collection removal
// eslint-disable-next-line @typescript-eslint/no-unused-vars
collectionID,
}: { collectionIndex: number; collectionID?: string }
) {
return {
state: (state as any).filter(
(_: any, i: number) => i !== collectionIndex
),
}
},
editCollection(
{ state }: GraphqlCollectionStoreType,
{
collectionIndex,
collection,
}: { collectionIndex: number; collection: Partial<HoppCollection> }
) {
return {
state: state.map((col, index) =>
index === collectionIndex ? { ...col, ...cloneDeep(collection) } : col
),
}
},
addFolder(
{ state }: GraphqlCollectionStoreType,
{ name, path }: { name: string; path: string }
) {
const newFolder: HoppCollection = makeCollection({
name,
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
variables: [],
description: null,
})
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const target = navigateToFolderWithIndexPath(newState, indexPaths)
if (target === null) {
console.log(`Could not parse path '${path}'. Ignoring add folder request`)
return {}
}
target.folders.push(newFolder)
return {
state: newState,
}
},
editFolder(
{ state }: GraphqlCollectionStoreType,
{ path, folder }: { path: string; folder: Partial<HoppCollection> }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const target = navigateToFolderWithIndexPath(newState, indexPaths)
if (target === null) {
console.log(
`Could not parse path '${path}'. Ignoring edit folder request`
)
return {}
}
Object.assign(target, {
...target,
...cloneDeep(folder),
})
return {
state: newState,
}
},
removeFolder(
{ state }: GraphqlCollectionStoreType,
// folderID is used to sync the folder removal in collections.sync.ts
// eslint-disable-next-line @typescript-eslint/no-unused-vars
{ path, folderID }: { path: string; folderID?: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
if (indexPaths.length === 0) {
console.log(
"Given path too short. If this is a collection, use removeCollection dispatcher instead. Skipping request."
)
return {}
}
// We get the index path to the folder itself,
// we have to find the folder containing the target folder,
// so we pop the last path index
const folderIndex = indexPaths.pop() as number
const containingFolder = navigateToFolderWithIndexPath(newState, indexPaths)
if (containingFolder === null) {
console.log(
`Could not resolve path '${path}'. Skipping removeFolder dispatch.`
)
return {}
}
containingFolder.folders.splice(folderIndex, 1)
return {
state: newState,
}
},
duplicateCollection(
{ state }: GraphqlCollectionStoreType,
// `collectionSyncID` is used to sync the duplicated collection in `gqlCollections.sync.ts`
// eslint-disable-next-line @typescript-eslint/no-unused-vars
{ path, collectionSyncID }: { path: string; collectionSyncID?: string }
) {
const t = getI18n()
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const isRootCollection = indexPaths.length === 1
const collection = navigateToFolderWithIndexPath(state, [...indexPaths])
if (collection) {
const name = `${collection.name} - ${t("action.duplicate")}`
const duplicatedCollection = {
...cloneDeep(collection),
name,
...(collection.id
? { id: `${collection.id}-duplicate-collection` }
: {}),
}
if (isRootCollection) {
newState.push(duplicatedCollection)
} else {
const parentCollectionIndexPath = indexPaths.slice(0, -1)
const parentCollection = navigateToFolderWithIndexPath(state, [
...parentCollectionIndexPath,
])
parentCollection?.folders.push(duplicatedCollection)
}
}
return {
state: newState,
}
},
editRequest(
{ state }: GraphqlCollectionStoreType,
{
path,
requestIndex,
requestNew,
}: { path: string; requestIndex: number; requestNew: any }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring editRequest dispatch.`
)
return {}
}
targetLocation.requests = targetLocation.requests.map((req, index) =>
index !== requestIndex ? req : requestNew
)
return {
state: newState,
}
},
saveRequestAs(
{ state }: GraphqlCollectionStoreType,
{ path, request }: { path: string; request: any }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring saveRequestAs dispatch.`
)
return {}
}
targetLocation.requests.push(request)
return {
state: newState,
}
},
removeRequest(
{ state }: GraphqlCollectionStoreType,
{
path,
requestIndex,
// this requestID is used to sync the request removal
// eslint-disable-next-line @typescript-eslint/no-unused-vars
requestID,
}: { path: string; requestIndex: number; requestID?: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring removeRequest dispatch.`
)
return {}
}
targetLocation.requests.splice(requestIndex, 1)
return {
state: newState,
}
},
moveRequest(
{ state }: GraphqlCollectionStoreType,
{
path,
requestIndex,
destinationPath,
}: { path: string; requestIndex: number; destinationPath: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve source path '${path}'. Skipping moveRequest dispatch.`
)
return {}
}
const req = targetLocation.requests[requestIndex]
const destIndexPaths = destinationPath.split("/").map((x) => parseInt(x))
const destLocation = navigateToFolderWithIndexPath(newState, destIndexPaths)
if (destLocation === null) {
console.log(
`Could not resolve destination path '${destinationPath}'. Skipping moveRequest dispatch.`
)
return {}
}
destLocation.requests.push(req)
targetLocation.requests.splice(requestIndex, 1)
return {
state: newState,
}
},
// only used for collections.sync.ts to prevent double insertion of collections from storeSync and Subscriptions
removeDuplicateCollectionOrFolder(
{ state },
{
id,
collectionPath,
type,
}: {
id: string
collectionPath: string
type: "collection" | "request"
}
) {
const after = removeDuplicateCollectionsFromPath(
id,
collectionPath,
state,
type ?? "collection"
)
return {
state: after,
}
},
})
export const restCollectionStore = new DispatchingStore(
defaultRESTCollectionState,
restCollectionDispatchers
)
export const graphqlCollectionStore = new DispatchingStore(
defaultGraphqlCollectionState,
gqlCollectionDispatchers
)
export function setRESTCollections(entries: HoppCollection[]) {
restCollectionStore.dispatch({
dispatcher: "setCollections",
payload: {
entries,
},
})
}
export const restCollections$ = restCollectionStore.subject$.pipe(
pluck("state")
)
export const graphqlCollections$ = graphqlCollectionStore.subject$.pipe(
pluck("state")
)
export function appendRESTCollections(entries: HoppCollection[]) {
restCollectionStore.dispatch({
dispatcher: "appendCollections",
payload: {
entries,
},
})
}
export function addRESTCollection(collection: HoppCollection) {
restCollectionStore.dispatch({
dispatcher: "addCollection",
payload: {
collection,
},
})
}
export function removeRESTCollection(
collectionIndex: number,
collectionID?: string
) {
if (!collectionID) {
collectionID = restCollectionStore.value.state[collectionIndex]?._ref_id
}
restCollectionStore.dispatch({
dispatcher: "removeCollection",
payload: {
collectionIndex,
collectionID,
},
})
}
export function getRESTCollection(collectionIndex: number) {
return restCollectionStore.value.state[collectionIndex]
}
function computeCollectionInheritedProps(
collection: HoppCollection,
ref_id: string,
type: "my-collections" | "team-collections" = "my-collections",
parentAuth: HoppRESTAuth | null = null,
parentHeaders: HoppRESTHeaders | null = null,
parentVariables: HoppCollectionVariable[] | null = null
): {
auth: HoppRESTAuth
headers: HoppRESTHeaders
variables: HoppCollectionVariable[]
} | null {
// Determine the inherited authentication and headers
const inheritedAuth =
collection.auth?.authType === "inherit" && collection.auth.authActive
? (parentAuth ?? { authType: "none", authActive: false })
: (collection.auth ?? { authType: "none", authActive: false })
const inheritedHeaders: HoppRESTHeaders = [
...(parentHeaders ?? []),
...collection.headers,
]
const inheritedVariables = [
...(parentVariables ?? []),
...collection.variables,
]
// Check if the current collection matches the target reference ID
const isTargetCollection =
type === "my-collections"
? collection._ref_id === ref_id
: collection.id === ref_id
if (isTargetCollection) {
return {
auth: inheritedAuth,
headers: inheritedHeaders,
variables: inheritedVariables,
}
}
// Recursively search in folders
for (const folder of collection.folders) {
const result = computeCollectionInheritedProps(
folder,
ref_id,
type,
inheritedAuth,
inheritedHeaders,
inheritedVariables
)
if (result) return result // Return as soon as a match is found
}
return null
}
export function getRESTCollectionInheritedProps(
collectionID: string,
collections: HoppCollection[] = restCollectionStore.value.state,
type: "my-collections" | "team-collections" = "my-collections"
): {
auth: HoppRESTAuth
headers: HoppRESTHeaders
variables: HoppCollectionVariable[]
} | null {
for (const collection of collections) {
const result = computeCollectionInheritedProps(
collection,
collectionID,
type
)
if (result) {
return result
}
}
return null
}
function findCollection(
collection: HoppCollection,
ref_id: string
): HoppCollection | null {
if (collection._ref_id === ref_id) {
return collection
}
for (const folder of collection.folders) {
const found = findCollection(folder, ref_id)
if (found) {
return found
}
}
return null
}
export function getRESTCollectionByRefId(ref_id: string) {
for (const collection of restCollectionStore.value.state) {
const found = findCollection(collection, ref_id)
if (found) {
return found
}
}
}
export function editRESTCollection(
collectionIndex: number,
partialCollection: Partial<HoppCollection>
) {
restCollectionStore.dispatch({
dispatcher: "editCollection",
payload: {
collectionIndex,
partialCollection: partialCollection,
},
})
}
export function sortRESTCollection(
collectionPath: number | null,
sortOrder: "asc" | "desc"
) {
restCollectionStore.dispatch({
dispatcher: "sortRESTCollection",
payload: {
collectionPath,
sortOrder,
},
})
}
export function addRESTFolder(name: string, path: string) {
restCollectionStore.dispatch({
dispatcher: "addFolder",
payload: {
name,
path,
},
})
}
export function editRESTFolder(path: string, folder: Partial<HoppCollection>) {
restCollectionStore.dispatch({
dispatcher: "editFolder",
payload: {
path,
folder,
},
})
}
export function removeRESTFolder(path: string, folderID?: string) {
if (!folderID) {
const folder = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)
folderID = folder?._ref_id
}
restCollectionStore.dispatch({
dispatcher: "removeFolder",
payload: {
path,
folderID,
},
})
}
export function moveRESTFolder(path: string, destinationPath: string | null) {
restCollectionStore.dispatch({
dispatcher: "moveFolder",
payload: {
path,
destinationPath,
},
})
}
export function sortRESTFolder(path: string, sortOrder: "asc" | "desc") {
restCollectionStore.dispatch({
dispatcher: "sortRESTFolder",
payload: {
path,
sortOrder,
},
})
}
export function duplicateRESTCollection(
path: string,
collectionSyncID?: string
) {
restCollectionStore.dispatch({
dispatcher: "duplicateCollection",
payload: {
path,
collectionSyncID,
},
})
}
export function removeDuplicateRESTCollectionOrFolder(
id: string,
collectionPath: string,
type?: "collection" | "request"
) {
restCollectionStore.dispatch({
dispatcher: "removeDuplicateCollectionOrFolder",
payload: {
id,
collectionPath,
type: type ?? "collection",
},
})
}
export function editRESTRequest(
path: string,
requestIndex: number,
requestNew: HoppRESTRequest
) {
const indexPaths = path.split("/").map((x) => parseInt(x))
if (
!navigateToFolderWithIndexPath(restCollectionStore.value.state, indexPaths)
)
throw new Error("Path not found")
restCollectionStore.dispatch({
dispatcher: "editRequest",
payload: {
path,
requestIndex,
requestNew,
},
})
}
export function saveRESTRequestAs(path: string, request: HoppRESTRequest) {
// For calculating the insertion request index
const targetLocation = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
path.split("/").map((x) => parseInt(x))
)
const insertionIndex = targetLocation!.requests.length
restCollectionStore.dispatch({
dispatcher: "saveRequestAs",
payload: {
path,
request,
},
})
return insertionIndex
}
export function removeRESTRequest(
path: string,
requestIndex: number,
requestID?: string
) {
if (!requestID) {
const request = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)?.requests[requestIndex]
requestID = request?.id || (request as HoppRESTRequest)?._ref_id
}
restCollectionStore.dispatch({
dispatcher: "removeRequest",
payload: {
path,
requestIndex,
requestID,
},
})
}
export function moveRESTRequest(
path: string,
requestIndex: number,
destinationPath: string
) {
restCollectionStore.dispatch({
dispatcher: "moveRequest",
payload: {
path,
requestIndex,
destinationPath,
},
})
}
export function updateRESTRequestOrder(
requestIndex: number,
destinationRequestIndex: number | null,
destinationCollectionPath: string
) {
restCollectionStore.dispatch({
dispatcher: "updateRequestOrder",
payload: {
requestIndex,
destinationRequestIndex,
destinationCollectionPath,
},
})
}
export function updateRESTCollectionOrder(
collectionIndex: string,
destinationCollectionIndex: string | null
) {
restCollectionStore.dispatch({
dispatcher: "updateCollectionOrder",
payload: {
collectionIndex,
destinationCollectionIndex,
},
})
}
export function setGraphqlCollections(entries: HoppCollection[]) {
graphqlCollectionStore.dispatch({
dispatcher: "setCollections",
payload: {
entries,
},
})
}
export function appendGraphqlCollections(entries: HoppCollection[]) {
graphqlCollectionStore.dispatch({
dispatcher: "appendCollections",
payload: {
entries,
},
})
}
export function addGraphqlCollection(collection: HoppCollection) {
graphqlCollectionStore.dispatch({
dispatcher: "addCollection",
payload: {
collection,
},
})
}
export function removeGraphqlCollection(
collectionIndex: number,
collectionID?: string
) {
if (!collectionID) {
collectionID = graphqlCollectionStore.value.state[collectionIndex]?._ref_id
}
graphqlCollectionStore.dispatch({
dispatcher: "removeCollection",
payload: {
collectionIndex,
collectionID,
},
})
}
export function editGraphqlCollection(
collectionIndex: number,
collection: Partial<HoppCollection>
) {
graphqlCollectionStore.dispatch({
dispatcher: "editCollection",
payload: {
collectionIndex,
collection,
},
})
}
export function addGraphqlFolder(name: string, path: string) {
graphqlCollectionStore.dispatch({
dispatcher: "addFolder",
payload: {
name,
path,
},
})
}
export function editGraphqlFolder(
path: string,
folder: Partial<HoppCollection>
) {
graphqlCollectionStore.dispatch({
dispatcher: "editFolder",
payload: {
path,
folder,
},
})
}
export function removeGraphqlFolder(path: string, folderID?: string) {
if (!folderID) {
const folder = navigateToFolderWithIndexPath(
graphqlCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)
folderID = folder?._ref_id
}
graphqlCollectionStore.dispatch({
dispatcher: "removeFolder",
payload: {
path,
folderID,
},
})
}
export function duplicateGraphQLCollection(
path: string,
collectionSyncID?: string
) {
graphqlCollectionStore.dispatch({
dispatcher: "duplicateCollection",
payload: {
path,
collectionSyncID,
},
})
}
export function removeDuplicateGraphqlCollectionOrFolder(
id: string,
collectionPath: string,
type?: "collection" | "request"
) {
graphqlCollectionStore.dispatch({
dispatcher: "removeDuplicateCollectionOrFolder",
payload: {
id,
collectionPath,
type: type ?? "collection",
},
})
}
export function editGraphqlRequest(
path: string,
requestIndex: number,
requestNew: HoppGQLRequest
) {
graphqlCollectionStore.dispatch({
dispatcher: "editRequest",
payload: {
path,
requestIndex,
requestNew,
},
})
}
export function saveGraphqlRequestAs(path: string, request: HoppGQLRequest) {
// For calculating the insertion request index
const targetLocation = navigateToFolderWithIndexPath(
graphqlCollectionStore.value.state,
path.split("/").map((x) => parseInt(x))
)
const insertionIndex = targetLocation!.requests.length
graphqlCollectionStore.dispatch({
dispatcher: "saveRequestAs",
payload: {
path,
request,
},
})
return insertionIndex
}
export function removeGraphqlRequest(
path: string,
requestIndex: number,
requestID?: string
) {
if (!requestID) {
const request = navigateToFolderWithIndexPath(
graphqlCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)?.requests[requestIndex]
requestID = request?.id || `${path}/${requestIndex}`
}
graphqlCollectionStore.dispatch({
dispatcher: "removeRequest",
payload: {
path,
requestIndex,
requestID,
},
})
}
export function moveGraphqlRequest(
path: string,
requestIndex: number,
destinationPath: string
) {
graphqlCollectionStore.dispatch({
dispatcher: "moveRequest",
payload: {
path,
requestIndex,
destinationPath,
},
})
}
function removeDuplicateCollectionsFromPath(
idToRemove: string,
collectionPath: string | null,
collections: HoppCollection[],
type: "collection" | "request"
): HoppCollection[] {
const indexes = collectionPath?.split("/").map((x) => parseInt(x))
indexes && indexes.pop()
const parentPath = indexes?.join("/")
const parentCollection = parentPath
? navigateToFolderWithIndexPath(
collections,
parentPath.split("/").map((x) => parseInt(x)) || []
)
: undefined
if (collectionPath && parentCollection) {
if (type === "collection") {
parentCollection.folders = removeDuplicatesFromAnArrayById(
idToRemove,
parentCollection.folders
)
} else {
parentCollection.requests = removeDuplicatesFromAnArrayById(
idToRemove,
parentCollection.requests
)
}
} else {
return removeDuplicatesFromAnArrayById(idToRemove, collections)
}
return collections
function removeDuplicatesFromAnArrayById<T extends { id?: string }>(
idToRemove: string,
arrayWithID: T[]
) {
const duplicateEntries = arrayWithID.filter(
(entry) => entry.id === idToRemove
)
if (duplicateEntries.length === 2) {
const duplicateEntryIndex = arrayWithID.findIndex(
(entry) => entry.id === idToRemove
)
arrayWithID.splice(duplicateEntryIndex, 1)
}
return arrayWithID
}
}