feat: duplicate REST/GraphQL collections (#4211)

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
James George
2024-07-29 06:07:34 -07:00
committed by GitHub
parent c24d5c5302
commit c9f92282bf
26 changed files with 734 additions and 105 deletions

View File

@@ -0,0 +1,3 @@
mutation DuplicateUserCollection($collectionID: String!, $reqType: ReqType!) {
duplicateUserCollection(collectionID: $collectionID, reqType: $reqType)
}

View File

@@ -40,6 +40,7 @@ createHoppApp("#app", {
platformFeatureFlags: {
exportAsGIST: false,
hasTelemetry: false,
duplicateCollectionDisabledInPersonalWorkspace: true,
},
infra: InfraPlatform,
})

View File

@@ -68,6 +68,9 @@ import {
UpdateUserCollectionMutation,
UpdateUserCollectionMutationVariables,
UpdateUserCollectionDocument,
DuplicateUserCollectionDocument,
DuplicateUserCollectionMutation,
DuplicateUserCollectionMutationVariables,
} from "../../api/generated/graphql"
export const createRESTRootUserCollection = (title: string, data?: string) =>
@@ -193,6 +196,19 @@ export const moveUserCollection = (
destCollectionID: destinationCollectionID,
})()
export const duplicateUserCollection = (
collectionID: string,
reqType: ReqType
) =>
runMutation<
DuplicateUserCollectionMutation,
DuplicateUserCollectionMutationVariables,
""
>(DuplicateUserCollectionDocument, {
collectionID,
reqType,
})()
export const editUserRequest = (
requestID: string,
title: string,

View File

@@ -284,6 +284,20 @@ function setupUserCollectionCreatedSubscription() {
return
}
// While duplicating a collection, the new entry added to the store has an ID with a suffix to be updated after the backend ID is received from the GQL subscription
// This is to prevent the new entry from being added to the store again when the GQL subscription
// The boolean return value indicates if the GQL subscription was fired because of a duplicate collection action and whether the collection should be added to the store
const shouldCreateCollection = issueBackendIDToDuplicatedCollection(
collectionStore,
collectionType,
userCollectionBackendID,
parentCollectionID
)
if (!shouldCreateCollection) {
return
}
const parentCollectionPath =
parentCollectionID &&
getCollectionPathFromCollectionID(
@@ -828,3 +842,105 @@ function getRequestIndex(
return requestIndex
}
function issueBackendIDToDuplicatedCollection(
collectionStore: ReturnType<
typeof getStoreByCollectionType
>["collectionStore"],
collectionType: ReqType,
userCollectionBackendID: string,
parentCollectionID?: string
): boolean {
// Collection added to store via duplicating is set an ID with a suffix to be updated after the backend ID is received from the GQL subscription
const collectionCreatedFromStoreIDSuffix = "-duplicate-collection"
// Duplicating a child collection
if (parentCollectionID) {
// Get the index path for the parent collection
const parentCollectionPath = getCollectionPathFromCollectionID(
parentCollectionID,
collectionStore.value.state
)
if (!parentCollectionPath) {
// Indicates the collection received from the GQL subscription should be created in the store
return true
}
const parentCollection = navigateToFolderWithIndexPath(
collectionStore.value.state,
parentCollectionPath.split("/").map((index) => parseInt(index))
)
if (!parentCollection) {
// Indicates the collection received from the GQL subscription should be created in the store
return true
}
// Grab the child collection inserted via store update with the ID suffix
const collectionInsertedViaStoreUpdateIdx =
parentCollection.folders.findIndex(({ id }) =>
id?.endsWith(collectionCreatedFromStoreIDSuffix)
)
// No entry indicates the GQL subscription was fired not because of a duplicate collection action
if (collectionInsertedViaStoreUpdateIdx === -1) {
// Indicates the collection received from the GQL subscription should be created in the store
return true
}
const collectionInsertedViaStoreUpdate =
parentCollection.folders[collectionInsertedViaStoreUpdateIdx]
const childCollectionPath = `${parentCollectionPath}/${collectionInsertedViaStoreUpdateIdx}`
// Update the ID for the child collection already existing in store with the backend ID
runDispatchWithOutSyncing(() => {
if (collectionType == ReqType.Rest) {
editRESTFolder(childCollectionPath, {
...collectionInsertedViaStoreUpdate,
id: userCollectionBackendID,
})
} else {
editGraphqlFolder(childCollectionPath, {
...collectionInsertedViaStoreUpdate,
id: userCollectionBackendID,
})
}
})
} else {
// Duplicating a root collection
// Grab the collection inserted via store update with the ID suffix
const collectionInsertedViaStoreUpdateIdx =
collectionStore.value.state.findIndex(({ id }) =>
id?.endsWith(collectionCreatedFromStoreIDSuffix)
)
// No entry indicates the GQL subscription was fired not because of a duplicate collection action
if (collectionInsertedViaStoreUpdateIdx === -1) {
// Indicates the collection received from the GQL subscription should be created in the store
return true
}
const collectionInsertedViaStoreUpdate =
collectionStore.value.state[collectionInsertedViaStoreUpdateIdx]
// Update the ID for the collection already existing in store with the backend ID
runDispatchWithOutSyncing(() => {
if (collectionType == ReqType.Rest) {
editRESTCollection(collectionInsertedViaStoreUpdateIdx, {
...collectionInsertedViaStoreUpdate,
id: userCollectionBackendID,
})
} else {
editGraphqlCollection(collectionInsertedViaStoreUpdateIdx, {
...collectionInsertedViaStoreUpdate,
id: userCollectionBackendID,
})
}
})
}
// Prevent adding the collection received from GQL subscription to the store
return false
}

View File

@@ -11,9 +11,7 @@ import {
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { getSyncInitFunction } from "../../lib/sync"
import { StoreSyncDefinitionOf } from "../../lib/sync"
import { getSyncInitFunction, StoreSyncDefinitionOf } from "../../lib/sync"
import { createMapper } from "../../lib/sync/mapper"
import {
createRESTChildUserCollection,
@@ -21,6 +19,7 @@ import {
createRESTUserRequest,
deleteUserCollection,
deleteUserRequest,
duplicateUserCollection,
editUserRequest,
moveUserCollection,
moveUserRequest,
@@ -29,6 +28,7 @@ import {
} from "./collections.api"
import * as E from "fp-ts/Either"
import { ReqType } from "../../api/generated/graphql"
// restCollectionsMapper uses the collectionPath as the local identifier
export const restCollectionsMapper = createMapper<string, string>()
@@ -280,6 +280,11 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
}
}
},
async duplicateCollection({ collectionSyncID }) {
if (collectionSyncID) {
await duplicateUserCollection(collectionSyncID, ReqType.Rest)
}
},
editRequest({ path, requestIndex, requestNew }) {
const request = navigateToFolderWithIndexPath(
restCollectionStore.value.state,

View File

@@ -20,11 +20,13 @@ import {
createGQLUserRequest,
deleteUserCollection,
deleteUserRequest,
duplicateUserCollection,
editGQLUserRequest,
updateUserCollection,
} from "./collections.api"
import * as E from "fp-ts/Either"
import { ReqType } from "../../api/generated/graphql"
import { moveOrReorderRequests } from "./collections.sync"
// gqlCollectionsMapper uses the collectionPath as the local identifier
@@ -261,6 +263,11 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
await deleteUserCollection(folderID)
}
},
async duplicateCollection({ collectionSyncID }) {
if (collectionSyncID) {
await duplicateUserCollection(collectionSyncID, ReqType.Gql)
}
},
editRequest({ path, requestIndex, requestNew }) {
const request = navigateToFolderWithIndexPath(
graphqlCollectionStore.value.state,