feat: duplicate REST/GraphQL collections (#4211)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com> Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
@@ -21,7 +21,11 @@ import {
|
|||||||
TEAM_MEMBER_NOT_FOUND,
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { PubSubService } from '../pubsub/pubsub.service';
|
import { PubSubService } from '../pubsub/pubsub.service';
|
||||||
import { escapeSqlLikeString, isValidLength } from 'src/utils';
|
import {
|
||||||
|
escapeSqlLikeString,
|
||||||
|
isValidLength,
|
||||||
|
transformCollectionData,
|
||||||
|
} from 'src/utils';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import {
|
import {
|
||||||
@@ -134,11 +138,13 @@ export class TeamCollectionService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const data = transformCollectionData(collection.right.data);
|
||||||
|
|
||||||
const result: CollectionFolder = {
|
const result: CollectionFolder = {
|
||||||
name: collection.right.title,
|
name: collection.right.title,
|
||||||
folders: childrenCollectionObjects,
|
folders: childrenCollectionObjects,
|
||||||
requests: requests.map((x) => x.request),
|
requests: requests.map((x) => x.request),
|
||||||
data: JSON.stringify(collection.right.data),
|
data,
|
||||||
};
|
};
|
||||||
|
|
||||||
return E.right(result);
|
return E.right(result);
|
||||||
@@ -309,11 +315,13 @@ export class TeamCollectionService {
|
|||||||
* @returns TeamCollection model
|
* @returns TeamCollection model
|
||||||
*/
|
*/
|
||||||
private cast(teamCollection: DBTeamCollection): TeamCollection {
|
private cast(teamCollection: DBTeamCollection): TeamCollection {
|
||||||
|
const data = transformCollectionData(teamCollection.data);
|
||||||
|
|
||||||
return <TeamCollection>{
|
return <TeamCollection>{
|
||||||
id: teamCollection.id,
|
id: teamCollection.id,
|
||||||
title: teamCollection.title,
|
title: teamCollection.title,
|
||||||
parentID: teamCollection.parentID,
|
parentID: teamCollection.parentID,
|
||||||
data: !teamCollection.data ? null : JSON.stringify(teamCollection.data),
|
data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ import {
|
|||||||
UserCollectionExportJSONData,
|
UserCollectionExportJSONData,
|
||||||
} from './user-collections.model';
|
} from './user-collections.model';
|
||||||
import { ReqType } from 'src/types/RequestTypes';
|
import { ReqType } from 'src/types/RequestTypes';
|
||||||
import { isValidLength, stringToJson } from 'src/utils';
|
import {
|
||||||
|
isValidLength,
|
||||||
|
stringToJson,
|
||||||
|
transformCollectionData,
|
||||||
|
} from 'src/utils';
|
||||||
import { CollectionFolder } from 'src/types/CollectionFolder';
|
import { CollectionFolder } from 'src/types/CollectionFolder';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -43,13 +47,15 @@ export class UserCollectionService {
|
|||||||
* @returns UserCollection model
|
* @returns UserCollection model
|
||||||
*/
|
*/
|
||||||
private cast(collection: UserCollection) {
|
private cast(collection: UserCollection) {
|
||||||
|
const data = transformCollectionData(collection.data);
|
||||||
|
|
||||||
return <UserCollectionModel>{
|
return <UserCollectionModel>{
|
||||||
id: collection.id,
|
id: collection.id,
|
||||||
title: collection.title,
|
title: collection.title,
|
||||||
type: collection.type,
|
type: collection.type,
|
||||||
parentID: collection.parentID,
|
parentID: collection.parentID,
|
||||||
userID: collection.userUid,
|
userID: collection.userUid,
|
||||||
data: !collection.data ? null : JSON.stringify(collection.data),
|
data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,6 +877,8 @@ export class UserCollectionService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const data = transformCollectionData(collection.right.data);
|
||||||
|
|
||||||
const result: CollectionFolder = {
|
const result: CollectionFolder = {
|
||||||
id: collection.right.id,
|
id: collection.right.id,
|
||||||
name: collection.right.title,
|
name: collection.right.title,
|
||||||
@@ -882,7 +890,7 @@ export class UserCollectionService {
|
|||||||
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
data: JSON.stringify(collection.right.data),
|
data,
|
||||||
};
|
};
|
||||||
|
|
||||||
return E.right(result);
|
return E.right(result);
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { ExecutionContext, HttpException } from '@nestjs/common';
|
import { ExecutionContext, HttpException } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import * as A from 'fp-ts/Array';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
import { pipe } from 'fp-ts/lib/function';
|
import { pipe } from 'fp-ts/lib/function';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as T from 'fp-ts/Task';
|
import * as T from 'fp-ts/Task';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
import * as A from 'fp-ts/Array';
|
import { AuthProvider } from './auth/helper';
|
||||||
import { TeamMemberRole } from './team/team.model';
|
|
||||||
import { User } from './user/user.model';
|
|
||||||
import {
|
import {
|
||||||
ENV_EMPTY_AUTH_PROVIDERS,
|
ENV_EMPTY_AUTH_PROVIDERS,
|
||||||
ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
|
ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
|
||||||
ENV_NOT_SUPPORT_AUTH_PROVIDERS,
|
ENV_NOT_SUPPORT_AUTH_PROVIDERS,
|
||||||
JSON_INVALID,
|
JSON_INVALID,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
import { AuthProvider } from './auth/helper';
|
import { TeamMemberRole } from './team/team.model';
|
||||||
import { RESTError } from './types/RESTError';
|
import { RESTError } from './types/RESTError';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -297,3 +297,22 @@ export function calculateExpirationDate(expiresOn: null | number) {
|
|||||||
if (expiresOn === null) return null;
|
if (expiresOn === null) return null;
|
||||||
return new Date(Date.now() + expiresOn * 24 * 60 * 60 * 1000);
|
return new Date(Date.now() + expiresOn * 24 * 60 * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Transforms the collection level properties (authorization & headers) under the `data` field.
|
||||||
|
* Preserves `null` values and prevents duplicate stringification.
|
||||||
|
*
|
||||||
|
* @param {Prisma.JsonValue} collectionData - The team collection data to transform.
|
||||||
|
* @returns {string | null} The transformed team collection data as a string.
|
||||||
|
*/
|
||||||
|
export function transformCollectionData(
|
||||||
|
collectionData: Prisma.JsonValue,
|
||||||
|
): string | null {
|
||||||
|
if (!collectionData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof collectionData === 'string'
|
||||||
|
? collectionData
|
||||||
|
: JSON.stringify(collectionData);
|
||||||
|
}
|
||||||
|
|||||||
@@ -194,7 +194,8 @@
|
|||||||
"save_to_collection": "Save to Collection",
|
"save_to_collection": "Save to Collection",
|
||||||
"select": "Select a Collection",
|
"select": "Select a Collection",
|
||||||
"select_location": "Select location",
|
"select_location": "Select location",
|
||||||
"details": "Details"
|
"details": "Details",
|
||||||
|
"duplicated": "Collection duplicated"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
||||||
|
|||||||
@@ -102,6 +102,11 @@
|
|||||||
@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="
|
||||||
|
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()"
|
||||||
@@ -144,6 +149,20 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-if="showDuplicateCollectionAction"
|
||||||
|
ref="duplicateAction"
|
||||||
|
:icon="IconCopy"
|
||||||
|
:label="t('action.duplicate')"
|
||||||
|
:loading="duplicateCollectionLoading"
|
||||||
|
:shortcut="['D']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('duplicate-collection'),
|
||||||
|
collectionsType === 'my-collections' ? hide() : null
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
ref="exportAction"
|
ref="exportAction"
|
||||||
:icon="IconDownload"
|
:icon="IconDownload"
|
||||||
@@ -229,7 +248,9 @@ import {
|
|||||||
changeCurrentReorderStatus,
|
changeCurrentReorderStatus,
|
||||||
currentReorderingStatus$,
|
currentReorderingStatus$,
|
||||||
} from "~/newstore/reordering"
|
} from "~/newstore/reordering"
|
||||||
|
import { platform } from "~/platform"
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
|
import IconCopy from "~icons/lucide/copy"
|
||||||
import IconDownload from "~icons/lucide/download"
|
import IconDownload from "~icons/lucide/download"
|
||||||
import IconEdit from "~icons/lucide/edit"
|
import IconEdit from "~icons/lucide/edit"
|
||||||
import IconFilePlus from "~icons/lucide/file-plus"
|
import IconFilePlus from "~icons/lucide/file-plus"
|
||||||
@@ -263,6 +284,7 @@ const props = withDefaults(
|
|||||||
hasNoTeamAccess?: boolean
|
hasNoTeamAccess?: boolean
|
||||||
collectionMoveLoading?: string[]
|
collectionMoveLoading?: string[]
|
||||||
isLastItem?: boolean
|
isLastItem?: boolean
|
||||||
|
duplicateCollectionLoading?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: "",
|
id: "",
|
||||||
@@ -274,6 +296,7 @@ const props = withDefaults(
|
|||||||
exportLoading: false,
|
exportLoading: false,
|
||||||
hasNoTeamAccess: false,
|
hasNoTeamAccess: false,
|
||||||
isLastItem: false,
|
isLastItem: false,
|
||||||
|
duplicateLoading: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -283,6 +306,7 @@ const emit = defineEmits<{
|
|||||||
(event: "add-folder"): void
|
(event: "add-folder"): void
|
||||||
(event: "edit-collection"): void
|
(event: "edit-collection"): void
|
||||||
(event: "edit-properties"): void
|
(event: "edit-properties"): void
|
||||||
|
(event: "duplicate-collection"): void
|
||||||
(event: "export-data"): void
|
(event: "export-data"): void
|
||||||
(event: "remove-collection"): void
|
(event: "remove-collection"): void
|
||||||
(event: "drop-event", payload: DataTransfer): void
|
(event: "drop-event", payload: DataTransfer): void
|
||||||
@@ -297,6 +321,7 @@ const tippyActions = ref<HTMLDivElement | null>(null)
|
|||||||
const requestAction = ref<HTMLButtonElement | null>(null)
|
const requestAction = ref<HTMLButtonElement | null>(null)
|
||||||
const folderAction = ref<HTMLButtonElement | null>(null)
|
const folderAction = ref<HTMLButtonElement | null>(null)
|
||||||
const edit = ref<HTMLButtonElement | null>(null)
|
const edit = ref<HTMLButtonElement | null>(null)
|
||||||
|
const duplicateAction = ref<HTMLButtonElement | null>(null)
|
||||||
const deleteAction = ref<HTMLButtonElement | null>(null)
|
const deleteAction = ref<HTMLButtonElement | null>(null)
|
||||||
const exportAction = ref<HTMLButtonElement | null>(null)
|
const exportAction = ref<HTMLButtonElement | null>(null)
|
||||||
const options = ref<TippyComponent | null>(null)
|
const options = ref<TippyComponent | null>(null)
|
||||||
@@ -314,6 +339,11 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
|||||||
parentID: "",
|
parentID: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
// Used to determine if the collection is being dragged to a different destination
|
// Used to determine if the collection is being dragged to a different destination
|
||||||
// This is used to make the highlight effect work
|
// This is used to make the highlight effect work
|
||||||
watch(
|
watch(
|
||||||
@@ -340,10 +370,25 @@ 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.exportLoading, props.duplicateCollectionLoading],
|
||||||
(val) => {
|
([newExportLoadingVal, newDuplicateCollectionLoadingVal]) => {
|
||||||
if (!val) {
|
if (!newExportLoadingVal && !newDuplicateCollectionLoadingVal) {
|
||||||
options.value!.tippy?.hide()
|
options.value!.tippy?.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
|
|||||||
metadata: {
|
metadata: {
|
||||||
id: "hopp_team_collections",
|
id: "hopp_team_collections",
|
||||||
name: "export.as_json",
|
name: "export.as_json",
|
||||||
title: "export.as_json_description",
|
title: "export.as_json",
|
||||||
icon: IconUser,
|
icon: IconUser,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
applicableTo: ["team-workspace"],
|
applicableTo: ["team-workspace"],
|
||||||
@@ -435,18 +435,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (E.isRight(res)) {
|
if (E.isRight(res)) {
|
||||||
const { exportCollectionsToJSON } = res.right
|
initializeDownloadCollection(res.right, "team-collections")
|
||||||
|
|
||||||
if (!JSON.parse(exportCollectionsToJSON).length) {
|
|
||||||
isHoppTeamCollectionExporterInProgress.value = false
|
|
||||||
|
|
||||||
return toast.error(t("error.no_collections_to_export"))
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeDownloadCollection(
|
|
||||||
exportCollectionsToJSON,
|
|
||||||
"team-collections"
|
|
||||||
)
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
@@ -454,7 +443,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
|
|||||||
platform: "rest",
|
platform: "rest",
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.left.error.toString())
|
toast.error(res.left)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,11 +481,6 @@ const HoppGistCollectionsExporter: ImporterOrExporter = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (E.isRight(collectionJSON)) {
|
if (E.isRight(collectionJSON)) {
|
||||||
if (!JSON.parse(collectionJSON.right).length) {
|
|
||||||
isHoppGistCollectionExporterInProgress.value = false
|
|
||||||
return toast.error(t("error.no_collections_to_export"))
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await gistExporter(collectionJSON.right, accessToken)
|
const res = await gistExporter(collectionJSON.right, accessToken)
|
||||||
|
|
||||||
if (E.isLeft(res)) {
|
if (E.isLeft(res)) {
|
||||||
@@ -513,6 +497,8 @@ const HoppGistCollectionsExporter: ImporterOrExporter = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
platform.io.openExternalLink(res.right)
|
platform.io.openExternalLink(res.right)
|
||||||
|
} else {
|
||||||
|
toast.error(collectionJSON.left)
|
||||||
}
|
}
|
||||||
|
|
||||||
isHoppGistCollectionExporterInProgress.value = false
|
isHoppGistCollectionExporterInProgress.value = false
|
||||||
@@ -589,9 +575,7 @@ const getCollectionJSON = async () => {
|
|||||||
props.collectionsType.selectedTeam?.teamID
|
props.collectionsType.selectedTeam?.teamID
|
||||||
)
|
)
|
||||||
|
|
||||||
return E.isRight(res)
|
return E.isRight(res) ? E.right(res.right) : E.left(res.left)
|
||||||
? E.right(res.right.exportCollectionsToJSON)
|
|
||||||
: E.left(res.left)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.collectionsType.type === "my-collections") {
|
if (props.collectionsType.type === "my-collections") {
|
||||||
|
|||||||
@@ -71,6 +71,13 @@
|
|||||||
collection: node.data.data.data,
|
collection: node.data.data.data,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@duplicate-collection="
|
||||||
|
node.data.type === 'collections' &&
|
||||||
|
emit('duplicate-collection', {
|
||||||
|
pathOrID: node.id,
|
||||||
|
collectionSyncID: node.data.data.data.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
@edit-properties="
|
@edit-properties="
|
||||||
node.data.type === 'collections' &&
|
node.data.type === 'collections' &&
|
||||||
emit('edit-properties', {
|
emit('edit-properties', {
|
||||||
@@ -146,6 +153,13 @@
|
|||||||
folder: node.data.data.data,
|
folder: node.data.data.data,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@duplicate-collection="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('duplicate-collection', {
|
||||||
|
pathOrID: node.id,
|
||||||
|
collectionSyncID: node.data.data.data.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
@edit-properties="
|
@edit-properties="
|
||||||
node.data.type === 'folders' &&
|
node.data.type === 'folders' &&
|
||||||
emit('edit-properties', {
|
emit('edit-properties', {
|
||||||
@@ -447,6 +461,13 @@ const emit = defineEmits<{
|
|||||||
folder: HoppCollection
|
folder: HoppCollection
|
||||||
}
|
}
|
||||||
): void
|
): void
|
||||||
|
(
|
||||||
|
event: "duplicate-collection",
|
||||||
|
payload: {
|
||||||
|
pathOrID: string
|
||||||
|
collectionSyncID?: string
|
||||||
|
}
|
||||||
|
): void
|
||||||
(
|
(
|
||||||
event: "edit-properties",
|
event: "edit-properties",
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
@dragover="handleDragOver($event)"
|
@dragover="handleDragOver($event)"
|
||||||
@dragleave="resetDragState"
|
@dragleave="resetDragState"
|
||||||
@dragend="resetDragState"
|
@dragend="resetDragState"
|
||||||
@contextmenu.prevent="options?.tippy.show()"
|
@contextmenu.prevent="options?.tippy?.show()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
|
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
|
||||||
@@ -112,12 +112,11 @@
|
|||||||
ref="duplicate"
|
ref="duplicate"
|
||||||
:icon="IconCopy"
|
:icon="IconCopy"
|
||||||
:label="t('action.duplicate')"
|
:label="t('action.duplicate')"
|
||||||
:loading="duplicateLoading"
|
:loading="duplicateRequestLoading"
|
||||||
:shortcut="['D']"
|
:shortcut="['D']"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
emit('duplicate-request'),
|
emit('duplicate-request')
|
||||||
collectionsType === 'my-collections' ? hide() : null
|
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -211,7 +210,7 @@ const props = defineProps({
|
|||||||
default: "my-collections",
|
default: "my-collections",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
duplicateLoading: {
|
duplicateRequestLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -259,7 +258,7 @@ const emit = defineEmits<{
|
|||||||
(event: "update-last-request-order", payload: DataTransfer): void
|
(event: "update-last-request-order", payload: DataTransfer): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const tippyActions = ref<TippyComponent | null>(null)
|
const tippyActions = ref<HTMLButtonElement | null>(null)
|
||||||
const edit = ref<HTMLButtonElement | null>(null)
|
const edit = ref<HTMLButtonElement | null>(null)
|
||||||
const deleteAction = ref<HTMLButtonElement | null>(null)
|
const deleteAction = ref<HTMLButtonElement | null>(null)
|
||||||
const options = ref<TippyComponent | null>(null)
|
const options = ref<TippyComponent | null>(null)
|
||||||
@@ -277,10 +276,10 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.duplicateLoading,
|
() => props.duplicateRequestLoading,
|
||||||
(val) => {
|
(val) => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
options.value!.tippy.hide()
|
options.value!.tippy?.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
:export-loading="exportLoading"
|
:export-loading="exportLoading"
|
||||||
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
||||||
:collection-move-loading="collectionMoveLoading"
|
:collection-move-loading="collectionMoveLoading"
|
||||||
|
:duplicate-collection-loading="duplicateCollectionLoading"
|
||||||
:is-last-item="node.data.isLastItem"
|
:is-last-item="node.data.isLastItem"
|
||||||
:is-selected="
|
:is-selected="
|
||||||
isSelected({
|
isSelected({
|
||||||
@@ -89,6 +90,12 @@
|
|||||||
collection: node.data.data.data,
|
collection: node.data.data.data,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@duplicate-collection="
|
||||||
|
node.data.type === 'collections' &&
|
||||||
|
emit('duplicate-collection', {
|
||||||
|
pathOrID: node.data.data.data.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
@edit-properties="
|
@edit-properties="
|
||||||
node.data.type === 'collections' &&
|
node.data.type === 'collections' &&
|
||||||
emit('edit-properties', {
|
emit('edit-properties', {
|
||||||
@@ -149,6 +156,7 @@
|
|||||||
:export-loading="exportLoading"
|
:export-loading="exportLoading"
|
||||||
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
||||||
:collection-move-loading="collectionMoveLoading"
|
:collection-move-loading="collectionMoveLoading"
|
||||||
|
:duplicate-collection-loading="duplicateCollectionLoading"
|
||||||
:is-last-item="node.data.isLastItem"
|
:is-last-item="node.data.isLastItem"
|
||||||
:is-selected="
|
:is-selected="
|
||||||
isSelected({
|
isSelected({
|
||||||
@@ -176,6 +184,12 @@
|
|||||||
folder: node.data.data.data,
|
folder: node.data.data.data,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@duplicate-collection="
|
||||||
|
node.data.type === 'folders' &&
|
||||||
|
emit('duplicate-collection', {
|
||||||
|
pathOrID: node.data.data.data.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
@edit-properties="
|
@edit-properties="
|
||||||
node.data.type === 'folders' &&
|
node.data.type === 'folders' &&
|
||||||
emit('edit-properties', {
|
emit('edit-properties', {
|
||||||
@@ -236,7 +250,7 @@
|
|||||||
:request-i-d="node.data.data.data.id"
|
:request-i-d="node.data.data.data.id"
|
||||||
:parent-i-d="node.data.data.parentIndex"
|
:parent-i-d="node.data.data.parentIndex"
|
||||||
:collections-type="collectionsType.type"
|
:collections-type="collectionsType.type"
|
||||||
:duplicate-loading="duplicateLoading"
|
:duplicate-request-loading="duplicateRequestLoading"
|
||||||
:is-active="isActiveRequest(node.data.data.data.id)"
|
:is-active="isActiveRequest(node.data.data.data.id)"
|
||||||
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
||||||
:request-move-loading="requestMoveLoading"
|
:request-move-loading="requestMoveLoading"
|
||||||
@@ -445,7 +459,12 @@ const props = defineProps({
|
|||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
duplicateLoading: {
|
duplicateRequestLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
duplicateCollectionLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -497,6 +516,13 @@ const emit = defineEmits<{
|
|||||||
folder: TeamCollection
|
folder: TeamCollection
|
||||||
}
|
}
|
||||||
): void
|
): void
|
||||||
|
(
|
||||||
|
event: "duplicate-collection",
|
||||||
|
payload: {
|
||||||
|
pathOrID: string
|
||||||
|
collectionSyncID?: string
|
||||||
|
}
|
||||||
|
): void
|
||||||
(
|
(
|
||||||
event: "edit-properties",
|
event: "edit-properties",
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -73,7 +73,13 @@
|
|||||||
@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="
|
||||||
|
showDuplicateCollectionAction
|
||||||
|
? duplicateAction.$el.click()
|
||||||
|
: null
|
||||||
|
"
|
||||||
@keyup.delete="deleteAction.$el.click()"
|
@keyup.delete="deleteAction.$el.click()"
|
||||||
|
@keyup.p="propertiesAction.$el.click()"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
@@ -116,6 +122,22 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-if="showDuplicateCollectionAction"
|
||||||
|
ref="duplicateAction"
|
||||||
|
:icon="IconCopy"
|
||||||
|
:label="t('action.duplicate')"
|
||||||
|
:shortcut="['D']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('duplicate-collection', {
|
||||||
|
path: `${collectionIndex}`,
|
||||||
|
collectionSyncID: collection.id,
|
||||||
|
}),
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
ref="deleteAction"
|
ref="deleteAction"
|
||||||
:icon="IconTrash2"
|
:icon="IconTrash2"
|
||||||
@@ -168,6 +190,7 @@
|
|||||||
@add-request="$emit('add-request', $event)"
|
@add-request="$emit('add-request', $event)"
|
||||||
@add-folder="$emit('add-folder', $event)"
|
@add-folder="$emit('add-folder', $event)"
|
||||||
@edit-folder="$emit('edit-folder', $event)"
|
@edit-folder="$emit('edit-folder', $event)"
|
||||||
|
@duplicate-collection="$emit('duplicate-collection', $event)"
|
||||||
@edit-request="$emit('edit-request', $event)"
|
@edit-request="$emit('edit-request', $event)"
|
||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
@duplicate-request="$emit('duplicate-request', $event)"
|
||||||
@edit-properties="
|
@edit-properties="
|
||||||
@@ -229,24 +252,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue"
|
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
|
||||||
import IconFolder from "~icons/lucide/folder"
|
|
||||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
|
||||||
import IconFilePlus from "~icons/lucide/file-plus"
|
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
|
||||||
import IconEdit from "~icons/lucide/edit"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
|
||||||
import IconSettings2 from "~icons/lucide/settings-2"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { removeGraphqlCollection } from "~/newstore/collections"
|
import { useToast } from "@composables/toast"
|
||||||
import { Picked } from "~/helpers/types/HoppPicked"
|
|
||||||
import { useService } from "dioc/vue"
|
|
||||||
import { GQLTabService } from "~/services/tab/graphql"
|
|
||||||
import { HoppCollection } from "@hoppscotch/data"
|
import { HoppCollection } from "@hoppscotch/data"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { computed, ref } from "vue"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
import { Picked } from "~/helpers/types/HoppPicked"
|
||||||
|
import { removeGraphqlCollection } from "~/newstore/collections"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
import { GQLTabService } from "~/services/tab/graphql"
|
||||||
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
|
import IconCopy from "~icons/lucide/copy"
|
||||||
|
import IconEdit from "~icons/lucide/edit"
|
||||||
|
import IconFilePlus from "~icons/lucide/file-plus"
|
||||||
|
import IconFolder from "~icons/lucide/folder"
|
||||||
|
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||||
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
|
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||||
|
import IconSettings2 from "~icons/lucide/settings-2"
|
||||||
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
picked: Picked | null
|
picked: Picked | null
|
||||||
@@ -271,6 +297,13 @@ const emit = defineEmits<{
|
|||||||
(e: "add-request", i: any): void
|
(e: "add-request", i: any): void
|
||||||
(e: "add-folder", i: any): void
|
(e: "add-folder", i: any): void
|
||||||
(e: "edit-folder", i: any): void
|
(e: "edit-folder", i: any): void
|
||||||
|
(
|
||||||
|
e: "duplicate-collection",
|
||||||
|
payload: {
|
||||||
|
path: string
|
||||||
|
collectionSyncID?: string
|
||||||
|
}
|
||||||
|
): void
|
||||||
(
|
(
|
||||||
e: "edit-properties",
|
e: "edit-properties",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -296,13 +329,20 @@ const options = ref<any | null>(null)
|
|||||||
const requestAction = ref<any | null>(null)
|
const requestAction = ref<any | null>(null)
|
||||||
const folderAction = ref<any | null>(null)
|
const folderAction = ref<any | null>(null)
|
||||||
const edit = ref<any | null>(null)
|
const edit = ref<any | null>(null)
|
||||||
|
const duplicateAction = ref<any | null>(null)
|
||||||
const deleteAction = ref<any | null>(null)
|
const deleteAction = ref<any | null>(null)
|
||||||
|
const propertiesAction = ref<any | null>(null)
|
||||||
|
|
||||||
const showChildren = ref(false)
|
const showChildren = ref(false)
|
||||||
const dragging = ref(false)
|
const dragging = ref(false)
|
||||||
|
|
||||||
const confirmRemove = ref(false)
|
const confirmRemove = ref(false)
|
||||||
|
|
||||||
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
const isSelected = computed(
|
const isSelected = computed(
|
||||||
() =>
|
() =>
|
||||||
props.picked?.pickedType === "gql-my-collection" &&
|
props.picked?.pickedType === "gql-my-collection" &&
|
||||||
@@ -315,6 +355,17 @@ 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",
|
||||||
|
|||||||
@@ -70,7 +70,13 @@
|
|||||||
@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="
|
||||||
|
showDuplicateCollectionAction
|
||||||
|
? duplicateAction.$el.click()
|
||||||
|
: null
|
||||||
|
"
|
||||||
@keyup.delete="deleteAction.$el.click()"
|
@keyup.delete="deleteAction.$el.click()"
|
||||||
|
@keyup.p="propertiesAction.$el.click()"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
@@ -109,6 +115,22 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-if="showDuplicateCollectionAction"
|
||||||
|
ref="duplicateAction"
|
||||||
|
:icon="IconCopy"
|
||||||
|
:label="t('action.duplicate')"
|
||||||
|
:shortcut="['D']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('duplicate-collection', {
|
||||||
|
path: folderPath,
|
||||||
|
collectionSyncID: folder.id,
|
||||||
|
}),
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
ref="deleteAction"
|
ref="deleteAction"
|
||||||
:icon="IconTrash2"
|
:icon="IconTrash2"
|
||||||
@@ -162,6 +184,7 @@
|
|||||||
@add-request="emit('add-request', $event)"
|
@add-request="emit('add-request', $event)"
|
||||||
@add-folder="emit('add-folder', $event)"
|
@add-folder="emit('add-folder', $event)"
|
||||||
@edit-folder="emit('edit-folder', $event)"
|
@edit-folder="emit('edit-folder', $event)"
|
||||||
|
@duplicate-collection="emit('duplicate-collection', $event)"
|
||||||
@edit-request="emit('edit-request', $event)"
|
@edit-request="emit('edit-request', $event)"
|
||||||
@duplicate-request="emit('duplicate-request', $event)"
|
@duplicate-request="emit('duplicate-request', $event)"
|
||||||
@edit-properties="
|
@edit-properties="
|
||||||
@@ -213,24 +236,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconEdit from "~icons/lucide/edit"
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
|
||||||
import IconFilePlus from "~icons/lucide/file-plus"
|
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
|
||||||
import IconFolder from "~icons/lucide/folder"
|
|
||||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
|
||||||
import IconSettings2 from "~icons/lucide/settings-2"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { removeGraphqlFolder } from "~/newstore/collections"
|
import { useToast } from "@composables/toast"
|
||||||
import { computed, ref } from "vue"
|
|
||||||
import { useService } from "dioc/vue"
|
|
||||||
import { GQLTabService } from "~/services/tab/graphql"
|
|
||||||
import { Picked } from "~/helpers/types/HoppPicked"
|
|
||||||
import { HoppCollection } from "@hoppscotch/data"
|
import { HoppCollection } from "@hoppscotch/data"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { computed, ref } from "vue"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
import { Picked } from "~/helpers/types/HoppPicked"
|
||||||
|
import { removeGraphqlFolder } from "~/newstore/collections"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
import { GQLTabService } from "~/services/tab/graphql"
|
||||||
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
|
import IconCopy from "~icons/lucide/copy"
|
||||||
|
import IconEdit from "~icons/lucide/edit"
|
||||||
|
import IconFilePlus from "~icons/lucide/file-plus"
|
||||||
|
import IconFolder from "~icons/lucide/folder"
|
||||||
|
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||||
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
|
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||||
|
import IconSettings2 from "~icons/lucide/settings-2"
|
||||||
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -255,6 +281,7 @@ const emit = defineEmits([
|
|||||||
"edit-request",
|
"edit-request",
|
||||||
"add-folder",
|
"add-folder",
|
||||||
"edit-folder",
|
"edit-folder",
|
||||||
|
"duplicate-collection",
|
||||||
"duplicate-request",
|
"duplicate-request",
|
||||||
"edit-properties",
|
"edit-properties",
|
||||||
"select-request",
|
"select-request",
|
||||||
@@ -267,12 +294,19 @@ const options = ref<any | null>(null)
|
|||||||
const requestAction = ref<any | null>(null)
|
const requestAction = ref<any | null>(null)
|
||||||
const folderAction = ref<any | null>(null)
|
const folderAction = ref<any | null>(null)
|
||||||
const edit = ref<any | null>(null)
|
const edit = ref<any | null>(null)
|
||||||
|
const duplicateAction = ref<any | null>(null)
|
||||||
const deleteAction = ref<any | null>(null)
|
const deleteAction = ref<any | null>(null)
|
||||||
|
const propertiesAction = ref<any | null>(null)
|
||||||
|
|
||||||
const showChildren = ref(false)
|
const showChildren = ref(false)
|
||||||
const dragging = ref(false)
|
const dragging = ref(false)
|
||||||
const confirmRemove = ref(false)
|
const confirmRemove = ref(false)
|
||||||
|
|
||||||
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
const isSelected = computed(
|
const isSelected = computed(
|
||||||
() =>
|
() =>
|
||||||
props.picked?.pickedType === "gql-my-folder" &&
|
props.picked?.pickedType === "gql-my-folder" &&
|
||||||
@@ -285,6 +319,17 @@ 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",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
@add-request="addRequest($event)"
|
@add-request="addRequest($event)"
|
||||||
@add-folder="addFolder($event)"
|
@add-folder="addFolder($event)"
|
||||||
@edit-folder="editFolder($event)"
|
@edit-folder="editFolder($event)"
|
||||||
|
@duplicate-collection="duplicateCollection($event)"
|
||||||
@edit-request="editRequest($event)"
|
@edit-request="editRequest($event)"
|
||||||
@duplicate-request="duplicateRequest($event)"
|
@duplicate-request="duplicateRequest($event)"
|
||||||
@select-collection="$emit('use-collection', collection)"
|
@select-collection="$emit('use-collection', collection)"
|
||||||
@@ -167,6 +168,7 @@ import {
|
|||||||
editGraphqlCollection,
|
editGraphqlCollection,
|
||||||
editGraphqlFolder,
|
editGraphqlFolder,
|
||||||
moveGraphqlRequest,
|
moveGraphqlRequest,
|
||||||
|
duplicateGraphQLCollection,
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
@@ -380,6 +382,14 @@ const editCollection = (
|
|||||||
displayModalEdit(true)
|
displayModalEdit(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duplicateCollection = ({
|
||||||
|
path,
|
||||||
|
collectionSyncID,
|
||||||
|
}: {
|
||||||
|
path: string
|
||||||
|
collectionSyncID?: string
|
||||||
|
}) => duplicateGraphQLCollection(path, collectionSyncID)
|
||||||
|
|
||||||
const onAddRequest = ({
|
const onAddRequest = ({
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
@add-request="addRequest"
|
@add-request="addRequest"
|
||||||
@edit-collection="editCollection"
|
@edit-collection="editCollection"
|
||||||
@edit-folder="editFolder"
|
@edit-folder="editFolder"
|
||||||
|
@duplicate-collection="duplicateCollection"
|
||||||
@edit-properties="editProperties"
|
@edit-properties="editProperties"
|
||||||
@export-data="exportData"
|
@export-data="exportData"
|
||||||
@remove-collection="removeCollection"
|
@remove-collection="removeCollection"
|
||||||
@@ -67,7 +68,8 @@
|
|||||||
"
|
"
|
||||||
:filter-text="filterTexts"
|
:filter-text="filterTexts"
|
||||||
:export-loading="exportLoading"
|
:export-loading="exportLoading"
|
||||||
:duplicate-loading="duplicateLoading"
|
:duplicate-request-loading="duplicateRequestLoading"
|
||||||
|
:duplicate-collection-loading="duplicateCollectionLoading"
|
||||||
:save-request="saveRequest"
|
:save-request="saveRequest"
|
||||||
:picked="picked"
|
:picked="picked"
|
||||||
:collection-move-loading="collectionMoveLoading"
|
:collection-move-loading="collectionMoveLoading"
|
||||||
@@ -76,6 +78,7 @@
|
|||||||
@add-folder="addFolder"
|
@add-folder="addFolder"
|
||||||
@edit-collection="editCollection"
|
@edit-collection="editCollection"
|
||||||
@edit-folder="editFolder"
|
@edit-folder="editFolder"
|
||||||
|
@duplicate-collection="duplicateCollection"
|
||||||
@edit-properties="editProperties"
|
@edit-properties="editProperties"
|
||||||
@export-data="exportData"
|
@export-data="exportData"
|
||||||
@remove-collection="removeCollection"
|
@remove-collection="removeCollection"
|
||||||
@@ -208,6 +211,7 @@ import {
|
|||||||
createChildCollection,
|
createChildCollection,
|
||||||
createNewRootCollection,
|
createNewRootCollection,
|
||||||
deleteCollection,
|
deleteCollection,
|
||||||
|
duplicateTeamCollection,
|
||||||
moveRESTTeamCollection,
|
moveRESTTeamCollection,
|
||||||
updateOrderRESTTeamCollection,
|
updateOrderRESTTeamCollection,
|
||||||
updateTeamCollection,
|
updateTeamCollection,
|
||||||
@@ -240,6 +244,7 @@ import {
|
|||||||
addRESTCollection,
|
addRESTCollection,
|
||||||
addRESTFolder,
|
addRESTFolder,
|
||||||
cascadeParentCollectionForHeaderAuth,
|
cascadeParentCollectionForHeaderAuth,
|
||||||
|
duplicateRESTCollection,
|
||||||
editRESTCollection,
|
editRESTCollection,
|
||||||
editRESTFolder,
|
editRESTFolder,
|
||||||
editRESTRequest,
|
editRESTRequest,
|
||||||
@@ -645,7 +650,8 @@ const isSelected = ({
|
|||||||
|
|
||||||
const modalLoadingState = ref(false)
|
const modalLoadingState = ref(false)
|
||||||
const exportLoading = ref(false)
|
const exportLoading = ref(false)
|
||||||
const duplicateLoading = ref(false)
|
const duplicateRequestLoading = ref(false)
|
||||||
|
const duplicateCollectionLoading = ref(false)
|
||||||
|
|
||||||
const showModalAdd = ref(false)
|
const showModalAdd = ref(false)
|
||||||
const showModalAddRequest = ref(false)
|
const showModalAddRequest = ref(false)
|
||||||
@@ -1044,6 +1050,34 @@ const updateEditingFolder = (newName: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duplicateCollection = async ({
|
||||||
|
pathOrID,
|
||||||
|
collectionSyncID,
|
||||||
|
}: {
|
||||||
|
pathOrID: string
|
||||||
|
collectionSyncID?: string
|
||||||
|
}) => {
|
||||||
|
if (collectionsType.value.type === "my-collections") {
|
||||||
|
duplicateRESTCollection(pathOrID, collectionSyncID)
|
||||||
|
} else if (hasTeamWriteAccess.value) {
|
||||||
|
duplicateCollectionLoading.value = true
|
||||||
|
|
||||||
|
await pipe(
|
||||||
|
duplicateTeamCollection(pathOrID),
|
||||||
|
TE.match(
|
||||||
|
(err: GQLError<string>) => {
|
||||||
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
|
duplicateCollectionLoading.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
toast.success(t("collection.duplicated"))
|
||||||
|
duplicateCollectionLoading.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const editRequest = (payload: {
|
const editRequest = (payload: {
|
||||||
folderPath: string | undefined
|
folderPath: string | undefined
|
||||||
requestIndex: string
|
requestIndex: string
|
||||||
@@ -1149,7 +1183,7 @@ const duplicateRequest = (payload: {
|
|||||||
saveRESTRequestAs(folderPath, newRequest)
|
saveRESTRequestAs(folderPath, newRequest)
|
||||||
toast.success(t("request.duplicated"))
|
toast.success(t("request.duplicated"))
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
duplicateLoading.value = true
|
duplicateRequestLoading.value = true
|
||||||
|
|
||||||
if (!collectionsType.value.selectedTeam) return
|
if (!collectionsType.value.selectedTeam) return
|
||||||
|
|
||||||
@@ -1164,10 +1198,10 @@ const duplicateRequest = (payload: {
|
|||||||
TE.match(
|
TE.match(
|
||||||
(err: GQLError<string>) => {
|
(err: GQLError<string>) => {
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
duplicateLoading.value = false
|
duplicateRequestLoading.value = false
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
duplicateLoading.value = false
|
duplicateRequestLoading.value = false
|
||||||
toast.success(t("request.duplicated"))
|
toast.success(t("request.duplicated"))
|
||||||
displayModalAddRequest(false)
|
displayModalAddRequest(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mutation DuplicateTeamCollection($collectionID: String!) {
|
||||||
|
duplicateTeamCollection(collectionID: $collectionID)
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
import * as A from "fp-ts/Array"
|
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import { pipe, flow } from "fp-ts/function"
|
|
||||||
import {
|
import {
|
||||||
HoppCollection,
|
HoppCollection,
|
||||||
|
HoppRESTAuth,
|
||||||
|
HoppRESTHeaders,
|
||||||
|
HoppRESTRequest,
|
||||||
makeCollection,
|
makeCollection,
|
||||||
translateToNewRequest,
|
translateToNewRequest,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
|
import * as A from "fp-ts/Array"
|
||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import { flow, pipe } from "fp-ts/function"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { getI18n } from "~/modules/i18n"
|
||||||
import { TeamCollection } from "../teams/TeamCollection"
|
import { TeamCollection } from "../teams/TeamCollection"
|
||||||
import { TeamRequest } from "../teams/TeamRequest"
|
import { TeamRequest } from "../teams/TeamRequest"
|
||||||
import { GQLError, runGQLQuery } from "./GQLClient"
|
import { GQLError, runGQLQuery } from "./GQLClient"
|
||||||
@@ -17,6 +23,15 @@ import {
|
|||||||
GetCollectionTitleAndDataDocument,
|
GetCollectionTitleAndDataDocument,
|
||||||
} from "./graphql"
|
} from "./graphql"
|
||||||
|
|
||||||
|
type TeamCollectionJSON = {
|
||||||
|
name: string
|
||||||
|
folders: TeamCollectionJSON[]
|
||||||
|
requests: HoppRESTRequest[]
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionDataProps = { auth: HoppRESTAuth; headers: HoppRESTHeaders }
|
||||||
|
|
||||||
export const BACKEND_PAGE_SIZE = 10
|
export const BACKEND_PAGE_SIZE = 10
|
||||||
|
|
||||||
const getCollectionChildrenIDs = async (collID: string) => {
|
const getCollectionChildrenIDs = async (collID: string) => {
|
||||||
@@ -78,6 +93,68 @@ const getCollectionRequests = async (collID: string) => {
|
|||||||
return E.right(reqList)
|
return E.right(reqList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pick the value from the parsed result if it is successful, otherwise, return the default value
|
||||||
|
const parseWithDefaultValue = <T>(
|
||||||
|
parseResult: z.SafeParseReturnType<T, T>,
|
||||||
|
defaultValue: T
|
||||||
|
): T => (parseResult.success ? parseResult.data : defaultValue)
|
||||||
|
|
||||||
|
// Parse the incoming value for the `data` (authorization/headers) field and obtain the value in the expected format
|
||||||
|
const parseCollectionData = (
|
||||||
|
data: string | Record<string, unknown> | null
|
||||||
|
): CollectionDataProps => {
|
||||||
|
const defaultDataProps: CollectionDataProps = {
|
||||||
|
auth: { authType: "inherit", authActive: true },
|
||||||
|
headers: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return defaultDataProps
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedData: CollectionDataProps | Record<string, unknown> | null
|
||||||
|
|
||||||
|
if (typeof data === "string") {
|
||||||
|
try {
|
||||||
|
parsedData = JSON.parse(data)
|
||||||
|
} catch {
|
||||||
|
return defaultDataProps
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsedData = data
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = parseWithDefaultValue<CollectionDataProps["auth"]>(
|
||||||
|
HoppRESTAuth.safeParse(parsedData?.auth),
|
||||||
|
defaultDataProps.auth
|
||||||
|
)
|
||||||
|
|
||||||
|
const headers = parseWithDefaultValue<CollectionDataProps["headers"]>(
|
||||||
|
HoppRESTHeaders.safeParse(parsedData?.headers),
|
||||||
|
defaultDataProps.headers
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
auth,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transforms the collection JSON string obtained with workspace level export to `HoppRESTCollection`
|
||||||
|
const teamCollectionJSONToHoppRESTColl = (
|
||||||
|
coll: TeamCollectionJSON
|
||||||
|
): HoppCollection => {
|
||||||
|
const { auth, headers } = parseCollectionData(coll.data)
|
||||||
|
|
||||||
|
return makeCollection({
|
||||||
|
name: coll.name,
|
||||||
|
folders: coll.folders.map(teamCollectionJSONToHoppRESTColl),
|
||||||
|
requests: coll.requests,
|
||||||
|
auth,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const getCompleteCollectionTree = (
|
export const getCompleteCollectionTree = (
|
||||||
collID: string
|
collID: string
|
||||||
): TE.TaskEither<GQLError<string>, TeamCollection> =>
|
): TE.TaskEither<GQLError<string>, TeamCollection> =>
|
||||||
@@ -146,10 +223,26 @@ export const teamCollToHoppRESTColl = (
|
|||||||
* @param teamID - ID of the team
|
* @param teamID - ID of the team
|
||||||
* @returns Either of the JSON string of the collection or the error
|
* @returns Either of the JSON string of the collection or the error
|
||||||
*/
|
*/
|
||||||
export const getTeamCollectionJSON = async (teamID: string) =>
|
export const getTeamCollectionJSON = async (teamID: string) => {
|
||||||
await runGQLQuery({
|
const data = await runGQLQuery({
|
||||||
query: ExportAsJsonDocument,
|
query: ExportAsJsonDocument,
|
||||||
variables: {
|
variables: {
|
||||||
teamID,
|
teamID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (E.isLeft(data)) {
|
||||||
|
return E.left(data.left.error.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
const collections = JSON.parse(data.right.exportCollectionsToJSON)
|
||||||
|
|
||||||
|
if (!collections.length) {
|
||||||
|
const t = getI18n()
|
||||||
|
|
||||||
|
return E.left(t("error.no_collections_to_export"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoppCollections = collections.map(teamCollectionJSONToHoppRESTColl)
|
||||||
|
return E.right(JSON.stringify(hoppCollections))
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
DeleteCollectionDocument,
|
DeleteCollectionDocument,
|
||||||
DeleteCollectionMutation,
|
DeleteCollectionMutation,
|
||||||
DeleteCollectionMutationVariables,
|
DeleteCollectionMutationVariables,
|
||||||
|
DuplicateTeamCollectionDocument,
|
||||||
|
DuplicateTeamCollectionMutation,
|
||||||
|
DuplicateTeamCollectionMutationVariables,
|
||||||
ImportFromJsonDocument,
|
ImportFromJsonDocument,
|
||||||
ImportFromJsonMutation,
|
ImportFromJsonMutation,
|
||||||
ImportFromJsonMutationVariables,
|
ImportFromJsonMutationVariables,
|
||||||
@@ -140,3 +143,12 @@ export const updateTeamCollection = (
|
|||||||
data,
|
data,
|
||||||
newTitle,
|
newTitle,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const duplicateTeamCollection = (collectionID: string) =>
|
||||||
|
runMutation<
|
||||||
|
DuplicateTeamCollectionMutation,
|
||||||
|
DuplicateTeamCollectionMutationVariables,
|
||||||
|
""
|
||||||
|
>(DuplicateTeamCollectionDocument, {
|
||||||
|
collectionID,
|
||||||
|
})
|
||||||
|
|||||||
@@ -324,10 +324,10 @@ export default class NewTeamCollectionAdapter {
|
|||||||
|
|
||||||
if (!parentCollection) return
|
if (!parentCollection) return
|
||||||
|
|
||||||
|
// Prevent adding child collections to a collection that has not been expanded yet incoming from GQL subscription, during import, etc
|
||||||
|
// Hence, add entries to the pre-existing list without setting 'children' if it is `null'
|
||||||
if (parentCollection.children !== null) {
|
if (parentCollection.children !== null) {
|
||||||
parentCollection.children.push(collection)
|
parentCollection.children.push(collection)
|
||||||
} else {
|
|
||||||
parentCollection.children = [collection]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { pluck } from "rxjs/operators"
|
|
||||||
import {
|
import {
|
||||||
HoppGQLRequest,
|
|
||||||
HoppRESTRequest,
|
|
||||||
HoppCollection,
|
HoppCollection,
|
||||||
makeCollection,
|
|
||||||
HoppGQLAuth,
|
HoppGQLAuth,
|
||||||
|
HoppGQLRequest,
|
||||||
|
HoppRESTAuth,
|
||||||
|
HoppRESTHeaders,
|
||||||
|
HoppRESTRequest,
|
||||||
|
makeCollection,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
|
import { pluck } from "rxjs/operators"
|
||||||
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
|
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
|
||||||
import { getService } from "~/modules/dioc"
|
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
|
||||||
import { HoppRESTAuth } from "@hoppscotch/data"
|
|
||||||
import { HoppRESTHeaders } from "@hoppscotch/data"
|
|
||||||
import { HoppGQLHeader } from "~/helpers/graphql"
|
import { HoppGQLHeader } from "~/helpers/graphql"
|
||||||
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
|
import { getService } from "~/modules/dioc"
|
||||||
|
import { getI18n } from "~/modules/i18n"
|
||||||
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||||
|
|
||||||
const defaultRESTCollectionState = {
|
const defaultRESTCollectionState = {
|
||||||
state: [
|
state: [
|
||||||
@@ -494,6 +495,50 @@ const restCollectionDispatchers = defineDispatchers({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
duplicateCollection(
|
||||||
|
{ state }: RESTCollectionStoreType,
|
||||||
|
// `collectionSyncID` is used to sync the duplicated collection in `collections.sync.ts`
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
{ path, collectionSyncID }: { path: string; collectionSyncID?: string }
|
||||||
|
) {
|
||||||
|
const t = getI18n()
|
||||||
|
|
||||||
|
const newState = state
|
||||||
|
|
||||||
|
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||||
|
|
||||||
|
const isRootCollection = indexPaths.length === 1
|
||||||
|
|
||||||
|
const collection = navigateToFolderWithIndexPath(state, [...indexPaths])
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const name = `${collection.name} - ${t("action.duplicate")}`
|
||||||
|
|
||||||
|
const duplicatedCollection = {
|
||||||
|
...cloneDeep(collection),
|
||||||
|
name,
|
||||||
|
...(collection.id
|
||||||
|
? { id: `${collection.id}-duplicate-collection` }
|
||||||
|
: {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRootCollection) {
|
||||||
|
newState.push(duplicatedCollection)
|
||||||
|
} else {
|
||||||
|
const parentCollectionIndexPath = indexPaths.slice(0, -1)
|
||||||
|
|
||||||
|
const parentCollection = navigateToFolderWithIndexPath(state, [
|
||||||
|
...parentCollectionIndexPath,
|
||||||
|
])
|
||||||
|
parentCollection?.folders.push(duplicatedCollection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: newState,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
editRequest(
|
editRequest(
|
||||||
{ state }: RESTCollectionStoreType,
|
{ state }: RESTCollectionStoreType,
|
||||||
{
|
{
|
||||||
@@ -896,6 +941,50 @@ const gqlCollectionDispatchers = defineDispatchers({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
duplicateCollection(
|
||||||
|
{ state }: GraphqlCollectionStoreType,
|
||||||
|
// `collectionSyncID` is used to sync the duplicated collection in `gqlCollections.sync.ts`
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
{ path, collectionSyncID }: { path: string; collectionSyncID?: string }
|
||||||
|
) {
|
||||||
|
const t = getI18n()
|
||||||
|
|
||||||
|
const newState = state
|
||||||
|
|
||||||
|
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||||
|
|
||||||
|
const isRootCollection = indexPaths.length === 1
|
||||||
|
|
||||||
|
const collection = navigateToFolderWithIndexPath(state, [...indexPaths])
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
const name = `${collection.name} - ${t("action.duplicate")}`
|
||||||
|
|
||||||
|
const duplicatedCollection = {
|
||||||
|
...cloneDeep(collection),
|
||||||
|
name,
|
||||||
|
...(collection.id
|
||||||
|
? { id: `${collection.id}-duplicate-collection` }
|
||||||
|
: {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRootCollection) {
|
||||||
|
newState.push(duplicatedCollection)
|
||||||
|
} else {
|
||||||
|
const parentCollectionIndexPath = indexPaths.slice(0, -1)
|
||||||
|
|
||||||
|
const parentCollection = navigateToFolderWithIndexPath(state, [
|
||||||
|
...parentCollectionIndexPath,
|
||||||
|
])
|
||||||
|
parentCollection?.folders.push(duplicatedCollection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: newState,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
editRequest(
|
editRequest(
|
||||||
{ state }: GraphqlCollectionStoreType,
|
{ state }: GraphqlCollectionStoreType,
|
||||||
{
|
{
|
||||||
@@ -1162,6 +1251,19 @@ export function moveRESTFolder(path: string, destinationPath: string | null) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function duplicateRESTCollection(
|
||||||
|
path: string,
|
||||||
|
collectionSyncID?: string
|
||||||
|
) {
|
||||||
|
restCollectionStore.dispatch({
|
||||||
|
dispatcher: "duplicateCollection",
|
||||||
|
payload: {
|
||||||
|
path,
|
||||||
|
collectionSyncID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function removeDuplicateRESTCollectionOrFolder(
|
export function removeDuplicateRESTCollectionOrFolder(
|
||||||
id: string,
|
id: string,
|
||||||
collectionPath: string,
|
collectionPath: string,
|
||||||
@@ -1362,6 +1464,19 @@ export function removeGraphqlFolder(path: string, folderID?: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function duplicateGraphQLCollection(
|
||||||
|
path: string,
|
||||||
|
collectionSyncID?: string
|
||||||
|
) {
|
||||||
|
graphqlCollectionStore.dispatch({
|
||||||
|
dispatcher: "duplicateCollection",
|
||||||
|
payload: {
|
||||||
|
path,
|
||||||
|
collectionSyncID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function removeDuplicateGraphqlCollectionOrFolder(
|
export function removeDuplicateGraphqlCollectionOrFolder(
|
||||||
id: string,
|
id: string,
|
||||||
collectionPath: string,
|
collectionPath: string,
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,7 +276,8 @@ const HoppGQLSaveContextSchema = z.nullable(
|
|||||||
.object({
|
.object({
|
||||||
originLocation: z.literal("user-collection"),
|
originLocation: z.literal("user-collection"),
|
||||||
folderPath: z.string(),
|
folderPath: z.string(),
|
||||||
requestIndex: z.number(),
|
// TODO: Investigate why this field is not populated at times
|
||||||
|
requestIndex: z.optional(z.number()),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
z
|
z
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mutation DuplicateUserCollection($collectionID: String!, $reqType: ReqType!) {
|
||||||
|
duplicateUserCollection(collectionID: $collectionID, reqType: $reqType)
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ createHoppApp("#app", {
|
|||||||
platformFeatureFlags: {
|
platformFeatureFlags: {
|
||||||
exportAsGIST: false,
|
exportAsGIST: false,
|
||||||
hasTelemetry: false,
|
hasTelemetry: false,
|
||||||
|
duplicateCollectionDisabledInPersonalWorkspace: true,
|
||||||
},
|
},
|
||||||
infra: InfraPlatform,
|
infra: InfraPlatform,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ import {
|
|||||||
UpdateUserCollectionMutation,
|
UpdateUserCollectionMutation,
|
||||||
UpdateUserCollectionMutationVariables,
|
UpdateUserCollectionMutationVariables,
|
||||||
UpdateUserCollectionDocument,
|
UpdateUserCollectionDocument,
|
||||||
|
DuplicateUserCollectionDocument,
|
||||||
|
DuplicateUserCollectionMutation,
|
||||||
|
DuplicateUserCollectionMutationVariables,
|
||||||
} from "../../api/generated/graphql"
|
} from "../../api/generated/graphql"
|
||||||
|
|
||||||
export const createRESTRootUserCollection = (title: string, data?: string) =>
|
export const createRESTRootUserCollection = (title: string, data?: string) =>
|
||||||
@@ -193,6 +196,19 @@ export const moveUserCollection = (
|
|||||||
destCollectionID: destinationCollectionID,
|
destCollectionID: destinationCollectionID,
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
export const duplicateUserCollection = (
|
||||||
|
collectionID: string,
|
||||||
|
reqType: ReqType
|
||||||
|
) =>
|
||||||
|
runMutation<
|
||||||
|
DuplicateUserCollectionMutation,
|
||||||
|
DuplicateUserCollectionMutationVariables,
|
||||||
|
""
|
||||||
|
>(DuplicateUserCollectionDocument, {
|
||||||
|
collectionID,
|
||||||
|
reqType,
|
||||||
|
})()
|
||||||
|
|
||||||
export const editUserRequest = (
|
export const editUserRequest = (
|
||||||
requestID: string,
|
requestID: string,
|
||||||
title: string,
|
title: string,
|
||||||
|
|||||||
@@ -284,6 +284,20 @@ 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(
|
||||||
@@ -828,3 +842,105 @@ function getRequestIndex(
|
|||||||
|
|
||||||
return requestIndex
|
return requestIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function issueBackendIDToDuplicatedCollection(
|
||||||
|
collectionStore: ReturnType<
|
||||||
|
typeof getStoreByCollectionType
|
||||||
|
>["collectionStore"],
|
||||||
|
collectionType: ReqType,
|
||||||
|
userCollectionBackendID: string,
|
||||||
|
parentCollectionID?: string
|
||||||
|
): boolean {
|
||||||
|
// Collection added to store via duplicating is set an ID with a suffix to be updated after the backend ID is received from the GQL subscription
|
||||||
|
const collectionCreatedFromStoreIDSuffix = "-duplicate-collection"
|
||||||
|
|
||||||
|
// Duplicating a child collection
|
||||||
|
if (parentCollectionID) {
|
||||||
|
// Get the index path for the parent collection
|
||||||
|
const parentCollectionPath = getCollectionPathFromCollectionID(
|
||||||
|
parentCollectionID,
|
||||||
|
collectionStore.value.state
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!parentCollectionPath) {
|
||||||
|
// Indicates the collection received from the GQL subscription should be created in the store
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentCollection = navigateToFolderWithIndexPath(
|
||||||
|
collectionStore.value.state,
|
||||||
|
parentCollectionPath.split("/").map((index) => parseInt(index))
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!parentCollection) {
|
||||||
|
// Indicates the collection received from the GQL subscription should be created in the store
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the child collection inserted via store update with the ID suffix
|
||||||
|
const collectionInsertedViaStoreUpdateIdx =
|
||||||
|
parentCollection.folders.findIndex(({ id }) =>
|
||||||
|
id?.endsWith(collectionCreatedFromStoreIDSuffix)
|
||||||
|
)
|
||||||
|
|
||||||
|
// No entry indicates the GQL subscription was fired not because of a duplicate collection action
|
||||||
|
if (collectionInsertedViaStoreUpdateIdx === -1) {
|
||||||
|
// Indicates the collection received from the GQL subscription should be created in the store
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const collectionInsertedViaStoreUpdate =
|
||||||
|
parentCollection.folders[collectionInsertedViaStoreUpdateIdx]
|
||||||
|
|
||||||
|
const childCollectionPath = `${parentCollectionPath}/${collectionInsertedViaStoreUpdateIdx}`
|
||||||
|
|
||||||
|
// Update the ID for the child collection already existing in store with the backend ID
|
||||||
|
runDispatchWithOutSyncing(() => {
|
||||||
|
if (collectionType == ReqType.Rest) {
|
||||||
|
editRESTFolder(childCollectionPath, {
|
||||||
|
...collectionInsertedViaStoreUpdate,
|
||||||
|
id: userCollectionBackendID,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
editGraphqlFolder(childCollectionPath, {
|
||||||
|
...collectionInsertedViaStoreUpdate,
|
||||||
|
id: userCollectionBackendID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Duplicating a root collection
|
||||||
|
|
||||||
|
// Grab the collection inserted via store update with the ID suffix
|
||||||
|
const collectionInsertedViaStoreUpdateIdx =
|
||||||
|
collectionStore.value.state.findIndex(({ id }) =>
|
||||||
|
id?.endsWith(collectionCreatedFromStoreIDSuffix)
|
||||||
|
)
|
||||||
|
|
||||||
|
// No entry indicates the GQL subscription was fired not because of a duplicate collection action
|
||||||
|
if (collectionInsertedViaStoreUpdateIdx === -1) {
|
||||||
|
// Indicates the collection received from the GQL subscription should be created in the store
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionInsertedViaStoreUpdate =
|
||||||
|
collectionStore.value.state[collectionInsertedViaStoreUpdateIdx]
|
||||||
|
|
||||||
|
// Update the ID for the collection already existing in store with the backend ID
|
||||||
|
runDispatchWithOutSyncing(() => {
|
||||||
|
if (collectionType == ReqType.Rest) {
|
||||||
|
editRESTCollection(collectionInsertedViaStoreUpdateIdx, {
|
||||||
|
...collectionInsertedViaStoreUpdate,
|
||||||
|
id: userCollectionBackendID,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
editGraphqlCollection(collectionInsertedViaStoreUpdateIdx, {
|
||||||
|
...collectionInsertedViaStoreUpdate,
|
||||||
|
id: userCollectionBackendID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent adding the collection received from GQL subscription to the store
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import {
|
|||||||
|
|
||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
|
||||||
import { getSyncInitFunction } from "../../lib/sync"
|
import { getSyncInitFunction, StoreSyncDefinitionOf } from "../../lib/sync"
|
||||||
|
|
||||||
import { StoreSyncDefinitionOf } from "../../lib/sync"
|
|
||||||
import { createMapper } from "../../lib/sync/mapper"
|
import { createMapper } from "../../lib/sync/mapper"
|
||||||
import {
|
import {
|
||||||
createRESTChildUserCollection,
|
createRESTChildUserCollection,
|
||||||
@@ -21,6 +19,7 @@ import {
|
|||||||
createRESTUserRequest,
|
createRESTUserRequest,
|
||||||
deleteUserCollection,
|
deleteUserCollection,
|
||||||
deleteUserRequest,
|
deleteUserRequest,
|
||||||
|
duplicateUserCollection,
|
||||||
editUserRequest,
|
editUserRequest,
|
||||||
moveUserCollection,
|
moveUserCollection,
|
||||||
moveUserRequest,
|
moveUserRequest,
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
} from "./collections.api"
|
} from "./collections.api"
|
||||||
|
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
|
import { ReqType } from "../../api/generated/graphql"
|
||||||
|
|
||||||
// restCollectionsMapper uses the collectionPath as the local identifier
|
// restCollectionsMapper uses the collectionPath as the local identifier
|
||||||
export const restCollectionsMapper = createMapper<string, string>()
|
export const restCollectionsMapper = createMapper<string, string>()
|
||||||
@@ -280,6 +280,11 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async duplicateCollection({ collectionSyncID }) {
|
||||||
|
if (collectionSyncID) {
|
||||||
|
await duplicateUserCollection(collectionSyncID, ReqType.Rest)
|
||||||
|
}
|
||||||
|
},
|
||||||
editRequest({ path, requestIndex, requestNew }) {
|
editRequest({ path, requestIndex, requestNew }) {
|
||||||
const request = navigateToFolderWithIndexPath(
|
const request = navigateToFolderWithIndexPath(
|
||||||
restCollectionStore.value.state,
|
restCollectionStore.value.state,
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ import {
|
|||||||
createGQLUserRequest,
|
createGQLUserRequest,
|
||||||
deleteUserCollection,
|
deleteUserCollection,
|
||||||
deleteUserRequest,
|
deleteUserRequest,
|
||||||
|
duplicateUserCollection,
|
||||||
editGQLUserRequest,
|
editGQLUserRequest,
|
||||||
updateUserCollection,
|
updateUserCollection,
|
||||||
} from "./collections.api"
|
} from "./collections.api"
|
||||||
|
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
|
import { ReqType } from "../../api/generated/graphql"
|
||||||
import { moveOrReorderRequests } from "./collections.sync"
|
import { moveOrReorderRequests } from "./collections.sync"
|
||||||
|
|
||||||
// gqlCollectionsMapper uses the collectionPath as the local identifier
|
// gqlCollectionsMapper uses the collectionPath as the local identifier
|
||||||
@@ -261,6 +263,11 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||||||
await deleteUserCollection(folderID)
|
await deleteUserCollection(folderID)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async duplicateCollection({ collectionSyncID }) {
|
||||||
|
if (collectionSyncID) {
|
||||||
|
await duplicateUserCollection(collectionSyncID, ReqType.Gql)
|
||||||
|
}
|
||||||
|
},
|
||||||
editRequest({ path, requestIndex, requestNew }) {
|
editRequest({ path, requestIndex, requestNew }) {
|
||||||
const request = navigateToFolderWithIndexPath(
|
const request = navigateToFolderWithIndexPath(
|
||||||
graphqlCollectionStore.value.state,
|
graphqlCollectionStore.value.state,
|
||||||
|
|||||||
Reference in New Issue
Block a user