diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue b/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue index 4776a15bf4..f6debfa999 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue @@ -299,7 +299,8 @@ const removeCollection = () => { ) { emit("select", null) } - removeGraphqlCollection(props.collectionIndex) + + removeGraphqlCollection(props.collectionIndex, props.collection.id) toast.success(`${t("state.deleted")}`) } diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue b/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue index 701dfa0dfa..7670dae149 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue @@ -279,7 +279,7 @@ const removeFolder = () => { emit("select", { picked: null }) } - removeGraphqlFolder(props.folderPath) + removeGraphqlFolder(props.folderPath, props.folder.id) toast.success(t("state.deleted")) } diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Request.vue b/packages/hoppscotch-common/src/components/collections/graphql/Request.vue index 0c42254044..0071a169a8 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Request.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Request.vue @@ -214,7 +214,7 @@ const removeRequest = () => { emit("select", null) } - removeGraphqlRequest(props.folderPath, props.requestIndex) + removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id) toast.success(`${t("state.deleted")}`) } diff --git a/packages/hoppscotch-common/src/components/collections/index.vue b/packages/hoppscotch-common/src/components/collections/index.vue index dd59e11155..cd7b48e10e 100644 --- a/packages/hoppscotch-common/src/components/collections/index.vue +++ b/packages/hoppscotch-common/src/components/collections/index.vue @@ -183,6 +183,8 @@ import { updateRESTRequestOrder, updateRESTCollectionOrder, moveRESTFolder, + navigateToFolderWithIndexPath, + restCollectionStore, } from "~/newstore/collections" import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter" import { @@ -1014,6 +1016,13 @@ const onRemoveCollection = () => { if (collectionsType.value.type === "my-collections") { const collectionIndex = editingCollectionIndex.value + const collectionToRemove = + collectionIndex || collectionIndex == 0 + ? navigateToFolderWithIndexPath(restCollectionStore.value.state, [ + collectionIndex, + ]) + : undefined + if (collectionIndex === null) return if ( @@ -1024,7 +1033,10 @@ const onRemoveCollection = () => { emit("select", null) } - removeRESTCollection(collectionIndex) + removeRESTCollection( + collectionIndex, + collectionToRemove ? collectionToRemove.id : undefined + ) resolveSaveContextOnCollectionReorder({ lastIndex: collectionIndex, @@ -1077,7 +1089,14 @@ const onRemoveFolder = () => { emit("select", null) } - removeRESTFolder(folderPath) + const folderToRemove = folderPath + ? navigateToFolderWithIndexPath( + restCollectionStore.value.state, + folderPath.split("/").map((i) => parseInt(i)) + ) + : undefined + + removeRESTFolder(folderPath, folderToRemove ? folderToRemove.id : undefined) const parentFolder = folderPath.split("/").slice(0, -1).join("/") // remove last folder to get parent folder resolveSaveContextOnCollectionReorder({ @@ -1151,7 +1170,12 @@ const onRemoveRequest = () => { possibleTab.value.document.isDirty = true } - removeRESTRequest(folderPath, requestIndex) + const requestToRemove = navigateToFolderWithIndexPath( + restCollectionStore.value.state, + folderPath.split("/").map((i) => parseInt(i)) + )?.requests[requestIndex] + + removeRESTRequest(folderPath, requestIndex, requestToRemove?.id) // the same function is used to reorder requests since after removing, it's basically doing reorder resolveSaveContextOnRequestReorder({ diff --git a/packages/hoppscotch-common/src/newstore/collections.ts b/packages/hoppscotch-common/src/newstore/collections.ts index aff9bd9b95..6123083635 100644 --- a/packages/hoppscotch-common/src/newstore/collections.ts +++ b/packages/hoppscotch-common/src/newstore/collections.ts @@ -33,6 +33,10 @@ const defaultGraphqlCollectionState = { 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[] @@ -86,7 +90,12 @@ const restCollectionDispatchers = defineDispatchers({ removeCollection( { state }: RESTCollectionStoreType, - { collectionIndex }: { collectionIndex: number } + { + 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( @@ -174,7 +183,12 @@ const restCollectionDispatchers = defineDispatchers({ } }, - removeFolder({ state }: RESTCollectionStoreType, { path }: { path: string }) { + 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)) @@ -415,7 +429,13 @@ const restCollectionDispatchers = defineDispatchers({ removeRequest( { state }: RESTCollectionStoreType, - { path, requestIndex }: { path: string; requestIndex: number } + { + 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 @@ -573,6 +593,31 @@ const restCollectionDispatchers = defineDispatchers({ 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({ @@ -605,7 +650,11 @@ const gqlCollectionDispatchers = defineDispatchers({ removeCollection( { state }: GraphqlCollectionStoreType, - { collectionIndex }: { collectionIndex: number } + { + 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( @@ -687,7 +736,9 @@ const gqlCollectionDispatchers = defineDispatchers({ removeFolder( { state }: GraphqlCollectionStoreType, - { path }: { path: string } + // 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 @@ -775,7 +826,13 @@ const gqlCollectionDispatchers = defineDispatchers({ removeRequest( { state }: GraphqlCollectionStoreType, - { path, requestIndex }: { path: string; requestIndex: number } + { + 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 @@ -838,6 +895,30 @@ const gqlCollectionDispatchers = defineDispatchers({ 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( @@ -887,11 +968,15 @@ export function addRESTCollection(collection: HoppCollection) { }) } -export function removeRESTCollection(collectionIndex: number) { +export function removeRESTCollection( + collectionIndex: number, + collectionID?: string +) { restCollectionStore.dispatch({ dispatcher: "removeCollection", payload: { collectionIndex, + collectionID, }, }) } @@ -936,11 +1021,12 @@ export function editRESTFolder( }) } -export function removeRESTFolder(path: string) { +export function removeRESTFolder(path: string, folderID?: string) { restCollectionStore.dispatch({ dispatcher: "removeFolder", payload: { path, + folderID, }, }) } @@ -955,6 +1041,21 @@ export function moveRESTFolder(path: string, destinationPath: string | null) { }) } +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, @@ -996,12 +1097,17 @@ export function saveRESTRequestAs(path: string, request: HoppRESTRequest) { return insertionIndex } -export function removeRESTRequest(path: string, requestIndex: number) { +export function removeRESTRequest( + path: string, + requestIndex: number, + requestID?: string +) { restCollectionStore.dispatch({ dispatcher: "removeRequest", payload: { path, requestIndex, + requestID, }, }) } @@ -1082,11 +1188,15 @@ export function addGraphqlCollection( }) } -export function removeGraphqlCollection(collectionIndex: number) { +export function removeGraphqlCollection( + collectionIndex: number, + collectionID?: string +) { graphqlCollectionStore.dispatch({ dispatcher: "removeCollection", payload: { collectionIndex, + collectionID, }, }) } @@ -1127,11 +1237,27 @@ export function editGraphqlFolder( }) } -export function removeGraphqlFolder(path: string) { +export function removeGraphqlFolder(path: string, folderID?: string) { graphqlCollectionStore.dispatch({ dispatcher: "removeFolder", payload: { path, + folderID, + }, + }) +} + +export function removeDuplicateGraphqlCollectionOrFolder( + id: string, + collectionPath: string, + type?: "collection" | "request" +) { + graphqlCollectionStore.dispatch({ + dispatcher: "removeDuplicateCollectionOrFolder", + payload: { + id, + collectionPath, + type: type ?? "collection", }, }) } @@ -1161,12 +1287,17 @@ export function saveGraphqlRequestAs(path: string, request: HoppGQLRequest) { }) } -export function removeGraphqlRequest(path: string, requestIndex: number) { +export function removeGraphqlRequest( + path: string, + requestIndex: number, + requestID?: string +) { graphqlCollectionStore.dispatch({ dispatcher: "removeRequest", payload: { path, requestIndex, + requestID, }, }) } @@ -1185,3 +1316,60 @@ export function moveGraphqlRequest( }, }) } + +function removeDuplicateCollectionsFromPath< + T extends HoppRESTRequest | HoppGQLRequest +>( + 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( + 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 + } +} diff --git a/packages/hoppscotch-data/src/graphql/index.ts b/packages/hoppscotch-data/src/graphql/index.ts index 67e85620ed..801b8fe827 100644 --- a/packages/hoppscotch-data/src/graphql/index.ts +++ b/packages/hoppscotch-data/src/graphql/index.ts @@ -11,6 +11,7 @@ export type GQLHeader = { } export type HoppGQLRequest = { + id?: string v: number name: string url: string