From 5e9f8743d443950d856b74712581983f340b579a Mon Sep 17 00:00:00 2001 From: James George <25279263+jamesgeorge007@users.noreply.github.com> Date: Mon, 30 Sep 2024 08:50:19 -0700 Subject: [PATCH] feat: extend duplicate collection to personal workspace in SH (#4368) --- .../src/components/collections/Collection.vue | 22 +- .../collections/graphql/Collection.vue | 18 +- .../components/collections/graphql/Folder.vue | 18 +- .../hoppscotch-common/src/platform/index.ts | 6 - .../UserCollectionCreated.graphql | 1 + .../UserCollectionDuplicated.graphql | 15 + .../platform/collections/collections.api.ts | 101 ++--- .../collections/collections.platform.ts | 352 ++++++++++++++--- .../userCollectionDuplicated.graphql | 15 + packages/hoppscotch-selfhost-web/src/main.ts | 1 - .../platform/collections/collections.api.ts | 7 + .../collections/collections.platform.ts | 363 +++++++++++------- 12 files changed, 624 insertions(+), 295 deletions(-) create mode 100644 packages/hoppscotch-selfhost-desktop/src/api/subscriptions/UserCollectionDuplicated.graphql create mode 100644 packages/hoppscotch-selfhost-web/src/api/subscriptions/userCollectionDuplicated.graphql diff --git a/packages/hoppscotch-common/src/components/collections/Collection.vue b/packages/hoppscotch-common/src/components/collections/Collection.vue index 0faf43bb6..d0e8052f9 100644 --- a/packages/hoppscotch-common/src/components/collections/Collection.vue +++ b/packages/hoppscotch-common/src/components/collections/Collection.vue @@ -102,11 +102,7 @@ @keyup.r="requestAction?.$el.click()" @keyup.n="folderAction?.$el.click()" @keyup.e="edit?.$el.click()" - @keyup.d=" - showDuplicateCollectionAction - ? duplicateAction?.$el.click() - : null - " + @keyup.d="duplicateAction?.$el.click()" @keyup.delete="deleteAction?.$el.click()" @keyup.x="exportAction?.$el.click()" @keyup.p="propertiesAction?.$el.click()" @@ -150,7 +146,6 @@ " /> { return (props.data as TeamCollection).title }) -const showDuplicateCollectionAction = computed(() => { - // Show if the user is not logged in - if (!currentUser.value) { - return true - } - - if (props.collectionsType === "team-collections") { - return true - } - - // Duplicate collection action is disabled on SH until the issue with syncing is resolved - return !platform.platformFeatureFlags - .duplicateCollectionDisabledInPersonalWorkspace -}) - watch( () => [props.exportLoading, props.duplicateCollectionLoading], ([newExportLoadingVal, newDuplicateCollectionLoadingVal]) => { diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue b/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue index 5fa4081ab..c42a3c723 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Collection.vue @@ -73,11 +73,7 @@ @keyup.r="requestAction.$el.click()" @keyup.n="folderAction.$el.click()" @keyup.e="edit.$el.click()" - @keyup.d=" - showDuplicateCollectionAction - ? duplicateAction.$el.click() - : null - " + @keyup.d="duplicateAction.$el.click()" @keyup.delete="deleteAction.$el.click()" @keyup.p="propertiesAction.$el.click()" @keyup.escape="hide()" @@ -123,7 +119,6 @@ " /> { return IconFolder }) -const showDuplicateCollectionAction = computed(() => { - // Show if the user is not logged in - if (!currentUser.value) { - return true - } - - // Duplicate collection action is disabled on SH until the issue with syncing is resolved - return !platform.platformFeatureFlags - .duplicateCollectionDisabledInPersonalWorkspace -}) - const pick = () => { emit("select", { pickedType: "gql-my-collection", diff --git a/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue b/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue index a30d8d0d0..3e26b77b9 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/Folder.vue @@ -70,11 +70,7 @@ @keyup.r="requestAction.$el.click()" @keyup.n="folderAction.$el.click()" @keyup.e="edit.$el.click()" - @keyup.d=" - showDuplicateCollectionAction - ? duplicateAction.$el.click() - : null - " + @keyup.d="duplicateAction.$el.click()" @keyup.delete="deleteAction.$el.click()" @keyup.p="propertiesAction.$el.click()" @keyup.escape="hide()" @@ -116,7 +112,6 @@ " /> { return IconFolder }) -const showDuplicateCollectionAction = computed(() => { - // Show if the user is not logged in - if (!currentUser.value) { - return true - } - - // Duplicate collection action is disabled on SH until the issue with syncing is resolved - return !platform.platformFeatureFlags - .duplicateCollectionDisabledInPersonalWorkspace -}) - const pick = () => { emit("select", { pickedType: "gql-my-folder", diff --git a/packages/hoppscotch-common/src/platform/index.ts b/packages/hoppscotch-common/src/platform/index.ts index e2629edb0..fb144c262 100644 --- a/packages/hoppscotch-common/src/platform/index.ts +++ b/packages/hoppscotch-common/src/platform/index.ts @@ -53,12 +53,6 @@ export type PlatformDef = { * Whether to show the A/B testing workspace switcher click login flow or not */ workspaceSwitcherLogin?: Ref - - /** - * There's an active issue wrt syncing in personal workspace under SH while duplicating a collection - * This is a temporary flag to disable the same - */ - duplicateCollectionDisabledInPersonalWorkspace?: boolean } infra?: InfraPlatformDef experiments?: ExperimentsPlatformDef diff --git a/packages/hoppscotch-selfhost-desktop/src/api/subscriptions/UserCollectionCreated.graphql b/packages/hoppscotch-selfhost-desktop/src/api/subscriptions/UserCollectionCreated.graphql index 99870281c..1accfec11 100644 --- a/packages/hoppscotch-selfhost-desktop/src/api/subscriptions/UserCollectionCreated.graphql +++ b/packages/hoppscotch-selfhost-desktop/src/api/subscriptions/UserCollectionCreated.graphql @@ -6,5 +6,6 @@ subscription UserCollectionCreated { id title type + data } } diff --git a/packages/hoppscotch-selfhost-desktop/src/api/subscriptions/UserCollectionDuplicated.graphql b/packages/hoppscotch-selfhost-desktop/src/api/subscriptions/UserCollectionDuplicated.graphql new file mode 100644 index 000000000..401686f96 --- /dev/null +++ b/packages/hoppscotch-selfhost-desktop/src/api/subscriptions/UserCollectionDuplicated.graphql @@ -0,0 +1,15 @@ +subscription UserCollectionDuplicated { + userCollectionDuplicated { + id + parentID + title + type + data + childCollections + requests { + id + request + collectionID + } + } +} diff --git a/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.api.ts b/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.api.ts index 2373d7d06..a41ed3cc7 100644 --- a/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.api.ts +++ b/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.api.ts @@ -4,67 +4,68 @@ import { runMutation, } from "@hoppscotch/common/helpers/backend/GQLClient" import { + CreateGqlChildUserCollectionDocument, + CreateGqlChildUserCollectionMutation, + CreateGqlChildUserCollectionMutationVariables, + CreateGqlRootUserCollectionDocument, + CreateGqlRootUserCollectionMutation, + CreateGqlRootUserCollectionMutationVariables, + CreateGqlUserRequestDocument, + CreateGqlUserRequestMutation, + CreateGqlUserRequestMutationVariables, + CreateRestChildUserCollectionDocument, + CreateRestChildUserCollectionMutation, + CreateRestChildUserCollectionMutationVariables, CreateRestRootUserCollectionDocument, CreateRestRootUserCollectionMutation, CreateRestRootUserCollectionMutationVariables, + CreateRestUserRequestDocument, CreateRestUserRequestMutation, CreateRestUserRequestMutationVariables, - CreateRestUserRequestDocument, - CreateRestChildUserCollectionMutation, - CreateRestChildUserCollectionMutationVariables, - CreateRestChildUserCollectionDocument, + DeleteUserCollectionDocument, DeleteUserCollectionMutation, DeleteUserCollectionMutationVariables, - DeleteUserCollectionDocument, - RenameUserCollectionMutation, - RenameUserCollectionMutationVariables, - RenameUserCollectionDocument, - MoveUserCollectionMutation, - MoveUserCollectionMutationVariables, - MoveUserCollectionDocument, + DeleteUserRequestDocument, DeleteUserRequestMutation, DeleteUserRequestMutationVariables, - DeleteUserRequestDocument, + ExportUserCollectionsToJsonDocument, + ExportUserCollectionsToJsonQuery, + ExportUserCollectionsToJsonQueryVariables, + GetGqlRootUserCollectionsDocument, + GetGqlRootUserCollectionsQuery, + GetGqlRootUserCollectionsQueryVariables, + GetUserRootCollectionsDocument, + GetUserRootCollectionsQuery, + GetUserRootCollectionsQueryVariables, + MoveUserCollectionDocument, + MoveUserCollectionMutation, + MoveUserCollectionMutationVariables, MoveUserRequestDocument, MoveUserRequestMutation, MoveUserRequestMutationVariables, - UpdateUserCollectionOrderMutation, - UpdateUserCollectionOrderMutationVariables, - UpdateUserCollectionOrderDocument, - GetUserRootCollectionsQuery, - GetUserRootCollectionsQueryVariables, - GetUserRootCollectionsDocument, - UserCollectionCreatedDocument, - UserCollectionUpdatedDocument, - UserCollectionRemovedDocument, - UserCollectionMovedDocument, - UserCollectionOrderUpdatedDocument, - ExportUserCollectionsToJsonQuery, - ExportUserCollectionsToJsonQueryVariables, - ExportUserCollectionsToJsonDocument, - UserRequestCreatedDocument, - UserRequestUpdatedDocument, - UserRequestMovedDocument, - UserRequestDeletedDocument, - UpdateRestUserRequestMutation, - UpdateRestUserRequestMutationVariables, - UpdateRestUserRequestDocument, - CreateGqlRootUserCollectionMutation, - CreateGqlRootUserCollectionMutationVariables, - CreateGqlRootUserCollectionDocument, - CreateGqlUserRequestMutation, - CreateGqlUserRequestMutationVariables, - CreateGqlUserRequestDocument, - CreateGqlChildUserCollectionMutation, - CreateGqlChildUserCollectionMutationVariables, - CreateGqlChildUserCollectionDocument, + RenameUserCollectionDocument, + RenameUserCollectionMutation, + RenameUserCollectionMutationVariables, + ReqType, + UpdateGqlUserRequestDocument, UpdateGqlUserRequestMutation, UpdateGqlUserRequestMutationVariables, - UpdateGqlUserRequestDocument, - GetGqlRootUserCollectionsQuery, - GetGqlRootUserCollectionsQueryVariables, - GetGqlRootUserCollectionsDocument, - ReqType, + UpdateRestUserRequestDocument, + UpdateRestUserRequestMutation, + UpdateRestUserRequestMutationVariables, + UpdateUserCollectionOrderDocument, + UpdateUserCollectionOrderMutation, + UpdateUserCollectionOrderMutationVariables, + UserCollectionCreatedDocument, + UserCollectionDuplicatedDocument, + UserCollectionMovedDocument, + UserCollectionOrderUpdatedDocument, + UserCollectionRemovedDocument, + UserCollectionUpdatedDocument, + UserRequestCreatedDocument, + UserRequestDeletedDocument, + UserRequestMovedDocument, + UserRequestUpdatedDocument, } from "../../api/generated/graphql" export const createRESTRootUserCollection = (title: string) => @@ -292,6 +293,12 @@ export const runUserCollectionOrderUpdatedSubscription = () => variables: {}, }) +export const runUserCollectionDuplicatedSubscription = () => + runGQLSubscription({ + query: UserCollectionDuplicatedDocument, + variables: {}, + }) + export const runUserRequestCreatedSubscription = () => runGQLSubscription({ query: UserRequestCreatedDocument, variables: {} }) diff --git a/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.platform.ts b/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.platform.ts index 674f6d83c..847e1bb03 100644 --- a/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.platform.ts +++ b/packages/hoppscotch-selfhost-desktop/src/platform/collections/collections.platform.ts @@ -1,10 +1,11 @@ -import { authEvents$, def as platformAuth } from "@platform/auth" import { CollectionsPlatformDef } from "@hoppscotch/common/platform/collections" +import { authEvents$, def as platformAuth } from "@platform/auth" import { runDispatchWithOutSyncing } from "../../lib/sync" import { exportUserCollectionsToJSON, runUserCollectionCreatedSubscription, + runUserCollectionDuplicatedSubscription, runUserCollectionMovedSubscription, runUserCollectionOrderUpdatedSubscription, runUserCollectionRemovedSubscription, @@ -16,44 +17,51 @@ import { } from "./collections.api" import { collectionsSyncer, getStoreByCollectionType } from "./collections.sync" -import * as E from "fp-ts/Either" -import { - addRESTCollection, - setRESTCollections, - editRESTCollection, - removeRESTCollection, - moveRESTFolder, - updateRESTCollectionOrder, - saveRESTRequestAs, - navigateToFolderWithIndexPath, - editRESTRequest, - removeRESTRequest, - moveRESTRequest, - updateRESTRequestOrder, - addRESTFolder, - editRESTFolder, - removeRESTFolder, - addGraphqlFolder, - addGraphqlCollection, - editGraphqlFolder, - editGraphqlCollection, - removeGraphqlFolder, - removeGraphqlCollection, - saveGraphqlRequestAs, - editGraphqlRequest, - moveGraphqlRequest, - removeGraphqlRequest, - setGraphqlCollections, - restCollectionStore, -} from "@hoppscotch/common/newstore/collections" import { runGQLSubscription } from "@hoppscotch/common/helpers/backend/GQLClient" import { + addGraphqlCollection, + addGraphqlFolder, + addRESTCollection, + addRESTFolder, + editGraphqlCollection, + editGraphqlFolder, + editGraphqlRequest, + editRESTCollection, + editRESTFolder, + editRESTRequest, + moveGraphqlRequest, + moveRESTFolder, + moveRESTRequest, + navigateToFolderWithIndexPath, + removeGraphqlCollection, + removeGraphqlFolder, + removeGraphqlRequest, + removeRESTCollection, + removeRESTFolder, + removeRESTRequest, + restCollectionStore, + saveGraphqlRequestAs, + saveRESTRequestAs, + setGraphqlCollections, + setRESTCollections, + updateRESTCollectionOrder, + updateRESTRequestOrder, +} from "@hoppscotch/common/newstore/collections" +import { + GQLHeader, HoppCollection, HoppGQLRequest, + HoppRESTHeaders, + HoppRESTParam, HoppRESTRequest, } from "@hoppscotch/data" +import * as E from "fp-ts/Either" +import { + ReqType, + UserCollectionDuplicatedData, + UserRequest, +} from "../../api/generated/graphql" import { gqlCollectionsSyncer } from "./gqlCollections.sync" -import { ReqType } from "../../api/generated/graphql" function initCollectionsSync() { const currentUser$ = platformAuth.getCurrentUserStream() @@ -89,6 +97,7 @@ type ExportedUserCollectionREST = { folders: ExportedUserCollectionREST[] requests: Array name: string + data: string } type ExportedUserCollectionGQL = { @@ -96,6 +105,16 @@ type ExportedUserCollectionGQL = { folders: ExportedUserCollectionGQL[] requests: Array name: string + data: string +} + +function addDescriptionField( + candidate: HoppRESTHeaders | GQLHeader[] | HoppRESTParam[] +) { + return candidate.map((item) => ({ + ...item, + description: "description" in item ? item.description : "", + })) } function exportedCollectionToHoppCollection( @@ -105,9 +124,17 @@ function exportedCollectionToHoppCollection( 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: 4, name: restCollection.name, folders: restCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) @@ -133,42 +160,70 @@ function exportedCollectionToHoppCollection( requestVariables, responses, } = request + + const resolvedParams = addDescriptionField(params) + const resolvedHeaders = addDescriptionField(headers) + return { v, id, name, endpoint, method, - params, + params: resolvedParams, + requestVariables, auth, - headers, + headers: resolvedHeaders, body, preRequestScript, testScript, - requestVariables, responses, } }), + auth: data.auth, + headers: addDescriptionField(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: 4, name: gqlCollection.name, folders: gqlCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) ), - requests: gqlCollection.requests.map( - ({ v, auth, headers, name, id }) => ({ + requests: gqlCollection.requests.map((request) => { + const requestParsedResult = HoppGQLRequest.safeParse(request) + if (requestParsedResult.type === "ok") { + return requestParsedResult.value + } + + const { v, auth, headers, name, id, query, url, variables } = request + + const resolvedHeaders = addDescriptionField(headers) + + return { id, v, auth, - headers, + headers: resolvedHeaders, name, - }) - ) as HoppGQLRequest[], + query, + url, + variables, + } + }), + auth: data.auth, + headers: addDescriptionField(data.headers), } } } @@ -178,7 +233,6 @@ async function loadUserCollections(collectionType: "REST" | "GQL") { undefined, collectionType == "REST" ? ReqType.Rest : ReqType.Gql ) - if (E.isRight(res)) { const collectionsJSONString = res.right.exportUserCollectionsToJSON.exportedCollection @@ -187,7 +241,6 @@ async function loadUserCollections(collectionType: "REST" | "GQL") { ExportedUserCollectionGQL | ExportedUserCollectionREST > ).map((collection) => ({ v: 1, ...collection })) - runDispatchWithOutSyncing(() => { collectionType == "REST" ? setRESTCollections( @@ -221,6 +274,9 @@ function setupSubscriptions() { const userCollectionMovedSub = setupUserCollectionMovedSubscription() const userCollectionOrderUpdatedSub = setupUserCollectionOrderUpdatedSubscription() + const userCollectionDuplicatedSub = + setupUserCollectionDuplicatedSubscription() + const userRequestCreatedSub = setupUserRequestCreatedSubscription() const userRequestUpdatedSub = setupUserRequestUpdatedSubscription() const userRequestDeletedSub = setupUserRequestDeletedSubscription() @@ -232,6 +288,7 @@ function setupSubscriptions() { userCollectionRemovedSub, userCollectionMovedSub, userCollectionOrderUpdatedSub, + userCollectionDuplicatedSub, userRequestCreatedSub, userRequestUpdatedSub, userRequestDeletedSub, @@ -302,19 +359,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: 4, + auth: data.auth, + headers: addDescriptionField(data.headers), }) : addRESTCollection({ name: res.right.userCollectionCreated.title, folders: [], requests: [], - v: 1, + v: 4, + auth: data.auth, + headers: addDescriptionField(data.headers), }) const localIndex = collectionStore.value.state.length - 1 @@ -491,6 +561,147 @@ function setupUserCollectionOrderUpdatedSubscription() { return userCollectionOrderUpdatedSub } +function setupUserCollectionDuplicatedSubscription() { + const [userCollectionDuplicated$, userCollectionDuplicatedSub] = + runUserCollectionDuplicatedSubscription() + + userCollectionDuplicated$.subscribe((res) => { + if (E.isRight(res)) { + const { + childCollections: childCollectionsJSONStr, + data, + id, + parentID: parentCollectionID, + requests: userRequests, + title: name, + type: collectionType, + } = res.right.userCollectionDuplicated + + const { collectionStore } = getStoreByCollectionType(collectionType) + + const parentCollectionPath = + parentCollectionID && + getCollectionPathFromCollectionID( + parentCollectionID, + collectionStore.value.state + ) + + // Incoming data transformed to the respective internal representations + const { auth, headers } = + data && data != "null" + ? JSON.parse(data) + : { + auth: { authType: "inherit", authActive: false }, + headers: [], + } + + const folders = transformDuplicatedCollections(childCollectionsJSONStr) + + const requests = transformDuplicatedCollectionRequests( + userRequests as UserRequest[] + ) + + // New collection to be added to store with the transformed data + const effectiveDuplicatedCollection: HoppCollection = { + id, + name, + folders, + requests, + v: 4, + auth, + headers: addDescriptionField(headers), + } + + // only folders will have parent collection id + if (parentCollectionID && parentCollectionPath) { + const collectionCreatedFromStoreIDSuffix = "-duplicate-collection" + + const parentCollection = navigateToFolderWithIndexPath( + collectionStore.value.state, + parentCollectionPath + .split("/") + .map((pathIndex) => parseInt(pathIndex)) + ) + + if (!parentCollection) { + return + } + + // Grab the child collection inserted via store update with the ID suffix + const collectionInsertedViaStoreUpdateIdx = + parentCollection.folders.findIndex(({ id }) => + id?.endsWith(collectionCreatedFromStoreIDSuffix) + ) + + if (collectionInsertedViaStoreUpdateIdx === -1) { + return + } + + const collectionInsertedViaStoreUpdateIndexPath = `${parentCollectionPath}/${collectionInsertedViaStoreUpdateIdx}` + + runDispatchWithOutSyncing(() => { + /** + * Step 1. Remove the collection inserted via store update with the ID suffix + * Step 2. Add the duplicated collection received from the GQL subscription + * Step 3. Update the duplicated collection with the relevant data + */ + + if (collectionType === "GQL") { + removeGraphqlFolder(collectionInsertedViaStoreUpdateIndexPath) + + addGraphqlFolder(name, parentCollectionPath) + + editGraphqlFolder( + collectionInsertedViaStoreUpdateIndexPath, + effectiveDuplicatedCollection + ) + } else { + removeRESTFolder(collectionInsertedViaStoreUpdateIndexPath) + + addRESTFolder(name, parentCollectionPath) + + editRESTFolder( + collectionInsertedViaStoreUpdateIndexPath, + effectiveDuplicatedCollection + ) + } + }) + } else { + // root collections won't have `parentCollectionID` + const collectionCreatedFromStoreIDSuffix = "-duplicate-collection" + + // Grab the child collection inserted via store update with the ID suffix + const collectionInsertedViaStoreUpdateIdx = + collectionStore.value.state.findIndex(({ id }) => + id?.endsWith(collectionCreatedFromStoreIDSuffix) + ) + + if (collectionInsertedViaStoreUpdateIdx === -1) { + return + } + + runDispatchWithOutSyncing(() => { + /** + * Step 1. Remove the collection inserted via store update with the ID suffix + * Step 2. Add the duplicated collection received from the GQL subscription + */ + if (collectionType === "GQL") { + removeGraphqlCollection(collectionInsertedViaStoreUpdateIdx) + + addGraphqlCollection(effectiveDuplicatedCollection) + } else { + removeRESTCollection(collectionInsertedViaStoreUpdateIdx) + + addRESTCollection(effectiveDuplicatedCollection) + } + }) + } + } + }) + + return userCollectionDuplicatedSub +} + function setupUserRequestCreatedSubscription() { const [userRequestCreated$, userRequestCreatedSub] = runUserRequestCreatedSubscription() @@ -797,3 +1008,52 @@ function getRequestIndex( return requestIndex } + +function transformDuplicatedCollections( + collectionsJSONStr: string +): HoppCollection[] { + const parsedCollections: UserCollectionDuplicatedData[] = + JSON.parse(collectionsJSONStr) + + return parsedCollections.map( + ({ + childCollections: childCollectionsJSONStr, + data, + id, + requests: userRequests, + title: name, + }) => { + const { auth, headers } = + data && data !== "null" + ? JSON.parse(data) + : { auth: { authType: "inherit", authActive: false }, headers: [] } + + const folders = transformDuplicatedCollections(childCollectionsJSONStr) + + const requests = transformDuplicatedCollectionRequests(userRequests) + + return { + id, + name, + folders, + requests, + v: 4, + auth, + headers: addDescriptionField(headers), + } + } + ) +} + +function transformDuplicatedCollectionRequests( + requests: UserRequest[] +): HoppRESTRequest[] | HoppGQLRequest[] { + return requests.map(({ id, request }) => { + const parsedRequest = JSON.parse(request) + + return { + ...parsedRequest, + id, + } + }) +} diff --git a/packages/hoppscotch-selfhost-web/src/api/subscriptions/userCollectionDuplicated.graphql b/packages/hoppscotch-selfhost-web/src/api/subscriptions/userCollectionDuplicated.graphql new file mode 100644 index 000000000..401686f96 --- /dev/null +++ b/packages/hoppscotch-selfhost-web/src/api/subscriptions/userCollectionDuplicated.graphql @@ -0,0 +1,15 @@ +subscription UserCollectionDuplicated { + userCollectionDuplicated { + id + parentID + title + type + data + childCollections + requests { + id + request + collectionID + } + } +} diff --git a/packages/hoppscotch-selfhost-web/src/main.ts b/packages/hoppscotch-selfhost-web/src/main.ts index 5aaf62e21..5119e8a34 100644 --- a/packages/hoppscotch-selfhost-web/src/main.ts +++ b/packages/hoppscotch-selfhost-web/src/main.ts @@ -40,7 +40,6 @@ createHoppApp("#app", { platformFeatureFlags: { exportAsGIST: false, hasTelemetry: false, - duplicateCollectionDisabledInPersonalWorkspace: true, }, infra: InfraPlatform, }) diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/collections.api.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/collections.api.ts index 70be93653..2ec1a9ee0 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/collections/collections.api.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/collections.api.ts @@ -39,6 +39,7 @@ import { UserCollectionRemovedDocument, UserCollectionMovedDocument, UserCollectionOrderUpdatedDocument, + UserCollectionDuplicatedDocument, ExportUserCollectionsToJsonQuery, ExportUserCollectionsToJsonQueryVariables, ExportUserCollectionsToJsonDocument, @@ -328,6 +329,12 @@ export const runUserCollectionOrderUpdatedSubscription = () => variables: {}, }) +export const runUserCollectionDuplicatedSubscription = () => + runGQLSubscription({ + query: UserCollectionDuplicatedDocument, + variables: {}, + }) + export const runUserRequestCreatedSubscription = () => runGQLSubscription({ query: UserRequestCreatedDocument, variables: {} }) diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/collections.platform.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/collections.platform.ts index 6cced1d27..2726b7477 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/collections/collections.platform.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/collections.platform.ts @@ -1,10 +1,11 @@ -import { authEvents$, def as platformAuth } from "@platform/auth/auth.platform" import { CollectionsPlatformDef } from "@hoppscotch/common/platform/collections" +import { authEvents$, def as platformAuth } from "@platform/auth/auth.platform" import { runDispatchWithOutSyncing } from "../../lib/sync" import { exportUserCollectionsToJSON, runUserCollectionCreatedSubscription, + runUserCollectionDuplicatedSubscription, runUserCollectionMovedSubscription, runUserCollectionOrderUpdatedSubscription, runUserCollectionRemovedSubscription, @@ -16,37 +17,36 @@ import { } from "./collections.api" import { collectionsSyncer, getStoreByCollectionType } from "./collections.sync" -import * as E from "fp-ts/Either" -import { - addRESTCollection, - setRESTCollections, - editRESTCollection, - removeRESTCollection, - moveRESTFolder, - updateRESTCollectionOrder, - saveRESTRequestAs, - navigateToFolderWithIndexPath, - editRESTRequest, - removeRESTRequest, - moveRESTRequest, - updateRESTRequestOrder, - addRESTFolder, - editRESTFolder, - removeRESTFolder, - addGraphqlFolder, - addGraphqlCollection, - editGraphqlFolder, - editGraphqlCollection, - removeGraphqlFolder, - removeGraphqlCollection, - saveGraphqlRequestAs, - editGraphqlRequest, - moveGraphqlRequest, - removeGraphqlRequest, - setGraphqlCollections, - restCollectionStore, -} from "@hoppscotch/common/newstore/collections" import { runGQLSubscription } from "@hoppscotch/common/helpers/backend/GQLClient" +import { + addGraphqlCollection, + addGraphqlFolder, + addRESTCollection, + addRESTFolder, + editGraphqlCollection, + editGraphqlFolder, + editGraphqlRequest, + editRESTCollection, + editRESTFolder, + editRESTRequest, + moveGraphqlRequest, + moveRESTFolder, + moveRESTRequest, + navigateToFolderWithIndexPath, + removeGraphqlCollection, + removeGraphqlFolder, + removeGraphqlRequest, + removeRESTCollection, + removeRESTFolder, + removeRESTRequest, + restCollectionStore, + saveGraphqlRequestAs, + saveRESTRequestAs, + setGraphqlCollections, + setRESTCollections, + updateRESTCollectionOrder, + updateRESTRequestOrder, +} from "@hoppscotch/common/newstore/collections" import { GQLHeader, HoppCollection, @@ -55,8 +55,13 @@ import { HoppRESTParam, HoppRESTRequest, } from "@hoppscotch/data" +import * as E from "fp-ts/Either" +import { + ReqType, + UserCollectionDuplicatedData, + UserRequest, +} from "../../api/generated/graphql" import { gqlCollectionsSyncer } from "./gqlCollections.sync" -import { ReqType } from "../../api/generated/graphql" function initCollectionsSync() { const currentUser$ = platformAuth.getCurrentUserStream() @@ -269,6 +274,9 @@ function setupSubscriptions() { const userCollectionMovedSub = setupUserCollectionMovedSubscription() const userCollectionOrderUpdatedSub = setupUserCollectionOrderUpdatedSubscription() + const userCollectionDuplicatedSub = + setupUserCollectionDuplicatedSubscription() + const userRequestCreatedSub = setupUserRequestCreatedSubscription() const userRequestUpdatedSub = setupUserRequestUpdatedSubscription() const userRequestDeletedSub = setupUserRequestDeletedSubscription() @@ -280,6 +288,7 @@ function setupSubscriptions() { userCollectionRemovedSub, userCollectionMovedSub, userCollectionOrderUpdatedSub, + userCollectionDuplicatedSub, userRequestCreatedSub, userRequestUpdatedSub, userRequestDeletedSub, @@ -314,20 +323,6 @@ 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( @@ -566,6 +561,147 @@ function setupUserCollectionOrderUpdatedSubscription() { return userCollectionOrderUpdatedSub } +function setupUserCollectionDuplicatedSubscription() { + const [userCollectionDuplicated$, userCollectionDuplicatedSub] = + runUserCollectionDuplicatedSubscription() + + userCollectionDuplicated$.subscribe((res) => { + if (E.isRight(res)) { + const { + childCollections: childCollectionsJSONStr, + data, + id, + parentID: parentCollectionID, + requests: userRequests, + title: name, + type: collectionType, + } = res.right.userCollectionDuplicated + + const { collectionStore } = getStoreByCollectionType(collectionType) + + const parentCollectionPath = + parentCollectionID && + getCollectionPathFromCollectionID( + parentCollectionID, + collectionStore.value.state + ) + + // Incoming data transformed to the respective internal representations + const { auth, headers } = + data && data != "null" + ? JSON.parse(data) + : { + auth: { authType: "inherit", authActive: false }, + headers: [], + } + + const folders = transformDuplicatedCollections(childCollectionsJSONStr) + + const requests = transformDuplicatedCollectionRequests( + userRequests as UserRequest[] + ) + + // New collection to be added to store with the transformed data + const effectiveDuplicatedCollection: HoppCollection = { + id, + name, + folders, + requests, + v: 3, + auth, + headers: addDescriptionField(headers), + } + + // only folders will have parent collection id + if (parentCollectionID && parentCollectionPath) { + const collectionCreatedFromStoreIDSuffix = "-duplicate-collection" + + const parentCollection = navigateToFolderWithIndexPath( + collectionStore.value.state, + parentCollectionPath + .split("/") + .map((pathIndex) => parseInt(pathIndex)) + ) + + if (!parentCollection) { + return + } + + // Grab the child collection inserted via store update with the ID suffix + const collectionInsertedViaStoreUpdateIdx = + parentCollection.folders.findIndex(({ id }) => + id?.endsWith(collectionCreatedFromStoreIDSuffix) + ) + + if (collectionInsertedViaStoreUpdateIdx === -1) { + return + } + + const collectionInsertedViaStoreUpdateIndexPath = `${parentCollectionPath}/${collectionInsertedViaStoreUpdateIdx}` + + runDispatchWithOutSyncing(() => { + /** + * Step 1. Remove the collection inserted via store update with the ID suffix + * Step 2. Add the duplicated collection received from the GQL subscription + * Step 3. Update the duplicated collection with the relevant data + */ + + if (collectionType === "GQL") { + removeGraphqlFolder(collectionInsertedViaStoreUpdateIndexPath) + + addGraphqlFolder(name, parentCollectionPath) + + editGraphqlFolder( + collectionInsertedViaStoreUpdateIndexPath, + effectiveDuplicatedCollection + ) + } else { + removeRESTFolder(collectionInsertedViaStoreUpdateIndexPath) + + addRESTFolder(name, parentCollectionPath) + + editRESTFolder( + collectionInsertedViaStoreUpdateIndexPath, + effectiveDuplicatedCollection + ) + } + }) + } else { + // root collections won't have `parentCollectionID` + const collectionCreatedFromStoreIDSuffix = "-duplicate-collection" + + // Grab the child collection inserted via store update with the ID suffix + const collectionInsertedViaStoreUpdateIdx = + collectionStore.value.state.findIndex(({ id }) => + id?.endsWith(collectionCreatedFromStoreIDSuffix) + ) + + if (collectionInsertedViaStoreUpdateIdx === -1) { + return + } + + runDispatchWithOutSyncing(() => { + /** + * Step 1. Remove the collection inserted via store update with the ID suffix + * Step 2. Add the duplicated collection received from the GQL subscription + */ + if (collectionType === "GQL") { + removeGraphqlCollection(collectionInsertedViaStoreUpdateIdx) + + addGraphqlCollection(effectiveDuplicatedCollection) + } else { + removeRESTCollection(collectionInsertedViaStoreUpdateIdx) + + addRESTCollection(effectiveDuplicatedCollection) + } + }) + } + } + }) + + return userCollectionDuplicatedSub +} + function setupUserRequestCreatedSubscription() { const [userRequestCreated$, userRequestCreatedSub] = runUserRequestCreatedSubscription() @@ -873,104 +1009,51 @@ 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" +function transformDuplicatedCollections( + collectionsJSONStr: string +): HoppCollection[] { + const parsedCollections: UserCollectionDuplicatedData[] = + JSON.parse(collectionsJSONStr) - // Duplicating a child collection - if (parentCollectionID) { - // Get the index path for the parent collection - const parentCollectionPath = getCollectionPathFromCollectionID( - parentCollectionID, - collectionStore.value.state - ) + return parsedCollections.map( + ({ + childCollections: childCollectionsJSONStr, + data, + id, + requests: userRequests, + title: name, + }) => { + const { auth, headers } = + data && data !== "null" + ? JSON.parse(data) + : { auth: { authType: "inherit", authActive: false }, headers: [] } - if (!parentCollectionPath) { - // Indicates the collection received from the GQL subscription should be created in the store - return true - } + const folders = transformDuplicatedCollections(childCollectionsJSONStr) - const parentCollection = navigateToFolderWithIndexPath( - collectionStore.value.state, - parentCollectionPath.split("/").map((index) => parseInt(index)) - ) + const requests = transformDuplicatedCollectionRequests(userRequests) - 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, - }) + return { + id, + name, + folders, + requests, + v: 3, + auth, + headers: addDescriptionField(headers), } - }) - } 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 + ) +} + +function transformDuplicatedCollectionRequests( + requests: UserRequest[] +): HoppRESTRequest[] | HoppGQLRequest[] { + return requests.map(({ id, request }) => { + const parsedRequest = JSON.parse(request) + + return { + ...parsedRequest, + id, + } + }) }