Browse Source

feat: collection import summaries (#4489)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
Akash K 3 months ago
parent
commit
b706a43d2e

+ 13 - 1
packages/hoppscotch-common/locales/en.json

@@ -502,8 +502,11 @@
     "from_file": "Import from File",
     "from_file": "Import from File",
     "from_gist": "Import from Gist",
     "from_gist": "Import from Gist",
     "from_gist_description": "Import from Gist URL",
     "from_gist_description": "Import from Gist URL",
+    "from_gist_import_summary": "All hoppscotch features are imported.",
+    "from_hoppscotch_importer_summary": "All hoppscotch features are imported.",
     "from_insomnia": "Import from Insomnia",
     "from_insomnia": "Import from Insomnia",
     "from_insomnia_description": "Import from Insomnia collection",
     "from_insomnia_description": "Import from Insomnia collection",
+    "from_insomnia_import_summary": "Collections and Requests will be imported.",
     "from_json": "Import from Hoppscotch",
     "from_json": "Import from Hoppscotch",
     "from_json_description": "Import from Hoppscotch collection file",
     "from_json_description": "Import from Hoppscotch collection file",
     "from_my_collections": "Import from Personal Collections",
     "from_my_collections": "Import from Personal Collections",
@@ -512,12 +515,15 @@
     "from_all_collections_description": "Import any collection from Another Workspace to the current workspace",
     "from_all_collections_description": "Import any collection from Another Workspace to the current workspace",
     "from_openapi": "Import from OpenAPI",
     "from_openapi": "Import from OpenAPI",
     "from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
     "from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
+    "from_openapi_import_summary": "Collections ( will be created from tags ), Requests and response examples will be imported.",
     "from_postman": "Import from Postman",
     "from_postman": "Import from Postman",
     "from_postman_description": "Import from Postman collection",
     "from_postman_description": "Import from Postman collection",
+    "from_postman_import_summary": "Collections, Requests and response examples will be imported.",
     "from_url": "Import from URL",
     "from_url": "Import from URL",
     "gist_url": "Enter Gist URL",
     "gist_url": "Enter Gist URL",
     "from_har": "Import from HAR",
     "from_har": "Import from HAR",
     "from_har_description": "Import from HAR file",
     "from_har_description": "Import from HAR file",
+    "from_har_import_summary": "Requests will be imported to a default collection.",
     "gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
     "gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
     "hoppscotch_environment": "Hoppscotch Environment",
     "hoppscotch_environment": "Hoppscotch Environment",
     "hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
     "hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
@@ -532,7 +538,13 @@
     "title": "Import",
     "title": "Import",
     "file_size_limit_exceeded_warning_multiple_files": "Chosen files exceed the recommended limit of {sizeLimit}MB. Only the first {files} selected will be imported",
     "file_size_limit_exceeded_warning_multiple_files": "Chosen files exceed the recommended limit of {sizeLimit}MB. Only the first {files} selected will be imported",
     "file_size_limit_exceeded_warning_single_file": "The currently chosen file exceeds the recommended limit of {sizeLimit}MB. Please select another file.",
     "file_size_limit_exceeded_warning_single_file": "The currently chosen file exceeds the recommended limit of {sizeLimit}MB. Please select another file.",
-    "success": "Successfully imported"
+    "success": "Successfully imported",
+    "import_summary_collections_title": "Collections",
+    "import_summary_requests_title": "Requests",
+    "import_summary_responses_title": "Responses",
+    "import_summary_pre_request_scripts_title": "Pre-request scripts",
+    "import_summary_test_scripts_title": "Test scripts",
+    "import_summary_not_supported_by_hoppscotch_import": "We do not support importing {featureLabel} from this source right now."
   },
   },
   "inspections": {
   "inspections": {
     "description": "Inspect possible errors",
     "description": "Inspect possible errors",

+ 84 - 9
packages/hoppscotch-common/src/components/collections/ImportExport.vue

@@ -11,7 +11,7 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { HoppCollection } from "@hoppscotch/data"
 import { HoppCollection } from "@hoppscotch/data"
 import * as E from "fp-ts/Either"
 import * as E from "fp-ts/Either"
-import { PropType, computed, ref } from "vue"
+import { PropType, Ref, computed, ref } from "vue"
 
 
 import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
 import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
 import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource"
 import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource"
@@ -106,7 +106,6 @@ const handleImportToStore = async (collections: HoppCollection[]) => {
 
 
   if (E.isRight(importResult)) {
   if (E.isRight(importResult)) {
     toast.success(t("state.file_imported"))
     toast.success(t("state.file_imported"))
-    emit("hide-modal")
   } else {
   } else {
     toast.error(t("import.failed"))
     toast.error(t("import.failed"))
   }
   }
@@ -175,6 +174,24 @@ const isTeamWorkspace = computed(() => {
   return props.collectionsType.type === "team-collections"
   return props.collectionsType.type === "team-collections"
 })
 })
 
 
+const currentImportSummary: Ref<{
+  showImportSummary: boolean
+  importedCollections: HoppCollection[] | null
+}> = ref({
+  showImportSummary: false,
+  importedCollections: null,
+})
+
+const setCurrentImportSummary = (collections: HoppCollection[]) => {
+  currentImportSummary.value.importedCollections = collections
+  currentImportSummary.value.showImportSummary = true
+}
+
+const unsetCurrentImportSummary = () => {
+  currentImportSummary.value.importedCollections = null
+  currentImportSummary.value.showImportSummary = false
+}
+
 const HoppRESTImporter: ImporterOrExporter = {
 const HoppRESTImporter: ImporterOrExporter = {
   metadata: {
   metadata: {
     id: "hopp_rest",
     id: "hopp_rest",
@@ -183,7 +200,9 @@ const HoppRESTImporter: ImporterOrExporter = {
     icon: IconFolderPlus,
     icon: IconFolderPlus,
     disabled: false,
     disabled: false,
     applicableTo: ["personal-workspace", "team-workspace", "url-import"],
     applicableTo: ["personal-workspace", "team-workspace", "url-import"],
+    format: "hoppscotch",
   },
   },
+  importSummary: currentImportSummary,
   component: FileSource({
   component: FileSource({
     caption: "import.from_file",
     caption: "import.from_file",
     acceptedFileTypes: ".json",
     acceptedFileTypes: ".json",
@@ -194,6 +213,8 @@ const HoppRESTImporter: ImporterOrExporter = {
       if (E.isRight(res)) {
       if (E.isRight(res)) {
         await handleImportToStore(res.right)
         await handleImportToStore(res.right)
 
 
+        setCurrentImportSummary(res.right)
+
         platform.analytics?.logEvent({
         platform.analytics?.logEvent({
           type: "HOPP_IMPORT_COLLECTION",
           type: "HOPP_IMPORT_COLLECTION",
           importer: "import.from_json",
           importer: "import.from_json",
@@ -202,10 +223,13 @@ const HoppRESTImporter: ImporterOrExporter = {
         })
         })
       } else {
       } else {
         showImportFailedError()
         showImportFailedError()
+
+        unsetCurrentImportSummary()
       }
       }
 
 
       isRESTImporterInProgress.value = false
       isRESTImporterInProgress.value = false
     },
     },
+    description: "import.from_hoppscotch_importer_summary",
     isLoading: isRESTImporterInProgress,
     isLoading: isRESTImporterInProgress,
   }),
   }),
 }
 }
@@ -218,6 +242,7 @@ const HoppAllCollectionImporter: ImporterOrExporter = {
     icon: IconUser,
     icon: IconUser,
     disabled: false,
     disabled: false,
     applicableTo: ["personal-workspace", "team-workspace"],
     applicableTo: ["personal-workspace", "team-workspace"],
+    format: "hoppscotch",
   },
   },
   onSelect() {
   onSelect() {
     if (!currentUser.value) {
     if (!currentUser.value) {
@@ -227,19 +252,26 @@ const HoppAllCollectionImporter: ImporterOrExporter = {
 
 
     return false
     return false
   },
   },
+  importSummary: currentImportSummary,
   component: defineStep("all_collection_import", AllCollectionImport, () => ({
   component: defineStep("all_collection_import", AllCollectionImport, () => ({
     loading: isAllCollectionImporterInProgress.value,
     loading: isAllCollectionImporterInProgress.value,
     async onImportCollection(content) {
     async onImportCollection(content) {
       isAllCollectionImporterInProgress.value = true
       isAllCollectionImporterInProgress.value = true
 
 
-      await handleImportToStore([content])
+      try {
+        await handleImportToStore([content])
+        setCurrentImportSummary([content])
 
 
-      // our analytics consider this as an export event, so keeping compatibility with that
-      platform.analytics?.logEvent({
-        type: "HOPP_EXPORT_COLLECTION",
-        exporter: "import_to_teams",
-        platform: "rest",
-      })
+        // our analytics consider this as an export event, so keeping compatibility with that
+        platform.analytics?.logEvent({
+          type: "HOPP_EXPORT_COLLECTION",
+          exporter: "import_to_teams",
+          platform: "rest",
+        })
+      } catch (e) {
+        showImportFailedError()
+        unsetCurrentImportSummary()
+      }
 
 
       isAllCollectionImporterInProgress.value = false
       isAllCollectionImporterInProgress.value = false
     },
     },
@@ -254,7 +286,9 @@ const HoppOpenAPIImporter: ImporterOrExporter = {
     icon: IconOpenAPI,
     icon: IconOpenAPI,
     disabled: false,
     disabled: false,
     applicableTo: ["personal-workspace", "team-workspace", "url-import"],
     applicableTo: ["personal-workspace", "team-workspace", "url-import"],
+    format: "openapi",
   },
   },
+  importSummary: currentImportSummary,
   supported_sources: [
   supported_sources: [
     {
     {
       id: "file_import",
       id: "file_import",
@@ -263,6 +297,7 @@ const HoppOpenAPIImporter: ImporterOrExporter = {
       step: FileSource({
       step: FileSource({
         caption: "import.from_file",
         caption: "import.from_file",
         acceptedFileTypes: ".json, .yaml, .yml",
         acceptedFileTypes: ".json, .yaml, .yml",
+        description: "import.from_openapi_import_summary",
         onImportFromFile: async (content) => {
         onImportFromFile: async (content) => {
           isOpenAPIImporterInProgress.value = true
           isOpenAPIImporterInProgress.value = true
 
 
@@ -271,6 +306,8 @@ const HoppOpenAPIImporter: ImporterOrExporter = {
           if (E.isRight(res)) {
           if (E.isRight(res)) {
             await handleImportToStore(res.right)
             await handleImportToStore(res.right)
 
 
+            setCurrentImportSummary(res.right)
+
             platform.analytics?.logEvent({
             platform.analytics?.logEvent({
               platform: "rest",
               platform: "rest",
               type: "HOPP_IMPORT_COLLECTION",
               type: "HOPP_IMPORT_COLLECTION",
@@ -279,6 +316,8 @@ const HoppOpenAPIImporter: ImporterOrExporter = {
             })
             })
           } else {
           } else {
             showImportFailedError()
             showImportFailedError()
+
+            unsetCurrentImportSummary()
           }
           }
 
 
           isOpenAPIImporterInProgress.value = false
           isOpenAPIImporterInProgress.value = false
@@ -292,6 +331,7 @@ const HoppOpenAPIImporter: ImporterOrExporter = {
       icon: IconLink,
       icon: IconLink,
       step: UrlSource({
       step: UrlSource({
         caption: "import.from_url",
         caption: "import.from_url",
+        description: "import.from_openapi_import_summary",
         onImportFromURL: async (content) => {
         onImportFromURL: async (content) => {
           isOpenAPIImporterInProgress.value = true
           isOpenAPIImporterInProgress.value = true
 
 
@@ -300,6 +340,8 @@ const HoppOpenAPIImporter: ImporterOrExporter = {
           if (E.isRight(res)) {
           if (E.isRight(res)) {
             await handleImportToStore(res.right)
             await handleImportToStore(res.right)
 
 
+            setCurrentImportSummary(res.right)
+
             platform.analytics?.logEvent({
             platform.analytics?.logEvent({
               platform: "rest",
               platform: "rest",
               type: "HOPP_IMPORT_COLLECTION",
               type: "HOPP_IMPORT_COLLECTION",
@@ -308,6 +350,8 @@ const HoppOpenAPIImporter: ImporterOrExporter = {
             })
             })
           } else {
           } else {
             showImportFailedError()
             showImportFailedError()
+
+            unsetCurrentImportSummary()
           }
           }
 
 
           isOpenAPIImporterInProgress.value = false
           isOpenAPIImporterInProgress.value = false
@@ -326,10 +370,13 @@ const HoppPostmanImporter: ImporterOrExporter = {
     icon: IconPostman,
     icon: IconPostman,
     disabled: false,
     disabled: false,
     applicableTo: ["personal-workspace", "team-workspace", "url-import"],
     applicableTo: ["personal-workspace", "team-workspace", "url-import"],
+    format: "postman",
   },
   },
+  importSummary: currentImportSummary,
   component: FileSource({
   component: FileSource({
     caption: "import.from_file",
     caption: "import.from_file",
     acceptedFileTypes: ".json",
     acceptedFileTypes: ".json",
+    description: "import.from_postman_import_summary",
     onImportFromFile: async (content) => {
     onImportFromFile: async (content) => {
       isPostmanImporterInProgress.value = true
       isPostmanImporterInProgress.value = true
 
 
@@ -338,6 +385,8 @@ const HoppPostmanImporter: ImporterOrExporter = {
       if (E.isRight(res)) {
       if (E.isRight(res)) {
         await handleImportToStore(res.right)
         await handleImportToStore(res.right)
 
 
+        setCurrentImportSummary(res.right)
+
         platform.analytics?.logEvent({
         platform.analytics?.logEvent({
           platform: "rest",
           platform: "rest",
           type: "HOPP_IMPORT_COLLECTION",
           type: "HOPP_IMPORT_COLLECTION",
@@ -346,6 +395,8 @@ const HoppPostmanImporter: ImporterOrExporter = {
         })
         })
       } else {
       } else {
         showImportFailedError()
         showImportFailedError()
+
+        unsetCurrentImportSummary()
       }
       }
 
 
       isPostmanImporterInProgress.value = false
       isPostmanImporterInProgress.value = false
@@ -362,10 +413,13 @@ const HoppInsomniaImporter: ImporterOrExporter = {
     icon: IconInsomnia,
     icon: IconInsomnia,
     disabled: false,
     disabled: false,
     applicableTo: ["personal-workspace", "team-workspace", "url-import"],
     applicableTo: ["personal-workspace", "team-workspace", "url-import"],
+    format: "insomnia",
   },
   },
+  importSummary: currentImportSummary,
   component: FileSource({
   component: FileSource({
     caption: "import.from_file",
     caption: "import.from_file",
     acceptedFileTypes: ".json",
     acceptedFileTypes: ".json",
+    description: "import.from_insomnia_import_summary",
     onImportFromFile: async (content) => {
     onImportFromFile: async (content) => {
       isInsomniaImporterInProgress.value = true
       isInsomniaImporterInProgress.value = true
 
 
@@ -374,6 +428,8 @@ const HoppInsomniaImporter: ImporterOrExporter = {
       if (E.isRight(res)) {
       if (E.isRight(res)) {
         await handleImportToStore(res.right)
         await handleImportToStore(res.right)
 
 
+        setCurrentImportSummary(res.right)
+
         platform.analytics?.logEvent({
         platform.analytics?.logEvent({
           platform: "rest",
           platform: "rest",
           type: "HOPP_IMPORT_COLLECTION",
           type: "HOPP_IMPORT_COLLECTION",
@@ -382,6 +438,8 @@ const HoppInsomniaImporter: ImporterOrExporter = {
         })
         })
       } else {
       } else {
         showImportFailedError()
         showImportFailedError()
+
+        unsetCurrentImportSummary()
       }
       }
 
 
       isInsomniaImporterInProgress.value = false
       isInsomniaImporterInProgress.value = false
@@ -398,9 +456,12 @@ const HoppGistImporter: ImporterOrExporter = {
     icon: IconGithub,
     icon: IconGithub,
     disabled: false,
     disabled: false,
     applicableTo: ["personal-workspace", "team-workspace", "url-import"],
     applicableTo: ["personal-workspace", "team-workspace", "url-import"],
+    format: "hoppscotch",
   },
   },
+  importSummary: currentImportSummary,
   component: GistSource({
   component: GistSource({
     caption: "import.from_url",
     caption: "import.from_url",
+    description: "import.from_gist_import_summary",
     onImportFromGist: async (content) => {
     onImportFromGist: async (content) => {
       if (E.isLeft(content)) {
       if (E.isLeft(content)) {
         showImportFailedError()
         showImportFailedError()
@@ -414,6 +475,8 @@ const HoppGistImporter: ImporterOrExporter = {
       if (E.isRight(res)) {
       if (E.isRight(res)) {
         await handleImportToStore(res.right)
         await handleImportToStore(res.right)
 
 
+        setCurrentImportSummary(res.right)
+
         platform.analytics?.logEvent({
         platform.analytics?.logEvent({
           platform: "rest",
           platform: "rest",
           type: "HOPP_IMPORT_COLLECTION",
           type: "HOPP_IMPORT_COLLECTION",
@@ -422,6 +485,8 @@ const HoppGistImporter: ImporterOrExporter = {
         })
         })
       } else {
       } else {
         showImportFailedError()
         showImportFailedError()
+
+        unsetCurrentImportSummary()
       }
       }
 
 
       isGistImporterInProgress.value = false
       isGistImporterInProgress.value = false
@@ -439,7 +504,9 @@ const HoppMyCollectionsExporter: ImporterOrExporter = {
     disabled: false,
     disabled: false,
     applicableTo: ["personal-workspace"],
     applicableTo: ["personal-workspace"],
     isLoading: isHoppMyCollectionExporterInProgress,
     isLoading: isHoppMyCollectionExporterInProgress,
+    format: "hoppscotch",
   },
   },
+  importSummary: currentImportSummary,
   action: async () => {
   action: async () => {
     if (!myCollections.value.length) {
     if (!myCollections.value.length) {
       return toast.error(t("error.no_collections_to_export"))
       return toast.error(t("error.no_collections_to_export"))
@@ -478,6 +545,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
     applicableTo: ["team-workspace"],
     applicableTo: ["team-workspace"],
     isLoading: isHoppTeamCollectionExporterInProgress,
     isLoading: isHoppTeamCollectionExporterInProgress,
   },
   },
+  importSummary: currentImportSummary,
   action: async () => {
   action: async () => {
     isHoppTeamCollectionExporterInProgress.value = true
     isHoppTeamCollectionExporterInProgress.value = true
     if (
     if (
@@ -574,10 +642,13 @@ const HARImporter: ImporterOrExporter = {
     icon: IconFile,
     icon: IconFile,
     disabled: false,
     disabled: false,
     applicableTo: ["personal-workspace", "team-workspace"],
     applicableTo: ["personal-workspace", "team-workspace"],
+    format: "har",
   },
   },
+  importSummary: currentImportSummary,
   component: FileSource({
   component: FileSource({
     caption: "import.from_file",
     caption: "import.from_file",
     acceptedFileTypes: ".har",
     acceptedFileTypes: ".har",
+    description: "import.from_har_import_summary",
     onImportFromFile: async (content) => {
     onImportFromFile: async (content) => {
       isHarImporterInProgress.value = true
       isHarImporterInProgress.value = true
 
 
@@ -586,6 +657,8 @@ const HARImporter: ImporterOrExporter = {
       if (E.isRight(res)) {
       if (E.isRight(res)) {
         await handleImportToStore(res.right)
         await handleImportToStore(res.right)
 
 
+        setCurrentImportSummary(res.right)
+
         platform.analytics?.logEvent({
         platform.analytics?.logEvent({
           type: "HOPP_IMPORT_COLLECTION",
           type: "HOPP_IMPORT_COLLECTION",
           importer: "import.from_har",
           importer: "import.from_har",
@@ -594,6 +667,8 @@ const HARImporter: ImporterOrExporter = {
         })
         })
       } else {
       } else {
         showImportFailedError()
         showImportFailedError()
+
+        unsetCurrentImportSummary()
       }
       }
 
 
       isHarImporterInProgress.value = false
       isHarImporterInProgress.value = false

+ 47 - 2
packages/hoppscotch-common/src/components/importExport/Base.vue

@@ -7,7 +7,7 @@
   >
   >
     <template #actions>
     <template #actions>
       <HoppButtonSecondary
       <HoppButtonSecondary
-        v-if="hasPreviousStep"
+        v-if="hasPreviousStep && !isImportSummaryStep"
         v-tippy="{ theme: 'tooltip' }"
         v-tippy="{ theme: 'tooltip' }"
         :title="t('action.go_back')"
         :title="t('action.go_back')"
         :icon="IconArrowLeft"
         :icon="IconArrowLeft"
@@ -23,13 +23,14 @@
 import IconArrowLeft from "~icons/lucide/arrow-left"
 import IconArrowLeft from "~icons/lucide/arrow-left"
 
 
 import { useI18n } from "~/composables/i18n"
 import { useI18n } from "~/composables/i18n"
-import { PropType, ref } from "vue"
+import { computed, PropType, ref, watch } from "vue"
 
 
 import { useSteps, defineStep } from "~/composables/step-components"
 import { useSteps, defineStep } from "~/composables/step-components"
 import ImportExportList from "./ImportExportList.vue"
 import ImportExportList from "./ImportExportList.vue"
 
 
 import ImportExportSourcesList from "./ImportExportSourcesList.vue"
 import ImportExportSourcesList from "./ImportExportSourcesList.vue"
 import { ImporterOrExporter } from "~/components/importExport/types"
 import { ImporterOrExporter } from "~/components/importExport/types"
+import ImportSummary from "~/components/importExport/ImportExportSteps/ImportSummary.vue"
 
 
 const t = useI18n()
 const t = useI18n()
 
 
@@ -60,6 +61,10 @@ const {
   hasPreviousStep,
   hasPreviousStep,
 } = useSteps()
 } = useSteps()
 
 
+const isImportSummaryStep = computed(() => {
+  return currentStep.value.id.startsWith("import_summary_")
+})
+
 const selectedImporterID = ref<string | null>(null)
 const selectedImporterID = ref<string | null>(null)
 const selectedSourceID = ref<string | null>(null)
 const selectedSourceID = ref<string | null>(null)
 
 
@@ -154,10 +159,50 @@ const chooseImportSource = defineStep(
 addStep(chooseImporterOrExporter)
 addStep(chooseImporterOrExporter)
 addStep(chooseImportSource)
 addStep(chooseImportSource)
 
 
+const selectedImporterImportSummary = computed(() => {
+  const importer = props.importerModules.find(
+    (i) => i.metadata.id === selectedImporterID.value
+  )
+
+  if (!importer?.importSummary) return null
+
+  return importer.importSummary
+})
+
+watch(
+  selectedImporterImportSummary,
+  (val) => {
+    if (val?.value.showImportSummary) {
+      goToStep(`import_summary_${selectedImporterID.value}`)
+    }
+  },
+  { deep: true }
+)
+
 props.importerModules.forEach((importer) => {
 props.importerModules.forEach((importer) => {
   if (importer.component) {
   if (importer.component) {
     addStep(importer.component)
     addStep(importer.component)
   }
   }
+
+  const importSummary = importer.importSummary
+
+  if (!importSummary) {
+    return
+  }
+
+  if (importSummary.value) {
+    addStep({
+      id: `import_summary_${importer.metadata.id}`,
+      component: ImportSummary,
+      props: () => ({
+        collections: importSummary.value.importedCollections,
+        importFormat: importer.metadata.format,
+        "on-close": () => {
+          emit("hide-modal")
+        },
+      }),
+    })
+  }
 })
 })
 
 
 const emit = defineEmits<{
 const emit = defineEmits<{

+ 22 - 16
packages/hoppscotch-common/src/components/importExport/ImportExportSteps/FileImport.vue

@@ -1,22 +1,26 @@
 <template>
 <template>
   <div class="space-y-4">
   <div class="space-y-4">
-    <p class="flex items-center">
-      <span
-        class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
-        :class="{
-          '!text-green-500': hasFile,
-        }"
-      >
-        <icon-lucide-check-circle class="svg-icons" />
-      </span>
-      <span>
-        {{ t(`${caption}`) }}
-      </span>
-    </p>
+    <div>
+      <p class="flex items-center">
+        <span
+          class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
+          :class="{
+            '!text-green-500': hasFile,
+          }"
+        >
+          <icon-lucide-check-circle class="svg-icons" />
+        </span>
+        <span>
+          {{ t(`${caption}`) }}
+        </span>
+      </p>
+
+      <p v-if="description" class="ml-10 mt-2 text-secondaryLight">
+        {{ t(description) }}
+      </p>
+    </div>
 
 
-    <div
-      class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
-    >
+    <div class="flex flex-col border border-dashed rounded border-dividerDark">
       <input
       <input
         id="inputChooseFileToImportFrom"
         id="inputChooseFileToImportFrom"
         ref="inputChooseFileToImportFrom"
         ref="inputChooseFileToImportFrom"
@@ -71,9 +75,11 @@ const props = withDefaults(
     caption: string
     caption: string
     acceptedFileTypes: string
     acceptedFileTypes: string
     loading?: boolean
     loading?: boolean
+    description?: string
   }>(),
   }>(),
   {
   {
     loading: false,
     loading: false,
+    description: undefined,
   }
   }
 )
 )
 
 

+ 242 - 0
packages/hoppscotch-common/src/components/importExport/ImportExportSteps/ImportSummary.vue

@@ -0,0 +1,242 @@
+<script setup lang="ts">
+import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
+import { computed, Ref, ref, watch } from "vue"
+import { useI18n } from "~/composables/i18n"
+import IconInfo from "~icons/lucide/info"
+import { SupportedImportFormat } from "./../types"
+
+const t = useI18n()
+
+type Feature =
+  | "collections"
+  | "requests"
+  | "responses"
+  | "preRequestScripts"
+  | "testScripts"
+
+type FeatureStatus =
+  | "SUPPORTED"
+  | "NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT"
+  | "NOT_SUPPORTED_BY_SOURCE"
+
+type FeatureWithCount = {
+  count: number
+  label: string
+  id: Feature
+}
+
+const props = defineProps<{
+  importFormat: SupportedImportFormat
+  collections: HoppCollection[]
+  onClose: () => void
+}>()
+
+const importSourceAndSupportedFeatures: Record<
+  SupportedImportFormat,
+  Record<Feature, FeatureStatus>
+> = {
+  hoppscotch: {
+    collections: "SUPPORTED",
+    requests: "SUPPORTED",
+    responses: "SUPPORTED",
+    preRequestScripts: "SUPPORTED",
+    testScripts: "SUPPORTED",
+  },
+  postman: {
+    collections: "SUPPORTED",
+    requests: "SUPPORTED",
+    responses: "SUPPORTED",
+    preRequestScripts: "NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT",
+    testScripts: "NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT",
+  },
+  insomnia: {
+    collections: "SUPPORTED",
+    requests: "SUPPORTED",
+    responses: "NOT_SUPPORTED_BY_SOURCE",
+    preRequestScripts: "NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT",
+    testScripts: "NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT",
+  },
+  openapi: {
+    collections: "SUPPORTED",
+    requests: "SUPPORTED",
+    responses: "SUPPORTED",
+    preRequestScripts: "NOT_SUPPORTED_BY_SOURCE",
+    testScripts: "NOT_SUPPORTED_BY_SOURCE",
+  },
+  har: {
+    collections: "SUPPORTED",
+    requests: "SUPPORTED",
+    responses: "NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT",
+    preRequestScripts: "NOT_SUPPORTED_BY_SOURCE",
+    testScripts: "NOT_SUPPORTED_BY_SOURCE",
+  },
+}
+
+const featuresWithCount: Ref<FeatureWithCount[]> = ref([])
+
+const countCollections = (collections: HoppCollection[]) => {
+  let collectionCount = 0
+  let requestCount = 0
+  let preRequestScriptsCount = 0
+  let testScriptsCount = 0
+  let responseCount = 0
+
+  const flattenHoppCollections = (_collections: HoppCollection[]) => {
+    _collections.forEach((collection) => {
+      collectionCount++
+
+      collection.requests.forEach((request) => {
+        requestCount++
+
+        const _request = request as HoppRESTRequest
+
+        preRequestScriptsCount += !!_request.preRequestScript?.trim() ? 1 : 0
+        testScriptsCount += !!_request.testScript?.trim() ? 1 : 0
+
+        responseCount += _request.responses
+          ? Object.values(_request.responses).length
+          : 0
+      })
+
+      flattenHoppCollections(collection.folders)
+    })
+  }
+
+  flattenHoppCollections(collections)
+
+  return {
+    collectionCount,
+    requestCount,
+    responseCount,
+    preRequestScriptsCount,
+    testScriptsCount,
+  }
+}
+
+watch(
+  props.collections,
+  (collections) => {
+    const {
+      collectionCount,
+      requestCount,
+      responseCount,
+      preRequestScriptsCount,
+      testScriptsCount,
+    } = countCollections(collections)
+
+    featuresWithCount.value = [
+      {
+        count: collectionCount,
+        label: "import.import_summary_collections_title",
+        id: "collections" as const,
+      },
+      {
+        count: requestCount,
+        label: "import.import_summary_requests_title",
+        id: "requests" as const,
+      },
+      {
+        count: responseCount,
+        label: "import.import_summary_responses_title",
+        id: "responses" as const,
+      },
+      {
+        count: preRequestScriptsCount,
+        label: "import.import_summary_pre_request_scripts_title",
+        id: "preRequestScripts" as const,
+      },
+      {
+        count: testScriptsCount,
+        label: "import.import_summary_test_scripts_title",
+        id: "testScripts" as const,
+      },
+    ]
+  },
+  {
+    immediate: true,
+  }
+)
+
+const featureSupportForImportFormat = computed(() => {
+  return importSourceAndSupportedFeatures[props.importFormat]
+})
+
+const visibleFeatures = computed(() => {
+  return featuresWithCount.value.filter((feature) => {
+    return (
+      importSourceAndSupportedFeatures[props.importFormat][feature.id] !==
+      "NOT_SUPPORTED_BY_SOURCE"
+    )
+  })
+})
+</script>
+
+<template>
+  <div class="space-y-4">
+    <div v-for="feature in visibleFeatures" :key="feature.id">
+      <p class="flex items-center">
+        <span
+          class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary"
+          :class="{
+            'text-green-500':
+              featureSupportForImportFormat[feature.id] === 'SUPPORTED',
+            'text-amber-500':
+              featureSupportForImportFormat[feature.id] ===
+              'NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT',
+          }"
+        >
+          <icon-lucide-check-circle
+            v-if="featureSupportForImportFormat[feature.id] === 'SUPPORTED'"
+            class="svg-icons"
+          />
+
+          <IconInfo
+            v-else-if="
+              featureSupportForImportFormat[feature.id] ===
+              'NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT'
+            "
+            class="svg-icons"
+          />
+        </span>
+        <span>{{ t(feature.label) }}</span>
+      </p>
+
+      <p class="ml-10 text-secondaryLight">
+        <template
+          v-if="featureSupportForImportFormat[feature.id] === 'SUPPORTED'"
+        >
+          {{ feature.count }}
+          {{
+            feature.count != 1
+              ? t(feature.label)
+              : t(feature.label).slice(0, -1)
+          }}
+          Imported
+        </template>
+
+        <template
+          v-else-if="
+            featureSupportForImportFormat[feature.id] ===
+            'NOT_SUPPORTED_BY_HOPPSCOTCH_IMPORT'
+          "
+        >
+          {{
+            t("import.import_summary_not_supported_by_hoppscotch_import", {
+              featureLabel: t(feature.label),
+            })
+          }}
+        </template>
+      </p>
+    </div>
+  </div>
+
+  <div class="mt-10">
+    <HoppButtonSecondary
+      class="w-full"
+      :label="t('action.close')"
+      outline
+      filled
+      @click="onClose"
+    />
+  </div>
+</template>

+ 23 - 15
packages/hoppscotch-common/src/components/importExport/ImportExportSteps/UrlImport.vue

@@ -1,19 +1,26 @@
 <template>
 <template>
   <div class="space-y-4">
   <div class="space-y-4">
-    <p class="flex items-center">
-      <span
-        class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
-        :class="{
-          '!text-green-500': hasURL,
-        }"
-      >
-        <icon-lucide-check-circle class="svg-icons" />
-      </span>
-      <span>
-        {{ t(caption) }}
-      </span>
-    </p>
-    <p class="flex flex-col ml-10">
+    <div>
+      <p class="flex items-center">
+        <span
+          class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
+          :class="{
+            '!text-green-500': hasURL,
+          }"
+        >
+          <icon-lucide-check-circle class="svg-icons" />
+        </span>
+        <span>
+          {{ t(caption) }}
+        </span>
+      </p>
+
+      <p v-if="description" class="ml-10 mt-2 text-secondaryLight">
+        {{ t(description) }}
+      </p>
+    </div>
+
+    <p class="flex flex-col">
       <input
       <input
         v-model="inputChooseGistToImportFrom"
         v-model="inputChooseGistToImportFrom"
         type="url"
         type="url"
@@ -49,8 +56,9 @@ const props = withDefaults(
     caption: string
     caption: string
     fetchLogic?: (url: string) => Promise<AxiosResponse<any>>
     fetchLogic?: (url: string) => Promise<AxiosResponse<any>>
     loading?: boolean
     loading?: boolean
+    description?: string
   }>(),
   }>(),
-  { fetchLogic: undefined, loading: false }
+  { fetchLogic: undefined, loading: false, description: undefined }
 )
 )
 
 
 const emit = defineEmits<{
 const emit = defineEmits<{

+ 13 - 0
packages/hoppscotch-common/src/components/importExport/types.ts

@@ -1,6 +1,14 @@
+import { HoppCollection } from "@hoppscotch/data"
 import { Component, Ref } from "vue"
 import { Component, Ref } from "vue"
 import { defineStep } from "~/composables/step-components"
 import { defineStep } from "~/composables/step-components"
 
 
+export type SupportedImportFormat =
+  | "hoppscotch"
+  | "postman"
+  | "insomnia"
+  | "openapi"
+  | "har"
+
 // TODO: move the metadata except disabled and isLoading to importers.ts
 // TODO: move the metadata except disabled and isLoading to importers.ts
 export type ImporterOrExporter = {
 export type ImporterOrExporter = {
   metadata: {
   metadata: {
@@ -11,6 +19,7 @@ export type ImporterOrExporter = {
     disabled: boolean
     disabled: boolean
     applicableTo: Array<"personal-workspace" | "team-workspace" | "url-import">
     applicableTo: Array<"personal-workspace" | "team-workspace" | "url-import">
     isLoading?: Ref<boolean>
     isLoading?: Ref<boolean>
+    format?: SupportedImportFormat
   }
   }
   supported_sources?: {
   supported_sources?: {
     id: string
     id: string
@@ -18,6 +27,10 @@ export type ImporterOrExporter = {
     icon: Component
     icon: Component
     step: ReturnType<typeof defineStep>
     step: ReturnType<typeof defineStep>
   }[]
   }[]
+  importSummary?: Ref<{
+    showImportSummary: boolean
+    importedCollections: HoppCollection[] | null
+  }>
   component?: ReturnType<typeof defineStep>
   component?: ReturnType<typeof defineStep>
   action?: (...args: any[]) => any
   action?: (...args: any[]) => any
   onSelect?: () => boolean
   onSelect?: () => boolean

+ 2 - 0
packages/hoppscotch-common/src/helpers/import-export/import/import-sources/FileSource.ts

@@ -9,6 +9,7 @@ export function FileSource(metadata: {
   caption: string
   caption: string
   onImportFromFile: (content: string[]) => any | Promise<any>
   onImportFromFile: (content: string[]) => any | Promise<any>
   isLoading?: Ref<boolean>
   isLoading?: Ref<boolean>
+  description?: string
 }) {
 }) {
   const stepID = uuidv4()
   const stepID = uuidv4()
 
 
@@ -17,5 +18,6 @@ export function FileSource(metadata: {
     caption: metadata.caption,
     caption: metadata.caption,
     onImportFromFile: metadata.onImportFromFile,
     onImportFromFile: metadata.onImportFromFile,
     loading: metadata.isLoading?.value,
     loading: metadata.isLoading?.value,
+    description: metadata.description,
   }))
   }))
 }
 }

+ 2 - 0
packages/hoppscotch-common/src/helpers/import-export/import/import-sources/GistSource.ts

@@ -14,11 +14,13 @@ export function GistSource(metadata: {
     importResult: E.Either<string, string[]>
     importResult: E.Either<string, string[]>
   ) => any | Promise<any>
   ) => any | Promise<any>
   isLoading?: Ref<boolean>
   isLoading?: Ref<boolean>
+  description?: string
 }) {
 }) {
   const stepID = uuidv4()
   const stepID = uuidv4()
 
 
   return defineStep(stepID, UrlImport, () => ({
   return defineStep(stepID, UrlImport, () => ({
     caption: metadata.caption,
     caption: metadata.caption,
+    description: metadata.description,
     onImportFromURL: (gistResponse: unknown) => {
     onImportFromURL: (gistResponse: unknown) => {
       const fileSchema = z.object({
       const fileSchema = z.object({
         files: z.record(z.object({ content: z.string() })),
         files: z.record(z.object({ content: z.string() })),

+ 2 - 0
packages/hoppscotch-common/src/helpers/import-export/import/import-sources/UrlSource.ts

@@ -9,6 +9,7 @@ export function UrlSource(metadata: {
   onImportFromURL: (content: string) => any | Promise<any>
   onImportFromURL: (content: string) => any | Promise<any>
   fetchLogic?: (url: string) => Promise<any>
   fetchLogic?: (url: string) => Promise<any>
   isLoading?: Ref<boolean>
   isLoading?: Ref<boolean>
+  description: string
 }) {
 }) {
   const stepID = uuidv4()
   const stepID = uuidv4()
 
 
@@ -20,5 +21,6 @@ export function UrlSource(metadata: {
       }
       }
     },
     },
     loading: metadata.isLoading?.value,
     loading: metadata.isLoading?.value,
+    description: metadata.description,
   }))
   }))
 }
 }