feat: extend duplicate collection to personal workspace in SH (#4368)

This commit is contained in:
James George
2024-09-30 08:50:19 -07:00
committed by GitHub
parent 58857be650
commit 5e9f8743d4
12 changed files with 624 additions and 295 deletions

View File

@@ -102,11 +102,7 @@
@keyup.r="requestAction?.$el.click()" @keyup.r="requestAction?.$el.click()"
@keyup.n="folderAction?.$el.click()" @keyup.n="folderAction?.$el.click()"
@keyup.e="edit?.$el.click()" @keyup.e="edit?.$el.click()"
@keyup.d=" @keyup.d="duplicateAction?.$el.click()"
showDuplicateCollectionAction
? duplicateAction?.$el.click()
: null
"
@keyup.delete="deleteAction?.$el.click()" @keyup.delete="deleteAction?.$el.click()"
@keyup.x="exportAction?.$el.click()" @keyup.x="exportAction?.$el.click()"
@keyup.p="propertiesAction?.$el.click()" @keyup.p="propertiesAction?.$el.click()"
@@ -150,7 +146,6 @@
" "
/> />
<HoppSmartItem <HoppSmartItem
v-if="showDuplicateCollectionAction"
ref="duplicateAction" ref="duplicateAction"
:icon="IconCopy" :icon="IconCopy"
:label="t('action.duplicate')" :label="t('action.duplicate')"
@@ -370,21 +365,6 @@ const collectionName = computed(() => {
return (props.data as TeamCollection).title 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( watch(
() => [props.exportLoading, props.duplicateCollectionLoading], () => [props.exportLoading, props.duplicateCollectionLoading],
([newExportLoadingVal, newDuplicateCollectionLoadingVal]) => { ([newExportLoadingVal, newDuplicateCollectionLoadingVal]) => {

View File

@@ -73,11 +73,7 @@
@keyup.r="requestAction.$el.click()" @keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()" @keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()" @keyup.e="edit.$el.click()"
@keyup.d=" @keyup.d="duplicateAction.$el.click()"
showDuplicateCollectionAction
? duplicateAction.$el.click()
: null
"
@keyup.delete="deleteAction.$el.click()" @keyup.delete="deleteAction.$el.click()"
@keyup.p="propertiesAction.$el.click()" @keyup.p="propertiesAction.$el.click()"
@keyup.escape="hide()" @keyup.escape="hide()"
@@ -123,7 +119,6 @@
" "
/> />
<HoppSmartItem <HoppSmartItem
v-if="showDuplicateCollectionAction"
ref="duplicateAction" ref="duplicateAction"
:icon="IconCopy" :icon="IconCopy"
:label="t('action.duplicate')" :label="t('action.duplicate')"
@@ -355,17 +350,6 @@ const collectionIcon = computed(() => {
return IconFolder 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 = () => { const pick = () => {
emit("select", { emit("select", {
pickedType: "gql-my-collection", pickedType: "gql-my-collection",

View File

@@ -70,11 +70,7 @@
@keyup.r="requestAction.$el.click()" @keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()" @keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()" @keyup.e="edit.$el.click()"
@keyup.d=" @keyup.d="duplicateAction.$el.click()"
showDuplicateCollectionAction
? duplicateAction.$el.click()
: null
"
@keyup.delete="deleteAction.$el.click()" @keyup.delete="deleteAction.$el.click()"
@keyup.p="propertiesAction.$el.click()" @keyup.p="propertiesAction.$el.click()"
@keyup.escape="hide()" @keyup.escape="hide()"
@@ -116,7 +112,6 @@
" "
/> />
<HoppSmartItem <HoppSmartItem
v-if="showDuplicateCollectionAction"
ref="duplicateAction" ref="duplicateAction"
:icon="IconCopy" :icon="IconCopy"
:label="t('action.duplicate')" :label="t('action.duplicate')"
@@ -319,17 +314,6 @@ const collectionIcon = computed(() => {
return IconFolder 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 = () => { const pick = () => {
emit("select", { emit("select", {
pickedType: "gql-my-folder", pickedType: "gql-my-folder",

View File

@@ -53,12 +53,6 @@ export type PlatformDef = {
* Whether to show the A/B testing workspace switcher click login flow or not * Whether to show the A/B testing workspace switcher click login flow or not
*/ */
workspaceSwitcherLogin?: Ref<boolean> workspaceSwitcherLogin?: Ref<boolean>
/**
* 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 infra?: InfraPlatformDef
experiments?: ExperimentsPlatformDef experiments?: ExperimentsPlatformDef

View File

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

View File

@@ -0,0 +1,15 @@
subscription UserCollectionDuplicated {
userCollectionDuplicated {
id
parentID
title
type
data
childCollections
requests {
id
request
collectionID
}
}
}

View File

@@ -4,67 +4,68 @@ import {
runMutation, runMutation,
} from "@hoppscotch/common/helpers/backend/GQLClient" } from "@hoppscotch/common/helpers/backend/GQLClient"
import { import {
CreateGqlChildUserCollectionDocument,
CreateGqlChildUserCollectionMutation,
CreateGqlChildUserCollectionMutationVariables,
CreateGqlRootUserCollectionDocument,
CreateGqlRootUserCollectionMutation,
CreateGqlRootUserCollectionMutationVariables,
CreateGqlUserRequestDocument,
CreateGqlUserRequestMutation,
CreateGqlUserRequestMutationVariables,
CreateRestChildUserCollectionDocument,
CreateRestChildUserCollectionMutation,
CreateRestChildUserCollectionMutationVariables,
CreateRestRootUserCollectionDocument, CreateRestRootUserCollectionDocument,
CreateRestRootUserCollectionMutation, CreateRestRootUserCollectionMutation,
CreateRestRootUserCollectionMutationVariables, CreateRestRootUserCollectionMutationVariables,
CreateRestUserRequestDocument,
CreateRestUserRequestMutation, CreateRestUserRequestMutation,
CreateRestUserRequestMutationVariables, CreateRestUserRequestMutationVariables,
CreateRestUserRequestDocument, DeleteUserCollectionDocument,
CreateRestChildUserCollectionMutation,
CreateRestChildUserCollectionMutationVariables,
CreateRestChildUserCollectionDocument,
DeleteUserCollectionMutation, DeleteUserCollectionMutation,
DeleteUserCollectionMutationVariables, DeleteUserCollectionMutationVariables,
DeleteUserCollectionDocument, DeleteUserRequestDocument,
RenameUserCollectionMutation,
RenameUserCollectionMutationVariables,
RenameUserCollectionDocument,
MoveUserCollectionMutation,
MoveUserCollectionMutationVariables,
MoveUserCollectionDocument,
DeleteUserRequestMutation, DeleteUserRequestMutation,
DeleteUserRequestMutationVariables, DeleteUserRequestMutationVariables,
DeleteUserRequestDocument, ExportUserCollectionsToJsonDocument,
ExportUserCollectionsToJsonQuery,
ExportUserCollectionsToJsonQueryVariables,
GetGqlRootUserCollectionsDocument,
GetGqlRootUserCollectionsQuery,
GetGqlRootUserCollectionsQueryVariables,
GetUserRootCollectionsDocument,
GetUserRootCollectionsQuery,
GetUserRootCollectionsQueryVariables,
MoveUserCollectionDocument,
MoveUserCollectionMutation,
MoveUserCollectionMutationVariables,
MoveUserRequestDocument, MoveUserRequestDocument,
MoveUserRequestMutation, MoveUserRequestMutation,
MoveUserRequestMutationVariables, MoveUserRequestMutationVariables,
UpdateUserCollectionOrderMutation, RenameUserCollectionDocument,
UpdateUserCollectionOrderMutationVariables, RenameUserCollectionMutation,
UpdateUserCollectionOrderDocument, RenameUserCollectionMutationVariables,
GetUserRootCollectionsQuery, ReqType,
GetUserRootCollectionsQueryVariables, UpdateGqlUserRequestDocument,
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,
UpdateGqlUserRequestMutation, UpdateGqlUserRequestMutation,
UpdateGqlUserRequestMutationVariables, UpdateGqlUserRequestMutationVariables,
UpdateGqlUserRequestDocument, UpdateRestUserRequestDocument,
GetGqlRootUserCollectionsQuery, UpdateRestUserRequestMutation,
GetGqlRootUserCollectionsQueryVariables, UpdateRestUserRequestMutationVariables,
GetGqlRootUserCollectionsDocument, UpdateUserCollectionOrderDocument,
ReqType, UpdateUserCollectionOrderMutation,
UpdateUserCollectionOrderMutationVariables,
UserCollectionCreatedDocument,
UserCollectionDuplicatedDocument,
UserCollectionMovedDocument,
UserCollectionOrderUpdatedDocument,
UserCollectionRemovedDocument,
UserCollectionUpdatedDocument,
UserRequestCreatedDocument,
UserRequestDeletedDocument,
UserRequestMovedDocument,
UserRequestUpdatedDocument,
} from "../../api/generated/graphql" } from "../../api/generated/graphql"
export const createRESTRootUserCollection = (title: string) => export const createRESTRootUserCollection = (title: string) =>
@@ -292,6 +293,12 @@ export const runUserCollectionOrderUpdatedSubscription = () =>
variables: {}, variables: {},
}) })
export const runUserCollectionDuplicatedSubscription = () =>
runGQLSubscription({
query: UserCollectionDuplicatedDocument,
variables: {},
})
export const runUserRequestCreatedSubscription = () => export const runUserRequestCreatedSubscription = () =>
runGQLSubscription({ query: UserRequestCreatedDocument, variables: {} }) runGQLSubscription({ query: UserRequestCreatedDocument, variables: {} })

View File

@@ -1,10 +1,11 @@
import { authEvents$, def as platformAuth } from "@platform/auth"
import { CollectionsPlatformDef } from "@hoppscotch/common/platform/collections" import { CollectionsPlatformDef } from "@hoppscotch/common/platform/collections"
import { authEvents$, def as platformAuth } from "@platform/auth"
import { runDispatchWithOutSyncing } from "../../lib/sync" import { runDispatchWithOutSyncing } from "../../lib/sync"
import { import {
exportUserCollectionsToJSON, exportUserCollectionsToJSON,
runUserCollectionCreatedSubscription, runUserCollectionCreatedSubscription,
runUserCollectionDuplicatedSubscription,
runUserCollectionMovedSubscription, runUserCollectionMovedSubscription,
runUserCollectionOrderUpdatedSubscription, runUserCollectionOrderUpdatedSubscription,
runUserCollectionRemovedSubscription, runUserCollectionRemovedSubscription,
@@ -16,44 +17,51 @@ import {
} from "./collections.api" } from "./collections.api"
import { collectionsSyncer, getStoreByCollectionType } from "./collections.sync" 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 { runGQLSubscription } from "@hoppscotch/common/helpers/backend/GQLClient"
import { 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, HoppCollection,
HoppGQLRequest, HoppGQLRequest,
HoppRESTHeaders,
HoppRESTParam,
HoppRESTRequest, HoppRESTRequest,
} from "@hoppscotch/data" } from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import {
ReqType,
UserCollectionDuplicatedData,
UserRequest,
} from "../../api/generated/graphql"
import { gqlCollectionsSyncer } from "./gqlCollections.sync" import { gqlCollectionsSyncer } from "./gqlCollections.sync"
import { ReqType } from "../../api/generated/graphql"
function initCollectionsSync() { function initCollectionsSync() {
const currentUser$ = platformAuth.getCurrentUserStream() const currentUser$ = platformAuth.getCurrentUserStream()
@@ -89,6 +97,7 @@ type ExportedUserCollectionREST = {
folders: ExportedUserCollectionREST[] folders: ExportedUserCollectionREST[]
requests: Array<HoppRESTRequest & { id: string }> requests: Array<HoppRESTRequest & { id: string }>
name: string name: string
data: string
} }
type ExportedUserCollectionGQL = { type ExportedUserCollectionGQL = {
@@ -96,6 +105,16 @@ type ExportedUserCollectionGQL = {
folders: ExportedUserCollectionGQL[] folders: ExportedUserCollectionGQL[]
requests: Array<HoppGQLRequest & { id: string }> requests: Array<HoppGQLRequest & { id: string }>
name: string name: string
data: string
}
function addDescriptionField(
candidate: HoppRESTHeaders | GQLHeader[] | HoppRESTParam[]
) {
return candidate.map((item) => ({
...item,
description: "description" in item ? item.description : "",
}))
} }
function exportedCollectionToHoppCollection( function exportedCollectionToHoppCollection(
@@ -105,9 +124,17 @@ function exportedCollectionToHoppCollection(
if (collectionType == "REST") { if (collectionType == "REST") {
const restCollection = collection as ExportedUserCollectionREST const restCollection = collection as ExportedUserCollectionREST
const data =
restCollection.data && restCollection.data !== "null"
? JSON.parse(restCollection.data)
: {
auth: { authType: "inherit", authActive: false },
headers: [],
}
return { return {
id: restCollection.id, id: restCollection.id,
v: 1, v: 4,
name: restCollection.name, name: restCollection.name,
folders: restCollection.folders.map((folder) => folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType) exportedCollectionToHoppCollection(folder, collectionType)
@@ -133,42 +160,70 @@ function exportedCollectionToHoppCollection(
requestVariables, requestVariables,
responses, responses,
} = request } = request
const resolvedParams = addDescriptionField(params)
const resolvedHeaders = addDescriptionField(headers)
return { return {
v, v,
id, id,
name, name,
endpoint, endpoint,
method, method,
params, params: resolvedParams,
requestVariables,
auth, auth,
headers, headers: resolvedHeaders,
body, body,
preRequestScript, preRequestScript,
testScript, testScript,
requestVariables,
responses, responses,
} }
}), }),
auth: data.auth,
headers: addDescriptionField(data.headers),
} }
} else { } else {
const gqlCollection = collection as ExportedUserCollectionGQL const gqlCollection = collection as ExportedUserCollectionGQL
const data =
gqlCollection.data && gqlCollection.data !== "null"
? JSON.parse(gqlCollection.data)
: {
auth: { authType: "inherit", authActive: false },
headers: [],
}
return { return {
id: gqlCollection.id, id: gqlCollection.id,
v: 1, v: 4,
name: gqlCollection.name, name: gqlCollection.name,
folders: gqlCollection.folders.map((folder) => folders: gqlCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType) exportedCollectionToHoppCollection(folder, collectionType)
), ),
requests: gqlCollection.requests.map( requests: gqlCollection.requests.map((request) => {
({ v, auth, headers, name, id }) => ({ 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, id,
v, v,
auth, auth,
headers, headers: resolvedHeaders,
name, name,
}) query,
) as HoppGQLRequest[], url,
variables,
}
}),
auth: data.auth,
headers: addDescriptionField(data.headers),
} }
} }
} }
@@ -178,7 +233,6 @@ async function loadUserCollections(collectionType: "REST" | "GQL") {
undefined, undefined,
collectionType == "REST" ? ReqType.Rest : ReqType.Gql collectionType == "REST" ? ReqType.Rest : ReqType.Gql
) )
if (E.isRight(res)) { if (E.isRight(res)) {
const collectionsJSONString = const collectionsJSONString =
res.right.exportUserCollectionsToJSON.exportedCollection res.right.exportUserCollectionsToJSON.exportedCollection
@@ -187,7 +241,6 @@ async function loadUserCollections(collectionType: "REST" | "GQL") {
ExportedUserCollectionGQL | ExportedUserCollectionREST ExportedUserCollectionGQL | ExportedUserCollectionREST
> >
).map((collection) => ({ v: 1, ...collection })) ).map((collection) => ({ v: 1, ...collection }))
runDispatchWithOutSyncing(() => { runDispatchWithOutSyncing(() => {
collectionType == "REST" collectionType == "REST"
? setRESTCollections( ? setRESTCollections(
@@ -221,6 +274,9 @@ function setupSubscriptions() {
const userCollectionMovedSub = setupUserCollectionMovedSubscription() const userCollectionMovedSub = setupUserCollectionMovedSubscription()
const userCollectionOrderUpdatedSub = const userCollectionOrderUpdatedSub =
setupUserCollectionOrderUpdatedSubscription() setupUserCollectionOrderUpdatedSubscription()
const userCollectionDuplicatedSub =
setupUserCollectionDuplicatedSubscription()
const userRequestCreatedSub = setupUserRequestCreatedSubscription() const userRequestCreatedSub = setupUserRequestCreatedSubscription()
const userRequestUpdatedSub = setupUserRequestUpdatedSubscription() const userRequestUpdatedSub = setupUserRequestUpdatedSubscription()
const userRequestDeletedSub = setupUserRequestDeletedSubscription() const userRequestDeletedSub = setupUserRequestDeletedSubscription()
@@ -232,6 +288,7 @@ function setupSubscriptions() {
userCollectionRemovedSub, userCollectionRemovedSub,
userCollectionMovedSub, userCollectionMovedSub,
userCollectionOrderUpdatedSub, userCollectionOrderUpdatedSub,
userCollectionDuplicatedSub,
userRequestCreatedSub, userRequestCreatedSub,
userRequestUpdatedSub, userRequestUpdatedSub,
userRequestDeletedSub, userRequestDeletedSub,
@@ -302,19 +359,32 @@ function setupUserCollectionCreatedSubscription() {
}) })
} else { } else {
// root collections won't have parentCollectionID // 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(() => { runDispatchWithOutSyncing(() => {
collectionType == "GQL" collectionType == "GQL"
? addGraphqlCollection({ ? addGraphqlCollection({
name: res.right.userCollectionCreated.title, name: res.right.userCollectionCreated.title,
folders: [], folders: [],
requests: [], requests: [],
v: 1, v: 4,
auth: data.auth,
headers: addDescriptionField(data.headers),
}) })
: addRESTCollection({ : addRESTCollection({
name: res.right.userCollectionCreated.title, name: res.right.userCollectionCreated.title,
folders: [], folders: [],
requests: [], requests: [],
v: 1, v: 4,
auth: data.auth,
headers: addDescriptionField(data.headers),
}) })
const localIndex = collectionStore.value.state.length - 1 const localIndex = collectionStore.value.state.length - 1
@@ -491,6 +561,147 @@ function setupUserCollectionOrderUpdatedSubscription() {
return userCollectionOrderUpdatedSub 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() { function setupUserRequestCreatedSubscription() {
const [userRequestCreated$, userRequestCreatedSub] = const [userRequestCreated$, userRequestCreatedSub] =
runUserRequestCreatedSubscription() runUserRequestCreatedSubscription()
@@ -797,3 +1008,52 @@ function getRequestIndex(
return requestIndex 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,
}
})
}

View File

@@ -0,0 +1,15 @@
subscription UserCollectionDuplicated {
userCollectionDuplicated {
id
parentID
title
type
data
childCollections
requests {
id
request
collectionID
}
}
}

View File

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

View File

@@ -39,6 +39,7 @@ import {
UserCollectionRemovedDocument, UserCollectionRemovedDocument,
UserCollectionMovedDocument, UserCollectionMovedDocument,
UserCollectionOrderUpdatedDocument, UserCollectionOrderUpdatedDocument,
UserCollectionDuplicatedDocument,
ExportUserCollectionsToJsonQuery, ExportUserCollectionsToJsonQuery,
ExportUserCollectionsToJsonQueryVariables, ExportUserCollectionsToJsonQueryVariables,
ExportUserCollectionsToJsonDocument, ExportUserCollectionsToJsonDocument,
@@ -328,6 +329,12 @@ export const runUserCollectionOrderUpdatedSubscription = () =>
variables: {}, variables: {},
}) })
export const runUserCollectionDuplicatedSubscription = () =>
runGQLSubscription({
query: UserCollectionDuplicatedDocument,
variables: {},
})
export const runUserRequestCreatedSubscription = () => export const runUserRequestCreatedSubscription = () =>
runGQLSubscription({ query: UserRequestCreatedDocument, variables: {} }) runGQLSubscription({ query: UserRequestCreatedDocument, variables: {} })

View File

@@ -1,10 +1,11 @@
import { authEvents$, def as platformAuth } from "@platform/auth/auth.platform"
import { CollectionsPlatformDef } from "@hoppscotch/common/platform/collections" import { CollectionsPlatformDef } from "@hoppscotch/common/platform/collections"
import { authEvents$, def as platformAuth } from "@platform/auth/auth.platform"
import { runDispatchWithOutSyncing } from "../../lib/sync" import { runDispatchWithOutSyncing } from "../../lib/sync"
import { import {
exportUserCollectionsToJSON, exportUserCollectionsToJSON,
runUserCollectionCreatedSubscription, runUserCollectionCreatedSubscription,
runUserCollectionDuplicatedSubscription,
runUserCollectionMovedSubscription, runUserCollectionMovedSubscription,
runUserCollectionOrderUpdatedSubscription, runUserCollectionOrderUpdatedSubscription,
runUserCollectionRemovedSubscription, runUserCollectionRemovedSubscription,
@@ -16,37 +17,36 @@ import {
} from "./collections.api" } from "./collections.api"
import { collectionsSyncer, getStoreByCollectionType } from "./collections.sync" 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 { 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 { import {
GQLHeader, GQLHeader,
HoppCollection, HoppCollection,
@@ -55,8 +55,13 @@ import {
HoppRESTParam, HoppRESTParam,
HoppRESTRequest, HoppRESTRequest,
} from "@hoppscotch/data" } from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import {
ReqType,
UserCollectionDuplicatedData,
UserRequest,
} from "../../api/generated/graphql"
import { gqlCollectionsSyncer } from "./gqlCollections.sync" import { gqlCollectionsSyncer } from "./gqlCollections.sync"
import { ReqType } from "../../api/generated/graphql"
function initCollectionsSync() { function initCollectionsSync() {
const currentUser$ = platformAuth.getCurrentUserStream() const currentUser$ = platformAuth.getCurrentUserStream()
@@ -269,6 +274,9 @@ function setupSubscriptions() {
const userCollectionMovedSub = setupUserCollectionMovedSubscription() const userCollectionMovedSub = setupUserCollectionMovedSubscription()
const userCollectionOrderUpdatedSub = const userCollectionOrderUpdatedSub =
setupUserCollectionOrderUpdatedSubscription() setupUserCollectionOrderUpdatedSubscription()
const userCollectionDuplicatedSub =
setupUserCollectionDuplicatedSubscription()
const userRequestCreatedSub = setupUserRequestCreatedSubscription() const userRequestCreatedSub = setupUserRequestCreatedSubscription()
const userRequestUpdatedSub = setupUserRequestUpdatedSubscription() const userRequestUpdatedSub = setupUserRequestUpdatedSubscription()
const userRequestDeletedSub = setupUserRequestDeletedSubscription() const userRequestDeletedSub = setupUserRequestDeletedSubscription()
@@ -280,6 +288,7 @@ function setupSubscriptions() {
userCollectionRemovedSub, userCollectionRemovedSub,
userCollectionMovedSub, userCollectionMovedSub,
userCollectionOrderUpdatedSub, userCollectionOrderUpdatedSub,
userCollectionDuplicatedSub,
userRequestCreatedSub, userRequestCreatedSub,
userRequestUpdatedSub, userRequestUpdatedSub,
userRequestDeletedSub, userRequestDeletedSub,
@@ -314,20 +323,6 @@ function setupUserCollectionCreatedSubscription() {
return 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 = const parentCollectionPath =
parentCollectionID && parentCollectionID &&
getCollectionPathFromCollectionID( getCollectionPathFromCollectionID(
@@ -566,6 +561,147 @@ function setupUserCollectionOrderUpdatedSubscription() {
return userCollectionOrderUpdatedSub 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() { function setupUserRequestCreatedSubscription() {
const [userRequestCreated$, userRequestCreatedSub] = const [userRequestCreated$, userRequestCreatedSub] =
runUserRequestCreatedSubscription() runUserRequestCreatedSubscription()
@@ -873,104 +1009,51 @@ function getRequestIndex(
return requestIndex return requestIndex
} }
function issueBackendIDToDuplicatedCollection( function transformDuplicatedCollections(
collectionStore: ReturnType< collectionsJSONStr: string
typeof getStoreByCollectionType ): HoppCollection[] {
>["collectionStore"], const parsedCollections: UserCollectionDuplicatedData[] =
collectionType: ReqType, JSON.parse(collectionsJSONStr)
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 return parsedCollections.map(
if (parentCollectionID) { ({
// Get the index path for the parent collection childCollections: childCollectionsJSONStr,
const parentCollectionPath = getCollectionPathFromCollectionID( data,
parentCollectionID, id,
collectionStore.value.state 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: 3,
auth,
headers: addDescriptionField(headers),
}
}
) )
if (!parentCollectionPath) {
// Indicates the collection received from the GQL subscription should be created in the store
return true
} }
const parentCollection = navigateToFolderWithIndexPath( function transformDuplicatedCollectionRequests(
collectionStore.value.state, requests: UserRequest[]
parentCollectionPath.split("/").map((index) => parseInt(index)) ): HoppRESTRequest[] | HoppGQLRequest[] {
) return requests.map(({ id, request }) => {
const parsedRequest = JSON.parse(request)
if (!parentCollection) { return {
// Indicates the collection received from the GQL subscription should be created in the store ...parsedRequest,
return true id,
}
// 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
}