feat: duplicate REST/GraphQL collections (#4211)

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

View File

@@ -102,6 +102,11 @@
@keyup.r="requestAction?.$el.click()"
@keyup.n="folderAction?.$el.click()"
@keyup.e="edit?.$el.click()"
@keyup.d="
showDuplicateCollectionAction
? duplicateAction?.$el.click()
: null
"
@keyup.delete="deleteAction?.$el.click()"
@keyup.x="exportAction?.$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
ref="exportAction"
:icon="IconDownload"
@@ -229,7 +248,9 @@ import {
changeCurrentReorderStatus,
currentReorderingStatus$,
} from "~/newstore/reordering"
import { platform } from "~/platform"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconCopy from "~icons/lucide/copy"
import IconDownload from "~icons/lucide/download"
import IconEdit from "~icons/lucide/edit"
import IconFilePlus from "~icons/lucide/file-plus"
@@ -263,6 +284,7 @@ const props = withDefaults(
hasNoTeamAccess?: boolean
collectionMoveLoading?: string[]
isLastItem?: boolean
duplicateCollectionLoading?: boolean
}>(),
{
id: "",
@@ -274,6 +296,7 @@ const props = withDefaults(
exportLoading: false,
hasNoTeamAccess: false,
isLastItem: false,
duplicateLoading: false,
}
)
@@ -283,6 +306,7 @@ const emit = defineEmits<{
(event: "add-folder"): void
(event: "edit-collection"): void
(event: "edit-properties"): void
(event: "duplicate-collection"): void
(event: "export-data"): void
(event: "remove-collection"): void
(event: "drop-event", payload: DataTransfer): void
@@ -297,6 +321,7 @@ const tippyActions = ref<HTMLDivElement | null>(null)
const requestAction = ref<HTMLButtonElement | null>(null)
const folderAction = ref<HTMLButtonElement | null>(null)
const edit = ref<HTMLButtonElement | null>(null)
const duplicateAction = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null)
const exportAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null)
@@ -314,6 +339,11 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
parentID: "",
})
const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
// Used to determine if the collection is being dragged to a different destination
// This is used to make the highlight effect work
watch(
@@ -340,10 +370,25 @@ const collectionName = computed(() => {
return (props.data as TeamCollection).title
})
const showDuplicateCollectionAction = computed(() => {
// Show if the user is not logged in
if (!currentUser.value) {
return true
}
if (props.collectionsType === "team-collections") {
return true
}
// Duplicate collection action is disabled on SH until the issue with syncing is resolved
return !platform.platformFeatureFlags
.duplicateCollectionDisabledInPersonalWorkspace
})
watch(
() => props.exportLoading,
(val) => {
if (!val) {
() => [props.exportLoading, props.duplicateCollectionLoading],
([newExportLoadingVal, newDuplicateCollectionLoadingVal]) => {
if (!newExportLoadingVal && !newDuplicateCollectionLoadingVal) {
options.value!.tippy?.hide()
}
}

View File

@@ -418,7 +418,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
metadata: {
id: "hopp_team_collections",
name: "export.as_json",
title: "export.as_json_description",
title: "export.as_json",
icon: IconUser,
disabled: false,
applicableTo: ["team-workspace"],
@@ -435,18 +435,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
)
if (E.isRight(res)) {
const { exportCollectionsToJSON } = res.right
if (!JSON.parse(exportCollectionsToJSON).length) {
isHoppTeamCollectionExporterInProgress.value = false
return toast.error(t("error.no_collections_to_export"))
}
initializeDownloadCollection(
exportCollectionsToJSON,
"team-collections"
)
initializeDownloadCollection(res.right, "team-collections")
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
@@ -454,7 +443,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
platform: "rest",
})
} else {
toast.error(res.left.error.toString())
toast.error(res.left)
}
}
@@ -492,11 +481,6 @@ const HoppGistCollectionsExporter: ImporterOrExporter = {
}
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)
if (E.isLeft(res)) {
@@ -513,6 +497,8 @@ const HoppGistCollectionsExporter: ImporterOrExporter = {
})
platform.io.openExternalLink(res.right)
} else {
toast.error(collectionJSON.left)
}
isHoppGistCollectionExporterInProgress.value = false
@@ -589,9 +575,7 @@ const getCollectionJSON = async () => {
props.collectionsType.selectedTeam?.teamID
)
return E.isRight(res)
? E.right(res.right.exportCollectionsToJSON)
: E.left(res.left)
return E.isRight(res) ? E.right(res.right) : E.left(res.left)
}
if (props.collectionsType.type === "my-collections") {

View File

@@ -71,6 +71,13 @@
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="
node.data.type === 'collections' &&
emit('edit-properties', {
@@ -146,6 +153,13 @@
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="
node.data.type === 'folders' &&
emit('edit-properties', {
@@ -447,6 +461,13 @@ const emit = defineEmits<{
folder: HoppCollection
}
): void
(
event: "duplicate-collection",
payload: {
pathOrID: string
collectionSyncID?: string
}
): void
(
event: "edit-properties",
payload: {

View File

@@ -20,7 +20,7 @@
@dragover="handleDragOver($event)"
@dragleave="resetDragState"
@dragend="resetDragState"
@contextmenu.prevent="options?.tippy.show()"
@contextmenu.prevent="options?.tippy?.show()"
>
<div
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
@@ -112,12 +112,11 @@
ref="duplicate"
:icon="IconCopy"
:label="t('action.duplicate')"
:loading="duplicateLoading"
:loading="duplicateRequestLoading"
:shortcut="['D']"
@click="
() => {
emit('duplicate-request'),
collectionsType === 'my-collections' ? hide() : null
emit('duplicate-request')
}
"
/>
@@ -211,7 +210,7 @@ const props = defineProps({
default: "my-collections",
required: true,
},
duplicateLoading: {
duplicateRequestLoading: {
type: Boolean,
default: false,
required: false,
@@ -259,7 +258,7 @@ const emit = defineEmits<{
(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 deleteAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null)
@@ -277,10 +276,10 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
})
watch(
() => props.duplicateLoading,
() => props.duplicateRequestLoading,
(val) => {
if (!val) {
options.value!.tippy.hide()
options.value!.tippy?.hide()
}
}
)

View File

@@ -61,6 +61,7 @@
:export-loading="exportLoading"
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
:collection-move-loading="collectionMoveLoading"
:duplicate-collection-loading="duplicateCollectionLoading"
:is-last-item="node.data.isLastItem"
:is-selected="
isSelected({
@@ -89,6 +90,12 @@
collection: node.data.data.data,
})
"
@duplicate-collection="
node.data.type === 'collections' &&
emit('duplicate-collection', {
pathOrID: node.data.data.data.id,
})
"
@edit-properties="
node.data.type === 'collections' &&
emit('edit-properties', {
@@ -149,6 +156,7 @@
:export-loading="exportLoading"
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
:collection-move-loading="collectionMoveLoading"
:duplicate-collection-loading="duplicateCollectionLoading"
:is-last-item="node.data.isLastItem"
:is-selected="
isSelected({
@@ -176,6 +184,12 @@
folder: node.data.data.data,
})
"
@duplicate-collection="
node.data.type === 'folders' &&
emit('duplicate-collection', {
pathOrID: node.data.data.data.id,
})
"
@edit-properties="
node.data.type === 'folders' &&
emit('edit-properties', {
@@ -236,7 +250,7 @@
:request-i-d="node.data.data.data.id"
:parent-i-d="node.data.data.parentIndex"
:collections-type="collectionsType.type"
:duplicate-loading="duplicateLoading"
:duplicate-request-loading="duplicateRequestLoading"
:is-active="isActiveRequest(node.data.data.data.id)"
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
:request-move-loading="requestMoveLoading"
@@ -445,7 +459,12 @@ const props = defineProps({
default: false,
required: false,
},
duplicateLoading: {
duplicateRequestLoading: {
type: Boolean,
default: false,
required: false,
},
duplicateCollectionLoading: {
type: Boolean,
default: false,
required: false,
@@ -497,6 +516,13 @@ const emit = defineEmits<{
folder: TeamCollection
}
): void
(
event: "duplicate-collection",
payload: {
pathOrID: string
collectionSyncID?: string
}
): void
(
event: "edit-properties",
payload: {

View File

@@ -73,7 +73,13 @@
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.d="
showDuplicateCollectionAction
? duplicateAction.$el.click()
: null
"
@keyup.delete="deleteAction.$el.click()"
@keyup.p="propertiesAction.$el.click()"
@keyup.escape="hide()"
>
<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
ref="deleteAction"
:icon="IconTrash2"
@@ -168,6 +190,7 @@
@add-request="$emit('add-request', $event)"
@add-folder="$emit('add-folder', $event)"
@edit-folder="$emit('edit-folder', $event)"
@duplicate-collection="$emit('duplicate-collection', $event)"
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@edit-properties="
@@ -229,24 +252,27 @@
</template>
<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 { useColorMode } from "@composables/theming"
import { removeGraphqlCollection } from "~/newstore/collections"
import { Picked } from "~/helpers/types/HoppPicked"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
import { useToast } from "@composables/toast"
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<{
picked: Picked | null
@@ -271,6 +297,13 @@ const emit = defineEmits<{
(e: "add-request", i: any): void
(e: "add-folder", i: any): void
(e: "edit-folder", i: any): void
(
e: "duplicate-collection",
payload: {
path: string
collectionSyncID?: string
}
): void
(
e: "edit-properties",
payload: {
@@ -296,13 +329,20 @@ const options = ref<any | null>(null)
const requestAction = ref<any | null>(null)
const folderAction = ref<any | null>(null)
const edit = ref<any | null>(null)
const duplicateAction = ref<any | null>(null)
const deleteAction = ref<any | null>(null)
const propertiesAction = ref<any | null>(null)
const showChildren = ref(false)
const dragging = ref(false)
const confirmRemove = ref(false)
const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
const isSelected = computed(
() =>
props.picked?.pickedType === "gql-my-collection" &&
@@ -315,6 +355,17 @@ const collectionIcon = computed(() => {
return IconFolder
})
const showDuplicateCollectionAction = computed(() => {
// Show if the user is not logged in
if (!currentUser.value) {
return true
}
// Duplicate collection action is disabled on SH until the issue with syncing is resolved
return !platform.platformFeatureFlags
.duplicateCollectionDisabledInPersonalWorkspace
})
const pick = () => {
emit("select", {
pickedType: "gql-my-collection",

View File

@@ -70,7 +70,13 @@
@keyup.r="requestAction.$el.click()"
@keyup.n="folderAction.$el.click()"
@keyup.e="edit.$el.click()"
@keyup.d="
showDuplicateCollectionAction
? duplicateAction.$el.click()
: null
"
@keyup.delete="deleteAction.$el.click()"
@keyup.p="propertiesAction.$el.click()"
@keyup.escape="hide()"
>
<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
ref="deleteAction"
:icon="IconTrash2"
@@ -162,6 +184,7 @@
@add-request="emit('add-request', $event)"
@add-folder="emit('add-folder', $event)"
@edit-folder="emit('edit-folder', $event)"
@duplicate-collection="emit('duplicate-collection', $event)"
@edit-request="emit('edit-request', $event)"
@duplicate-request="emit('duplicate-request', $event)"
@edit-properties="
@@ -213,24 +236,27 @@
</template>
<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 { useColorMode } from "@composables/theming"
import { removeGraphqlFolder } from "~/newstore/collections"
import { computed, ref } from "vue"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
import { Picked } from "~/helpers/types/HoppPicked"
import { useToast } from "@composables/toast"
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 t = useI18n()
@@ -255,6 +281,7 @@ const emit = defineEmits([
"edit-request",
"add-folder",
"edit-folder",
"duplicate-collection",
"duplicate-request",
"edit-properties",
"select-request",
@@ -267,12 +294,19 @@ const options = ref<any | null>(null)
const requestAction = ref<any | null>(null)
const folderAction = ref<any | null>(null)
const edit = ref<any | null>(null)
const duplicateAction = ref<any | null>(null)
const deleteAction = ref<any | null>(null)
const propertiesAction = ref<any | null>(null)
const showChildren = ref(false)
const dragging = ref(false)
const confirmRemove = ref(false)
const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
const isSelected = computed(
() =>
props.picked?.pickedType === "gql-my-folder" &&
@@ -285,6 +319,17 @@ const collectionIcon = computed(() => {
return IconFolder
})
const showDuplicateCollectionAction = computed(() => {
// Show if the user is not logged in
if (!currentUser.value) {
return true
}
// Duplicate collection action is disabled on SH until the issue with syncing is resolved
return !platform.platformFeatureFlags
.duplicateCollectionDisabledInPersonalWorkspace
})
const pick = () => {
emit("select", {
pickedType: "gql-my-folder",

View File

@@ -54,6 +54,7 @@
@add-request="addRequest($event)"
@add-folder="addFolder($event)"
@edit-folder="editFolder($event)"
@duplicate-collection="duplicateCollection($event)"
@edit-request="editRequest($event)"
@duplicate-request="duplicateRequest($event)"
@select-collection="$emit('use-collection', collection)"
@@ -167,6 +168,7 @@ import {
editGraphqlCollection,
editGraphqlFolder,
moveGraphqlRequest,
duplicateGraphQLCollection,
} from "~/newstore/collections"
import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle"
@@ -380,6 +382,14 @@ const editCollection = (
displayModalEdit(true)
}
const duplicateCollection = ({
path,
collectionSyncID,
}: {
path: string
collectionSyncID?: string
}) => duplicateGraphQLCollection(path, collectionSyncID)
const onAddRequest = ({
name,
path,

View File

@@ -37,6 +37,7 @@
@add-request="addRequest"
@edit-collection="editCollection"
@edit-folder="editFolder"
@duplicate-collection="duplicateCollection"
@edit-properties="editProperties"
@export-data="exportData"
@remove-collection="removeCollection"
@@ -67,7 +68,8 @@
"
:filter-text="filterTexts"
:export-loading="exportLoading"
:duplicate-loading="duplicateLoading"
:duplicate-request-loading="duplicateRequestLoading"
:duplicate-collection-loading="duplicateCollectionLoading"
:save-request="saveRequest"
:picked="picked"
:collection-move-loading="collectionMoveLoading"
@@ -76,6 +78,7 @@
@add-folder="addFolder"
@edit-collection="editCollection"
@edit-folder="editFolder"
@duplicate-collection="duplicateCollection"
@edit-properties="editProperties"
@export-data="exportData"
@remove-collection="removeCollection"
@@ -208,6 +211,7 @@ import {
createChildCollection,
createNewRootCollection,
deleteCollection,
duplicateTeamCollection,
moveRESTTeamCollection,
updateOrderRESTTeamCollection,
updateTeamCollection,
@@ -240,6 +244,7 @@ import {
addRESTCollection,
addRESTFolder,
cascadeParentCollectionForHeaderAuth,
duplicateRESTCollection,
editRESTCollection,
editRESTFolder,
editRESTRequest,
@@ -645,7 +650,8 @@ const isSelected = ({
const modalLoadingState = ref(false)
const exportLoading = ref(false)
const duplicateLoading = ref(false)
const duplicateRequestLoading = ref(false)
const duplicateCollectionLoading = ref(false)
const showModalAdd = 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: {
folderPath: string | undefined
requestIndex: string
@@ -1149,7 +1183,7 @@ const duplicateRequest = (payload: {
saveRESTRequestAs(folderPath, newRequest)
toast.success(t("request.duplicated"))
} else if (hasTeamWriteAccess.value) {
duplicateLoading.value = true
duplicateRequestLoading.value = true
if (!collectionsType.value.selectedTeam) return
@@ -1164,10 +1198,10 @@ const duplicateRequest = (payload: {
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
duplicateLoading.value = false
duplicateRequestLoading.value = false
},
() => {
duplicateLoading.value = false
duplicateRequestLoading.value = false
toast.success(t("request.duplicated"))
displayModalAddRequest(false)
}