Browse Source

feat: added reordering and moving for collection (#2916)

Nivedin 2 years ago
parent
commit
4ca6e9ec3a

+ 11 - 0
packages/hoppscotch-common/locales/en.json

@@ -117,12 +117,16 @@
   },
   "collection": {
     "created": "Collection created",
+    "different_parent": "Cannot reorder collection with different parent",
     "edit": "Edit Collection",
     "invalid_name": "Please provide a name for the collection",
+    "invalid_root_move": "Collection already in the root",
+    "moved": "Moved Successfully",
     "my_collections": "My Collections",
     "name": "My New Collection",
     "name_length_insufficient": "Collection name should be at least 3 characters long",
     "new": "New Collection",
+    "order_changed": "Collection Order Updated",
     "renamed": "Collection renamed",
     "request_in_use": "Request in use",
     "save_as": "Save as",
@@ -389,6 +393,7 @@
       "text": "Text"
     },
     "copy_link": "Copy link",
+    "different_collection": "Cannot reorder requests from different collections",
     "duration": "Duration",
     "enter_curl": "Enter cURL command",
     "duplicated": "Request duplicated",
@@ -397,8 +402,10 @@
     "header_list": "Header List",
     "invalid_name": "Please provide a name for the request",
     "method": "Method",
+    "moved": "Request moved",
     "name": "Request name",
     "new": "New Request",
+    "order_changed": "Request Order Updated",
     "override": "Override",
     "override_help": "Set <kbd>Content-Type</kbd> in Headers",
     "overriden": "Overridden",
@@ -655,6 +662,7 @@
     "exit_disabled": "Only owner cannot exit the team",
     "invalid_email_format": "Email format is invalid",
     "invalid_id": "Invalid team ID. Contact your team owner.",
+    "invalid_coll_id": "Invalid collection ID",
     "invalid_invite_link": "Invalid invite link",
     "invalid_invite_link_description": "The link you followed is invalid. Contact your team owner.",
     "invalid_member_permission": "Please provide a valid permission to the team member",
@@ -683,10 +691,13 @@
     "new_name": "My New Team",
     "no_access": "You do not have edit access to these collections",
     "no_invite_found": "Invitation not found. Contact your team owner.",
+    "no_request_found": "Request not found.",
     "not_found": "Team not found. Contact your team owner.",
     "not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
+    "parent_coll_move": "Cannot move collection to a child collection",
     "pending_invites": "Pending invites",
     "permissions": "Permissions",
+    "same_target_destination": "Same target and destination",
     "saved": "Team saved",
     "select_a_team": "Select a team",
     "title": "Teams",

+ 153 - 161
packages/hoppscotch-common/src/components.d.ts

@@ -1,170 +1,162 @@
 // generated by unplugin-vue-components
 // We suggest you to commit this file into source control
 // Read more: https://github.com/vuejs/core/pull/3399
-import '@vue/runtime-core'
+import "@vue/runtime-core"
 
 export {}
 
-declare module '@vue/runtime-core' {
+declare module "@vue/runtime-core" {
   export interface GlobalComponents {
-    AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
-    AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
-    AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
-    AppFooter: typeof import('./components/app/Footer.vue')['default']
-    AppFuse: typeof import('./components/app/Fuse.vue')['default']
-    AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
-    AppHeader: typeof import('./components/app/Header.vue')['default']
-    AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
-    AppLogo: typeof import('./components/app/Logo.vue')['default']
-    AppOptions: typeof import('./components/app/Options.vue')['default']
-    AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
-    AppPowerSearch: typeof import('./components/app/PowerSearch.vue')['default']
-    AppPowerSearchEntry: typeof import('./components/app/PowerSearchEntry.vue')['default']
-    AppShare: typeof import('./components/app/Share.vue')['default']
-    AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
-    AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
-    AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
-    AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
-    AppSupport: typeof import('./components/app/Support.vue')['default']
-    Collections: typeof import('./components/collections/index.vue')['default']
-    CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
-    CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default']
-    CollectionsAddRequest: typeof import('./components/collections/AddRequest.vue')['default']
-    CollectionsCollection: typeof import('./components/collections/Collection.vue')['default']
-    CollectionsEdit: typeof import('./components/collections/Edit.vue')['default']
-    CollectionsEditFolder: typeof import('./components/collections/EditFolder.vue')['default']
-    CollectionsEditRequest: typeof import('./components/collections/EditRequest.vue')['default']
-    CollectionsGraphql: typeof import('./components/collections/graphql/index.vue')['default']
-    CollectionsGraphqlAdd: typeof import('./components/collections/graphql/Add.vue')['default']
-    CollectionsGraphqlAddFolder: typeof import('./components/collections/graphql/AddFolder.vue')['default']
-    CollectionsGraphqlAddRequest: typeof import('./components/collections/graphql/AddRequest.vue')['default']
-    CollectionsGraphqlCollection: typeof import('./components/collections/graphql/Collection.vue')['default']
-    CollectionsGraphqlEdit: typeof import('./components/collections/graphql/Edit.vue')['default']
-    CollectionsGraphqlEditFolder: typeof import('./components/collections/graphql/EditFolder.vue')['default']
-    CollectionsGraphqlEditRequest: typeof import('./components/collections/graphql/EditRequest.vue')['default']
-    CollectionsGraphqlFolder: typeof import('./components/collections/graphql/Folder.vue')['default']
-    CollectionsGraphqlImportExport: typeof import('./components/collections/graphql/ImportExport.vue')['default']
-    CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
-    CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
-    CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
-    CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
-    CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
-    CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
-    CollectionsTeamSelect: typeof import('./components/collections/TeamSelect.vue')['default']
-    Environments: typeof import('./components/environments/index.vue')['default']
-    EnvironmentsChooseType: typeof import('./components/environments/ChooseType.vue')['default']
-    EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
-    EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
-    EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
-    EnvironmentsMyEnvironment: typeof import('./components/environments/my/Environment.vue')['default']
-    EnvironmentsTeams: typeof import('./components/environments/teams/index.vue')['default']
-    EnvironmentsTeamsDetails: typeof import('./components/environments/teams/Details.vue')['default']
-    EnvironmentsTeamsEnvironment: typeof import('./components/environments/teams/Environment.vue')['default']
-    FirebaseLogin: typeof import('./components/firebase/Login.vue')['default']
-    FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
-    GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
-    GraphqlField: typeof import('./components/graphql/Field.vue')['default']
-    GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
-    GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
-    GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
-    GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
-    GraphqlType: typeof import('./components/graphql/Type.vue')['default']
-    GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
-    History: typeof import('./components/history/index.vue')['default']
-    HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
-    HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
-    HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
-    HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
-    HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
-    HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
-    HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
-    HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
-    HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
-    HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
-    HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
-    HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
-    HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
-    HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
-    HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
-    HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
-    HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
-    HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
-    HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
-    HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
-    HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
-    HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
-    HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
-    HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
-    HttpBody: typeof import('./components/http/Body.vue')['default']
-    HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
-    HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
-    HttpHeaders: typeof import('./components/http/Headers.vue')['default']
-    HttpImportCurl: typeof import('./components/http/ImportCurl.vue')['default']
-    HttpOAuth2Authorization: typeof import('./components/http/OAuth2Authorization.vue')['default']
-    HttpParameters: typeof import('./components/http/Parameters.vue')['default']
-    HttpPreRequestScript: typeof import('./components/http/PreRequestScript.vue')['default']
-    HttpRawBody: typeof import('./components/http/RawBody.vue')['default']
-    HttpReqChangeConfirmModal: typeof import('./components/http/ReqChangeConfirmModal.vue')['default']
-    HttpRequest: typeof import('./components/http/Request.vue')['default']
-    HttpRequestOptions: typeof import('./components/http/RequestOptions.vue')['default']
-    HttpResponse: typeof import('./components/http/Response.vue')['default']
-    HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
-    HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
-    HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
-    HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
-    HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
-    HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
-    HttpTests: typeof import('./components/http/Tests.vue')['default']
-    HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
-    IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
-    IconLucideBrush: typeof import('~icons/lucide/brush')['default']
-    IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
-    IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
-    IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
-    IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
-    IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
-    IconLucideInfo: typeof import('~icons/lucide/info')['default']
-    IconLucideLayers: typeof import('~icons/lucide/layers')['default']
-    IconLucideMinus: typeof import('~icons/lucide/minus')['default']
-    IconLucideRss: typeof import('~icons/lucide/rss')['default']
-    IconLucideSearch: typeof import('~icons/lucide/search')['default']
-    IconLucideUser: typeof import('~icons/lucide/user')['default']
-    IconLucideUsers: typeof import('~icons/lucide/users')['default']
-    IconLucideVerified: typeof import('~icons/lucide/verified')['default']
-    LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
-    LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
-    LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default']
-    LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default']
-    LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default']
-    LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default']
-    LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
-    LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
-    LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
-    ProfilePicture: typeof import('./components/profile/Picture.vue')['default']
-    ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
-    ProfileShortcodes: typeof import('./components/profile/Shortcodes.vue')['default']
-    ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
-    RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
-    RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']
-    RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
-    RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
-    RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
-    SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
-    SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default']
-    SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default']
-    SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
-    SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
-    SmartTree: typeof import('./components/smart/Tree.vue')['default']
-    SmartTreeBranch: typeof import('./components/smart/TreeBranch.vue')['default']
-    TabPrimary: typeof import('./components/tab/Primary.vue')['default']
-    TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
-    Teams: typeof import('./components/teams/index.vue')['default']
-    TeamsAdd: typeof import('./components/teams/Add.vue')['default']
-    TeamsEdit: typeof import('./components/teams/Edit.vue')['default']
-    TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
-    TeamsModal: typeof import('./components/teams/Modal.vue')['default']
-    TeamsTeam: typeof import('./components/teams/Team.vue')['default']
-    Tippy: typeof import('vue-tippy')['Tippy']
+    AppActionHandler: typeof import("./components/app/ActionHandler.vue")["default"]
+    AppAnnouncement: typeof import("./components/app/Announcement.vue")["default"]
+    AppDeveloperOptions: typeof import("./components/app/DeveloperOptions.vue")["default"]
+    AppFooter: typeof import("./components/app/Footer.vue")["default"]
+    AppFuse: typeof import("./components/app/Fuse.vue")["default"]
+    AppGitHubStarButton: typeof import("./components/app/GitHubStarButton.vue")["default"]
+    AppHeader: typeof import("./components/app/Header.vue")["default"]
+    AppInterceptor: typeof import("./components/app/Interceptor.vue")["default"]
+    AppLogo: typeof import("./components/app/Logo.vue")["default"]
+    AppOptions: typeof import("./components/app/Options.vue")["default"]
+    AppPaneLayout: typeof import("./components/app/PaneLayout.vue")["default"]
+    AppPowerSearch: typeof import("./components/app/PowerSearch.vue")["default"]
+    AppPowerSearchEntry: typeof import("./components/app/PowerSearchEntry.vue")["default"]
+    AppShare: typeof import("./components/app/Share.vue")["default"]
+    AppShortcuts: typeof import("./components/app/Shortcuts.vue")["default"]
+    AppShortcutsEntry: typeof import("./components/app/ShortcutsEntry.vue")["default"]
+    AppShortcutsPrompt: typeof import("./components/app/ShortcutsPrompt.vue")["default"]
+    AppSidenav: typeof import("./components/app/Sidenav.vue")["default"]
+    AppSupport: typeof import("./components/app/Support.vue")["default"]
+    Collections: typeof import("./components/collections/index.vue")["default"]
+    CollectionsAdd: typeof import("./components/collections/Add.vue")["default"]
+    CollectionsAddFolder: typeof import("./components/collections/AddFolder.vue")["default"]
+    CollectionsAddRequest: typeof import("./components/collections/AddRequest.vue")["default"]
+    CollectionsCollection: typeof import("./components/collections/Collection.vue")["default"]
+    CollectionsEdit: typeof import("./components/collections/Edit.vue")["default"]
+    CollectionsEditFolder: typeof import("./components/collections/EditFolder.vue")["default"]
+    CollectionsEditRequest: typeof import("./components/collections/EditRequest.vue")["default"]
+    CollectionsGraphql: typeof import("./components/collections/graphql/index.vue")["default"]
+    CollectionsGraphqlAdd: typeof import("./components/collections/graphql/Add.vue")["default"]
+    CollectionsGraphqlAddFolder: typeof import("./components/collections/graphql/AddFolder.vue")["default"]
+    CollectionsGraphqlAddRequest: typeof import("./components/collections/graphql/AddRequest.vue")["default"]
+    CollectionsGraphqlCollection: typeof import("./components/collections/graphql/Collection.vue")["default"]
+    CollectionsGraphqlEdit: typeof import("./components/collections/graphql/Edit.vue")["default"]
+    CollectionsGraphqlEditFolder: typeof import("./components/collections/graphql/EditFolder.vue")["default"]
+    CollectionsGraphqlEditRequest: typeof import("./components/collections/graphql/EditRequest.vue")["default"]
+    CollectionsGraphqlFolder: typeof import("./components/collections/graphql/Folder.vue")["default"]
+    CollectionsGraphqlImportExport: typeof import("./components/collections/graphql/ImportExport.vue")["default"]
+    CollectionsGraphqlRequest: typeof import("./components/collections/graphql/Request.vue")["default"]
+    CollectionsImportExport: typeof import("./components/collections/ImportExport.vue")["default"]
+    CollectionsMyCollections: typeof import("./components/collections/MyCollections.vue")["default"]
+    CollectionsRequest: typeof import("./components/collections/Request.vue")["default"]
+    CollectionsSaveRequest: typeof import("./components/collections/SaveRequest.vue")["default"]
+    CollectionsTeamCollections: typeof import("./components/collections/TeamCollections.vue")["default"]
+    CollectionsTeamSelect: typeof import("./components/collections/TeamSelect.vue")["default"]
+    Environments: typeof import("./components/environments/index.vue")["default"]
+    EnvironmentsChooseType: typeof import("./components/environments/ChooseType.vue")["default"]
+    EnvironmentsImportExport: typeof import("./components/environments/ImportExport.vue")["default"]
+    EnvironmentsMy: typeof import("./components/environments/my/index.vue")["default"]
+    EnvironmentsMyDetails: typeof import("./components/environments/my/Details.vue")["default"]
+    EnvironmentsMyEnvironment: typeof import("./components/environments/my/Environment.vue")["default"]
+    EnvironmentsTeams: typeof import("./components/environments/teams/index.vue")["default"]
+    EnvironmentsTeamsDetails: typeof import("./components/environments/teams/Details.vue")["default"]
+    EnvironmentsTeamsEnvironment: typeof import("./components/environments/teams/Environment.vue")["default"]
+    FirebaseLogin: typeof import("./components/firebase/Login.vue")["default"]
+    FirebaseLogout: typeof import("./components/firebase/Logout.vue")["default"]
+    GraphqlAuthorization: typeof import("./components/graphql/Authorization.vue")["default"]
+    GraphqlField: typeof import("./components/graphql/Field.vue")["default"]
+    GraphqlRequest: typeof import("./components/graphql/Request.vue")["default"]
+    GraphqlRequestOptions: typeof import("./components/graphql/RequestOptions.vue")["default"]
+    GraphqlResponse: typeof import("./components/graphql/Response.vue")["default"]
+    GraphqlSidebar: typeof import("./components/graphql/Sidebar.vue")["default"]
+    GraphqlType: typeof import("./components/graphql/Type.vue")["default"]
+    GraphqlTypeLink: typeof import("./components/graphql/TypeLink.vue")["default"]
+    History: typeof import("./components/history/index.vue")["default"]
+    HistoryGraphqlCard: typeof import("./components/history/graphql/Card.vue")["default"]
+    HistoryRestCard: typeof import("./components/history/rest/Card.vue")["default"]
+    HoppButtonPrimary: typeof import("@hoppscotch/ui")["HoppButtonPrimary"]
+    HoppButtonSecondary: typeof import("@hoppscotch/ui")["HoppButtonSecondary"]
+    HoppSmartAnchor: typeof import("@hoppscotch/ui")["HoppSmartAnchor"]
+    HoppSmartConfirmModal: typeof import("@hoppscotch/ui")["HoppSmartConfirmModal"]
+    HoppSmartExpand: typeof import("@hoppscotch/ui")["HoppSmartExpand"]
+    HoppSmartFileChip: typeof import("@hoppscotch/ui")["HoppSmartFileChip"]
+    HoppSmartIntersection: typeof import("@hoppscotch/ui")["HoppSmartIntersection"]
+    HoppSmartExpand: typeof import("@hoppscotch/ui")["HoppSmartExpand"]
+    HoppSmartFileChip: typeof import("@hoppscotch/ui")["HoppSmartFileChip"]
+    HoppSmartIntersection: typeof import("@hoppscotch/ui")["HoppSmartIntersection"]
+    HoppSmartItem: typeof import("@hoppscotch/ui")["HoppSmartItem"]
+    HoppSmartLink: typeof import("@hoppscotch/ui")["HoppSmartLink"]
+    HoppSmartModal: typeof import("@hoppscotch/ui")["HoppSmartModal"]
+    HoppSmartRadioGroup: typeof import("@hoppscotch/ui")["HoppSmartRadioGroup"]
+    HoppSmartSlideOver: typeof import("@hoppscotch/ui")["HoppSmartSlideOver"]
+    HoppSmartSpinner: typeof import("@hoppscotch/ui")["HoppSmartSpinner"]
+    HoppSmartTab: typeof import("@hoppscotch/ui")["HoppSmartTab"]
+    HoppSmartTabs: typeof import("@hoppscotch/ui")["HoppSmartTabs"]
+    HoppSmartToggle: typeof import("@hoppscotch/ui")["HoppSmartToggle"]
+    HoppSmartWindow: typeof import("@hoppscotch/ui")["HoppSmartWindow"]
+    HoppSmartWindows: typeof import("@hoppscotch/ui")["HoppSmartWindows"]
+    HoppSmartTab: typeof import("@hoppscotch/ui")["HoppSmartTab"]
+    HoppSmartTabs: typeof import("@hoppscotch/ui")["HoppSmartTabs"]
+    HttpAuthorization: typeof import("./components/http/Authorization.vue")["default"]
+    HttpBody: typeof import("./components/http/Body.vue")["default"]
+    HttpBodyParameters: typeof import("./components/http/BodyParameters.vue")["default"]
+    HttpCodegenModal: typeof import("./components/http/CodegenModal.vue")["default"]
+    HttpHeaders: typeof import("./components/http/Headers.vue")["default"]
+    HttpImportCurl: typeof import("./components/http/ImportCurl.vue")["default"]
+    HttpOAuth2Authorization: typeof import("./components/http/OAuth2Authorization.vue")["default"]
+    HttpParameters: typeof import("./components/http/Parameters.vue")["default"]
+    HttpPreRequestScript: typeof import("./components/http/PreRequestScript.vue")["default"]
+    HttpRawBody: typeof import("./components/http/RawBody.vue")["default"]
+    HttpReqChangeConfirmModal: typeof import("./components/http/ReqChangeConfirmModal.vue")["default"]
+    HttpRequest: typeof import("./components/http/Request.vue")["default"]
+    HttpRequestOptions: typeof import("./components/http/RequestOptions.vue")["default"]
+    HttpResponse: typeof import("./components/http/Response.vue")["default"]
+    HttpResponseMeta: typeof import("./components/http/ResponseMeta.vue")["default"]
+    HttpSidebar: typeof import("./components/http/Sidebar.vue")["default"]
+    HttpTestResult: typeof import("./components/http/TestResult.vue")["default"]
+    HttpTestResultEntry: typeof import("./components/http/TestResultEntry.vue")["default"]
+    HttpTestResultEnv: typeof import("./components/http/TestResultEnv.vue")["default"]
+    HttpTestResultReport: typeof import("./components/http/TestResultReport.vue")["default"]
+    HttpTests: typeof import("./components/http/Tests.vue")["default"]
+    HttpURLEncodedParams: typeof import("./components/http/URLEncodedParams.vue")["default"]
+    IconLucideChevronRight: typeof import("~icons/lucide/chevron-right")["default"]
+    IconLucideInbox: typeof import("~icons/lucide/inbox")["default"]
+    IconLucideInfo: typeof import("~icons/lucide/info")["default"]
+    IconLucideSearch: typeof import("~icons/lucide/search")["default"]
+    IconLucideUser: typeof import("~icons/lucide/user")["default"]
+    IconLucideUsers: typeof import("~icons/lucide/users")["default"]
+    LensesHeadersRenderer: typeof import("./components/lenses/HeadersRenderer.vue")["default"]
+    LensesHeadersRendererEntry: typeof import("./components/lenses/HeadersRendererEntry.vue")["default"]
+    LensesRenderersHTMLLensRenderer: typeof import("./components/lenses/renderers/HTMLLensRenderer.vue")["default"]
+    LensesRenderersImageLensRenderer: typeof import("./components/lenses/renderers/ImageLensRenderer.vue")["default"]
+    LensesRenderersJSONLensRenderer: typeof import("./components/lenses/renderers/JSONLensRenderer.vue")["default"]
+    LensesRenderersPDFLensRenderer: typeof import("./components/lenses/renderers/PDFLensRenderer.vue")["default"]
+    LensesRenderersRawLensRenderer: typeof import("./components/lenses/renderers/RawLensRenderer.vue")["default"]
+    LensesRenderersXMLLensRenderer: typeof import("./components/lenses/renderers/XMLLensRenderer.vue")["default"]
+    LensesResponseBodyRenderer: typeof import("./components/lenses/ResponseBodyRenderer.vue")["default"]
+    ProfilePicture: typeof import("./components/profile/Picture.vue")["default"]
+    ProfileShortcode: typeof import("./components/profile/Shortcode.vue")["default"]
+    ProfileShortcodes: typeof import("./components/profile/Shortcodes.vue")["default"]
+    ProfileUserDelete: typeof import("./components/profile/UserDelete.vue")["default"]
+    RealtimeCommunication: typeof import("./components/realtime/Communication.vue")["default"]
+    RealtimeConnectionConfig: typeof import("./components/realtime/ConnectionConfig.vue")["default"]
+    RealtimeLog: typeof import("./components/realtime/Log.vue")["default"]
+    RealtimeLogEntry: typeof import("./components/realtime/LogEntry.vue")["default"]
+    RealtimeSubscription: typeof import("./components/realtime/Subscription.vue")["default"]
+    SmartAccentModePicker: typeof import("./components/smart/AccentModePicker.vue")["default"]
+    SmartChangeLanguage: typeof import("./components/smart/ChangeLanguage.vue")["default"]
+    SmartColorModePicker: typeof import("./components/smart/ColorModePicker.vue")["default"]
+    SmartEnvInput: typeof import("./components/smart/EnvInput.vue")["default"]
+    SmartFontSizePicker: typeof import("./components/smart/FontSizePicker.vue")["default"]
+    SmartTree: typeof import("./components/smart/Tree.vue")["default"]
+    SmartTreeBranch: typeof import("./components/smart/TreeBranch.vue")["default"]
+    TabPrimary: typeof import("./components/tab/Primary.vue")["default"]
+    TabSecondary: typeof import("./components/tab/Secondary.vue")["default"]
+    Teams: typeof import("./components/teams/index.vue")["default"]
+    TeamsAdd: typeof import("./components/teams/Add.vue")["default"]
+    TeamsEdit: typeof import("./components/teams/Edit.vue")["default"]
+    TeamsInvite: typeof import("./components/teams/Invite.vue")["default"]
+    TeamsModal: typeof import("./components/teams/Modal.vue")["default"]
+    TeamsTeam: typeof import("./components/teams/Team.vue")["default"]
+    Tippy: typeof import("vue-tippy")["Tippy"]
   }
-
 }

+ 220 - 133
packages/hoppscotch-common/src/components/collections/Collection.vue

@@ -1,138 +1,160 @@
 <template>
-  <div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
+  <div class="flex flex-col">
     <div
-      class="flex items-stretch group"
-      @dragover.prevent
-      @drop.prevent="dropEvent"
-      @dragover="dragging = true"
-      @drop="dragging = false"
-      @dragleave="dragging = false"
-      @dragend="dragging = false"
-      @contextmenu.prevent="options?.tippy.show()"
-    >
-      <span
-        class="flex items-center justify-center px-4 cursor-pointer"
-        @click="emit('toggle-children')"
+      class="h-1 w-full transition"
+      :class="[
+        {
+          'bg-accentDark': ordering && notSameDestination,
+        },
+      ]"
+      @drop="orderUpdateCollectionEvent"
+      @dragover.prevent="ordering = true"
+      @dragleave="ordering = false"
+      @dragend="resetDragState"
+    ></div>
+    <div class="flex flex-col relative">
+      <div
+        class="absolute bg-accent opacity-0 pointer-events-none inset-0 z-1 transition"
+        :class="{
+          'opacity-25': dragging && notSameDestination,
+        }"
+      ></div>
+      <div
+        class="flex items-stretch group relative z-3"
+        :draggable="!hasNoTeamAccess"
+        @dragstart="dragStart"
+        @drop="dropEvent"
+        @dragover="dragging = true"
+        @dragleave="dragging = false"
+        @dragend="resetDragState"
+        @contextmenu.prevent="options?.tippy.show()"
       >
-        <component
-          :is="collectionIcon"
-          class="svg-icons"
-          :class="{ 'text-accent': isSelected }"
-        />
-      </span>
-      <span
-        class="flex flex-1 min-w-0 py-2 pr-2 transition cursor-pointer group-hover:text-secondaryDark"
-        @click="emit('toggle-children')"
-      >
-        <span class="truncate" :class="{ 'text-accent': isSelected }">
-          {{ collectionName }}
+        <span
+          class="flex items-center justify-center px-4 cursor-pointer"
+          @click="emit('toggle-children')"
+        >
+          <HoppSmartSpinner v-if="isCollLoading" />
+          <component
+            :is="collectionIcon"
+            v-else
+            class="svg-icons"
+            :class="{ 'text-accent': isSelected }"
+          />
         </span>
-      </span>
-      <div v-if="!hasNoTeamAccess" class="flex">
-        <HoppButtonSecondary
-          v-tippy="{ theme: 'tooltip' }"
-          :icon="IconFilePlus"
-          :title="t('request.new')"
-          class="hidden group-hover:inline-flex"
-          @click="emit('add-request')"
-        />
-        <HoppButtonSecondary
-          v-tippy="{ theme: 'tooltip' }"
-          :icon="IconFolderPlus"
-          :title="t('folder.new')"
-          class="hidden group-hover:inline-flex"
-          @click="emit('add-folder')"
-        />
-        <span>
-          <tippy
-            ref="options"
-            interactive
-            trigger="click"
-            theme="popover"
-            :on-shown="() => tippyActions!.focus()"
-          >
-            <HoppButtonSecondary
-              v-tippy="{ theme: 'tooltip' }"
-              :title="t('action.more')"
-              :icon="IconMoreVertical"
-            />
-            <template #content="{ hide }">
-              <div
-                ref="tippyActions"
-                class="flex flex-col focus:outline-none"
-                tabindex="0"
-                @keyup.r="requestAction?.$el.click()"
-                @keyup.n="folderAction?.$el.click()"
-                @keyup.e="edit?.$el.click()"
-                @keyup.delete="deleteAction?.$el.click()"
-                @keyup.x="exportAction?.$el.click()"
-                @keyup.escape="hide()"
-              >
-                <HoppSmartItem
-                  ref="requestAction"
-                  :icon="IconFilePlus"
-                  :label="t('request.new')"
-                  :shortcut="['R']"
-                  @click="
-                    () => {
-                      emit('add-request')
-                      hide()
-                    }
-                  "
-                />
-                <HoppSmartItem
-                  ref="folderAction"
-                  :icon="IconFolderPlus"
-                  :label="t('folder.new')"
-                  :shortcut="['N']"
-                  @click="
-                    () => {
-                      emit('add-folder')
-                      hide()
-                    }
-                  "
-                />
-                <HoppSmartItem
-                  ref="edit"
-                  :icon="IconEdit"
-                  :label="t('action.edit')"
-                  :shortcut="['E']"
-                  @click="
-                    () => {
-                      emit('edit-collection')
-                      hide()
-                    }
-                  "
-                />
-                <HoppSmartItem
-                  ref="exportAction"
-                  :icon="IconDownload"
-                  :label="t('export.title')"
-                  :shortcut="['X']"
-                  :loading="exportLoading"
-                  @click="
-                    () => {
-                      emit('export-data'),
-                        collectionsType === 'my-collections' ? hide() : null
-                    }
-                  "
-                />
-                <HoppSmartItem
-                  ref="deleteAction"
-                  :icon="IconTrash2"
-                  :label="t('action.delete')"
-                  :shortcut="['⌫']"
-                  @click="
-                    () => {
-                      emit('remove-collection')
-                      hide()
-                    }
-                  "
-                />
-              </div>
-            </template>
-          </tippy>
+        <span
+          class="flex flex-1 min-w-0 py-2 pr-2 transition cursor-pointer group-hover:text-secondaryDark"
+          @click="emit('toggle-children')"
+        >
+          <span class="truncate" :class="{ 'text-accent': isSelected }">
+            {{ collectionName }}
+          </span>
         </span>
+        <div v-if="!hasNoTeamAccess" class="flex">
+          <HoppButtonSecondary
+            v-tippy="{ theme: 'tooltip' }"
+            :icon="IconFilePlus"
+            :title="t('request.new')"
+            class="hidden group-hover:inline-flex"
+            @click="emit('add-request')"
+          />
+          <HoppButtonSecondary
+            v-tippy="{ theme: 'tooltip' }"
+            :icon="IconFolderPlus"
+            :title="t('folder.new')"
+            class="hidden group-hover:inline-flex"
+            @click="emit('add-folder')"
+          />
+          <span>
+            <tippy
+              ref="options"
+              interactive
+              trigger="click"
+              theme="popover"
+              :on-shown="() => tippyActions!.focus()"
+            >
+              <HoppButtonSecondary
+                v-tippy="{ theme: 'tooltip' }"
+                :title="t('action.more')"
+                :icon="IconMoreVertical"
+              />
+              <template #content="{ hide }">
+                <div
+                  ref="tippyActions"
+                  class="flex flex-col focus:outline-none"
+                  tabindex="0"
+                  @keyup.r="requestAction?.$el.click()"
+                  @keyup.n="folderAction?.$el.click()"
+                  @keyup.e="edit?.$el.click()"
+                  @keyup.delete="deleteAction?.$el.click()"
+                  @keyup.x="exportAction?.$el.click()"
+                  @keyup.escape="hide()"
+                >
+                  <HoppSmartItem
+                    ref="requestAction"
+                    :icon="IconFilePlus"
+                    :label="t('request.new')"
+                    :shortcut="['R']"
+                    @click="
+                      () => {
+                        emit('add-request')
+                        hide()
+                      }
+                    "
+                  />
+                  <HoppSmartItem
+                    ref="folderAction"
+                    :icon="IconFolderPlus"
+                    :label="t('folder.new')"
+                    :shortcut="['N']"
+                    @click="
+                      () => {
+                        emit('add-folder')
+                        hide()
+                      }
+                    "
+                  />
+                  <HoppSmartItem
+                    ref="edit"
+                    :icon="IconEdit"
+                    :label="t('action.edit')"
+                    :shortcut="['E']"
+                    @click="
+                      () => {
+                        emit('edit-collection')
+                        hide()
+                      }
+                    "
+                  />
+                  <HoppSmartItem
+                    ref="exportAction"
+                    :icon="IconDownload"
+                    :label="t('export.title')"
+                    :shortcut="['X']"
+                    :loading="exportLoading"
+                    @click="
+                      () => {
+                        emit('export-data'),
+                          collectionsType === 'my-collections' ? hide() : null
+                      }
+                    "
+                  />
+                  <HoppSmartItem
+                    ref="deleteAction"
+                    :icon="IconTrash2"
+                    :label="t('action.delete')"
+                    :shortcut="['⌫']"
+                    @click="
+                      () => {
+                        emit('remove-collection')
+                        hide()
+                      }
+                    "
+                  />
+                </div>
+              </template>
+            </tippy>
+          </span>
+        </div>
       </div>
     </div>
   </div>
@@ -160,6 +182,11 @@ type FolderType = "collection" | "folder"
 const t = useI18n()
 
 const props = defineProps({
+  id: {
+    type: String,
+    default: "",
+    required: true,
+  },
   data: {
     type: Object as PropType<HoppCollection<HoppRESTRequest> | TeamCollection>,
     default: () => ({}),
@@ -185,7 +212,7 @@ const props = defineProps({
     required: true,
   },
   isSelected: {
-    type: Boolean,
+    type: Boolean as PropType<boolean | null>,
     default: false,
     required: false,
   },
@@ -199,6 +226,11 @@ const props = defineProps({
     default: false,
     required: false,
   },
+  collectionMoveLoading: {
+    type: Array as PropType<string[]>,
+    default: () => [],
+    required: false,
+  },
 })
 
 const emit = defineEmits<{
@@ -209,6 +241,9 @@ const emit = defineEmits<{
   (event: "export-data"): void
   (event: "remove-collection"): void
   (event: "drop-event", payload: DataTransfer): void
+  (event: "drag-event", payload: DataTransfer): void
+  (event: "dragging", payload: boolean): void
+  (event: "update-collection-order", payload: DataTransfer): void
 }>()
 
 const tippyActions = ref<TippyComponent | null>(null)
@@ -220,6 +255,21 @@ const exportAction = ref<HTMLButtonElement | null>(null)
 const options = ref<TippyComponent | null>(null)
 
 const dragging = ref(false)
+const ordering = ref(false)
+const dropItemID = ref("")
+
+// Used to determine if the collection is being dragged to a different destination
+// This is used to make the highlight effect work
+watch(
+  () => dragging.value,
+  (val) => {
+    if (val && notSameDestination.value) {
+      emit("dragging", true)
+    } else {
+      emit("dragging", false)
+    }
+  }
+)
 
 const collectionIcon = computed(() => {
   if (props.isSelected) return IconCheckCircle
@@ -243,10 +293,47 @@ watch(
   }
 )
 
-const dropEvent = ({ dataTransfer }: DragEvent) => {
+const dragStart = ({ dataTransfer }: DragEvent) => {
   if (dataTransfer) {
+    emit("drag-event", dataTransfer)
+    dropItemID.value = dataTransfer.getData("collectionIndex")
+    dragging.value = !dragging.value
+  }
+}
+
+const dropEvent = (e: DragEvent) => {
+  if (e.dataTransfer) {
+    e.stopPropagation()
+    emit("drop-event", e.dataTransfer)
     dragging.value = !dragging.value
-    emit("drop-event", dataTransfer)
+    dropItemID.value = ""
   }
 }
+
+const orderUpdateCollectionEvent = (e: DragEvent) => {
+  if (e.dataTransfer) {
+    e.stopPropagation()
+    emit("update-collection-order", e.dataTransfer)
+    ordering.value = !ordering.value
+    dropItemID.value = ""
+  }
+}
+
+const notSameDestination = computed(() => {
+  return dropItemID.value !== props.id
+})
+
+const isCollLoading = computed(() => {
+  if (props.collectionMoveLoading.length > 0 && props.data.id) {
+    return props.collectionMoveLoading.includes(props.data.id)
+  } else {
+    return false
+  }
+})
+
+const resetDragState = () => {
+  dragging.value = false
+  ordering.value = false
+  dropItemID.value = ""
+}
 </script>

+ 96 - 7
packages/hoppscotch-common/src/components/collections/MyCollections.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="flex flex-col flex-1">
+  <div class="flex flex-col flex-1 bg-primary">
     <div
       class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
       :style="
@@ -33,9 +33,12 @@
     </div>
     <div class="flex flex-col flex-1">
       <SmartTree :adapter="myAdapter">
-        <template #content="{ node, toggleChildren, isOpen }">
+        <template
+          #content="{ node, toggleChildren, isOpen, highlightChildren }"
+        >
           <CollectionsCollection
             v-if="node.data.type === 'collections'"
+            :id="node.id"
             :data="node.data.data.data"
             :collections-type="collectionsType.type"
             :is-open="isOpen"
@@ -72,6 +75,11 @@
             "
             @remove-collection="emit('remove-collection', node.id)"
             @drop-event="dropEvent($event, node.id)"
+            @drag-event="dragEvent($event, node.id)"
+            @update-collection-order="updateCollectionOrder($event, node.id)"
+            @dragging="
+              (isDraging) => highlightChildren(isDraging ? node.id : null)
+            "
             @toggle-children="
               () => {
                 toggleChildren(),
@@ -85,6 +93,7 @@
           />
           <CollectionsCollection
             v-if="node.data.type === 'folders'"
+            :id="node.id"
             :data="node.data.data.data"
             :collections-type="collectionsType.type"
             :is-open="isOpen"
@@ -121,6 +130,11 @@
             "
             @remove-collection="emit('remove-folder', node.id)"
             @drop-event="dropEvent($event, node.id)"
+            @drag-event="dragEvent($event, node.id)"
+            @update-collection-order="updateCollectionOrder($event, node.id)"
+            @dragging="
+              (isDraging) => highlightChildren(isDraging ? node.id : null)
+            "
             @toggle-children="
               () => {
                 toggleChildren(),
@@ -182,7 +196,13 @@
             @drag-request="
               dragRequest($event, {
                 folderPath: node.data.data.parentIndex,
-                requestIndex: pathToIndex(node.id),
+                requestIndex: node.id,
+              })
+            "
+            @update-request-order="
+              updateRequestOrder($event, {
+                folderPath: node.data.data.parentIndex,
+                requestIndex: node.id,
               })
             "
           />
@@ -413,7 +433,29 @@ const emit = defineEmits<{
     payload: {
       folderPath: string
       requestIndex: string
-      collectionIndex: string
+      destinationCollectionIndex: string
+    }
+  ): void
+  (
+    event: "drop-collection",
+    payload: {
+      collectionIndexDragged: string
+      destinationCollectionIndex: string
+    }
+  ): void
+  (
+    event: "update-request-order",
+    payload: {
+      dragedRequestIndex: string
+      destinationRequestIndex: string
+      destinationCollectionIndex: string
+    }
+  ): void
+  (
+    event: "update-collection-order",
+    payload: {
+      dragedCollectionIndex: string
+      destinationCollectionIndex: string
     }
   ): void
   (event: "select", payload: Picked | null): void
@@ -502,6 +544,10 @@ const selectRequest = (data: {
   }
 }
 
+const dragEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
+  dataTransfer.setData("collectionIndex", collectionIndex)
+}
+
 const dragRequest = (
   dataTransfer: DataTransfer,
   {
@@ -514,13 +560,56 @@ const dragRequest = (
   dataTransfer.setData("requestIndex", requestIndex)
 }
 
-const dropEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
+const dropEvent = (
+  dataTransfer: DataTransfer,
+  destinationCollectionIndex: string
+) => {
   const folderPath = dataTransfer.getData("folderPath")
   const requestIndex = dataTransfer.getData("requestIndex")
-  emit("drop-request", {
+  const collectionIndexDragged = dataTransfer.getData("collectionIndex")
+
+  if (folderPath && requestIndex) {
+    emit("drop-request", {
+      folderPath,
+      requestIndex,
+      destinationCollectionIndex,
+    })
+  } else {
+    emit("drop-collection", {
+      collectionIndexDragged,
+      destinationCollectionIndex,
+    })
+  }
+}
+
+const updateRequestOrder = (
+  dataTransfer: DataTransfer,
+  {
     folderPath,
     requestIndex,
-    collectionIndex,
+  }: { folderPath: string | null; requestIndex: string }
+) => {
+  if (!folderPath) return
+  const dragedRequestIndex = dataTransfer.getData("requestIndex")
+  const destinationRequestIndex = requestIndex
+  const destinationCollectionIndex = folderPath
+
+  emit("update-request-order", {
+    dragedRequestIndex,
+    destinationRequestIndex,
+    destinationCollectionIndex,
+  })
+}
+
+const updateCollectionOrder = (
+  dataTransfer: DataTransfer,
+  destinationCollectionIndex: string
+) => {
+  const dragedCollectionIndex = dataTransfer.getData("collectionIndex")
+
+  emit("update-collection-order", {
+    dragedCollectionIndex,
+    destinationCollectionIndex,
   })
 }
 

+ 46 - 5
packages/hoppscotch-common/src/components/collections/Request.vue

@@ -1,10 +1,22 @@
 <template>
-  <div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
+  <div class="flex flex-col">
+    <div
+      class="h-1"
+      :class="[
+        {
+          'bg-accentDark': ordering,
+        },
+      ]"
+      @drop="dropEvent"
+      @dragover.prevent="ordering = true"
+      @dragleave="ordering = false"
+      @dragend="ordering = false"
+    ></div>
     <div
       class="flex items-stretch group"
-      draggable="true"
+      :draggable="!hasNoTeamAccess"
       @dragstart="dragStart"
-      @dragover.stop
+      @dragover.prevent="dragging = true"
       @dragleave="dragging = false"
       @dragend="dragging = false"
       @contextmenu.prevent="options?.tippy.show()"
@@ -20,6 +32,7 @@
           class="svg-icons"
           :class="{ 'text-accent': isSelected }"
         />
+        <HoppSmartSpinner v-else-if="isRequestLoading" />
         <span v-else class="font-semibold truncate text-tiny">
           {{ request.method }}
         </span>
@@ -149,6 +162,11 @@ const props = defineProps({
     default: () => ({}),
     required: true,
   },
+  requestID: {
+    type: String,
+    default: "",
+    required: false,
+  },
   collectionsType: {
     type: String as PropType<CollectionType>,
     default: "my-collections",
@@ -175,10 +193,15 @@ const props = defineProps({
     required: false,
   },
   isSelected: {
-    type: Boolean,
+    type: Boolean as PropType<boolean | null>,
     default: false,
     required: false,
   },
+  requestMoveLoading: {
+    type: Array as PropType<string[]>,
+    default: () => [],
+    required: false,
+  },
 })
 
 const emit = defineEmits<{
@@ -187,6 +210,7 @@ const emit = defineEmits<{
   (event: "remove-request"): void
   (event: "select-request"): void
   (event: "drag-request", payload: DataTransfer): void
+  (event: "update-request-order", payload: DataTransfer): void
 }>()
 
 const tippyActions = ref<TippyComponent | null>(null)
@@ -196,6 +220,7 @@ const options = ref<TippyComponent | null>(null)
 const duplicate = ref<HTMLButtonElement | null>(null)
 
 const dragging = ref(false)
+const ordering = ref(false)
 
 const requestMethodLabels = {
   get: "text-green-500",
@@ -228,8 +253,24 @@ const selectRequest = () => {
 
 const dragStart = ({ dataTransfer }: DragEvent) => {
   if (dataTransfer) {
-    dragging.value = !dragging.value
     emit("drag-request", dataTransfer)
+    dragging.value = !dragging.value
+  }
+}
+
+const dropEvent = (e: DragEvent) => {
+  if (e.dataTransfer) {
+    e.stopPropagation()
+    ordering.value = !ordering.value
+    emit("update-request-order", e.dataTransfer)
   }
 }
+
+const isRequestLoading = computed(() => {
+  if (props.requestMoveLoading.length > 0 && props.requestID) {
+    return props.requestMoveLoading.includes(props.requestID)
+  } else {
+    return false
+  }
+})
 </script>

+ 156 - 28
packages/hoppscotch-common/src/components/collections/TeamCollections.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="flex flex-col flex-1">
+  <div class="flex flex-col flex-1 bg-primary">
     <div
       class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
       :style="
@@ -47,14 +47,18 @@
     </div>
     <div class="flex flex-col overflow-hidden">
       <SmartTree :adapter="teamAdapter">
-        <template #content="{ node, toggleChildren, isOpen }">
+        <template
+          #content="{ node, toggleChildren, isOpen, highlightChildren }"
+        >
           <CollectionsCollection
             v-if="node.data.type === 'collections'"
+            :id="node.data.data.data.id"
             :data="node.data.data.data"
             :collections-type="collectionsType.type"
             :is-open="isOpen"
             :export-loading="exportLoading"
             :has-no-team-access="hasNoTeamAccess"
+            :collection-move-loading="collectionMoveLoading"
             :is-selected="
               isSelected({
                 collectionID: node.id,
@@ -87,6 +91,15 @@
                 emit('export-data', node.data.data.data)
             "
             @remove-collection="emit('remove-collection', node.id)"
+            @drop-event="dropEvent($event, node.id)"
+            @drag-event="dragEvent($event, node.id)"
+            @update-collection-order="
+              updateCollectionOrder($event, node.data.data.data.id)
+            "
+            @dragging="
+              (isDraging) =>
+                highlightChildren(isDraging ? node.data.data.data.id : null)
+            "
             @toggle-children="
               () => {
                 toggleChildren(),
@@ -100,11 +113,13 @@
           />
           <CollectionsCollection
             v-if="node.data.type === 'folders'"
+            :id="node.data.data.data.id"
             :data="node.data.data.data"
             :collections-type="collectionsType.type"
             :is-open="isOpen"
             :export-loading="exportLoading"
             :has-no-team-access="hasNoTeamAccess"
+            :collection-move-loading="collectionMoveLoading"
             :is-selected="
               isSelected({
                 folderID: node.data.data.data.id,
@@ -139,6 +154,15 @@
               node.data.type === 'folders' &&
                 emit('remove-folder', node.data.data.data.id)
             "
+            @drop-event="dropEvent($event, node.data.data.data.id)"
+            @drag-event="dragEvent($event, node.data.data.data.id)"
+            @update-collection-order="
+              updateCollectionOrder($event, node.data.data.data.id)
+            "
+            @dragging="
+              (isDraging) =>
+                highlightChildren(isDraging ? node.data.data.data.id : null)
+            "
             @toggle-children="
               () => {
                 toggleChildren(),
@@ -153,10 +177,12 @@
           <CollectionsRequest
             v-if="node.data.type === 'requests'"
             :request="node.data.data.data.request"
+            :request-i-d="node.data.data.data.id"
             :collections-type="collectionsType.type"
             :duplicate-loading="duplicateLoading"
             :is-active="isActiveRequest(node.data.data.data.id)"
             :has-no-team-access="hasNoTeamAccess"
+            :request-move-loading="requestMoveLoading"
             :is-selected="
               isSelected({
                 requestID: node.data.data.data.id,
@@ -190,18 +216,31 @@
                   requestIndex: node.data.data.data.id,
                 })
             "
+            @drag-request="
+              dragRequest($event, {
+                folderPath: node.data.data.parentIndex,
+                requestIndex: node.data.data.data.id,
+              })
+            "
+            @update-request-order="
+              updateRequestOrder($event, {
+                folderPath: node.data.data.parentIndex,
+                requestIndex: node.data.data.data.id,
+              })
+            "
           />
         </template>
         <template #emptyNode="{ node }">
           <div v-if="node === null">
             <div
               class="flex flex-col items-center justify-center p-4 text-secondaryLight"
+              @drop="(e) => e.stopPropagation()"
             >
               <img
                 :src="`/images/states/${colorMode.value}/pack.svg`"
                 loading="lazy"
                 class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
-                :alt="`${t('empty.collections')}`"
+                :alt="`${t('empty.collection')}`"
               />
               <span class="pb-4 text-center">
                 {{ t("empty.collections") }}
@@ -213,11 +252,12 @@
                 filled
                 outline
                 :title="t('team.no_access')"
-                :label="t('add.new')"
+                :label="t('action.new')"
               />
               <HoppButtonSecondary
                 v-else
-                :label="t('add.new')"
+                :icon="IconPlus"
+                :label="t('action.new')"
                 filled
                 outline
                 @click="emit('display-modal-add')"
@@ -227,6 +267,7 @@
           <div
             v-else-if="node.data.type === 'collections'"
             class="flex flex-col items-center justify-center p-4 text-secondaryLight"
+            @drop="(e) => e.stopPropagation()"
           >
             <img
               :src="`/images/states/${colorMode.value}/pack.svg`"
@@ -235,34 +276,13 @@
               :alt="`${t('empty.collection')}`"
             />
             <span class="pb-4 text-center">
-              {{ t("empty.collection") }}
+              {{ t("empty.collections") }}
             </span>
-            <HoppButtonSecondary
-              v-if="hasNoTeamAccess"
-              v-tippy="{ theme: 'tooltip' }"
-              disabled
-              filled
-              outline
-              :title="t('team.no_access')"
-              :label="t('add.new')"
-            />
-            <HoppButtonSecondary
-              v-else
-              :label="t('add.new')"
-              filled
-              outline
-              @click="
-                node.data.type === 'collections' &&
-                  emit('add-folder', {
-                    path: node.id,
-                    folder: node.data.data.data,
-                  })
-              "
-            />
           </div>
           <div
             v-else-if="node.data.type === 'folders'"
             class="flex flex-col items-center justify-center p-4 text-secondaryLight"
+            @drop="(e) => e.stopPropagation()"
           >
             <img
               :src="`/images/states/${colorMode.value}/pack.svg`"
@@ -347,6 +367,16 @@ const props = defineProps({
     default: null,
     required: false,
   },
+  collectionMoveLoading: {
+    type: Array as PropType<string[]>,
+    default: () => [],
+    required: false,
+  },
+  requestMoveLoading: {
+    type: Array as PropType<string[]>,
+    default: () => [],
+    required: false,
+  },
 })
 
 const emit = defineEmits<{
@@ -410,6 +440,36 @@ const emit = defineEmits<{
       folderPath?: string | undefined
     }
   ): void
+  (
+    event: "drop-request",
+    payload: {
+      folderPath: string
+      requestIndex: string
+      destinationCollectionIndex: string
+    }
+  ): void
+  (
+    event: "drop-collection",
+    payload: {
+      collectionIndexDragged: string
+      destinationCollectionIndex: string
+    }
+  ): void
+  (
+    event: "update-request-order",
+    payload: {
+      dragedRequestIndex: string
+      destinationRequestIndex: string
+      destinationCollectionIndex: string
+    }
+  ): void
+  (
+    event: "update-collection-order",
+    payload: {
+      dragedCollectionIndex: string
+      destinationCollectionIndex: string
+    }
+  ): void
   (event: "select", payload: Picked | null): void
   (event: "expand-team-collection", payload: string): void
   (event: "display-modal-add"): void
@@ -493,6 +553,74 @@ const selectRequest = (data: {
   }
 }
 
+const dragRequest = (
+  dataTransfer: DataTransfer,
+  {
+    folderPath,
+    requestIndex,
+  }: { folderPath: string | null; requestIndex: string }
+) => {
+  if (!folderPath) return
+  dataTransfer.setData("folderPath", folderPath)
+  dataTransfer.setData("requestIndex", requestIndex)
+}
+
+const dragEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
+  dataTransfer.setData("collectionIndex", collectionIndex)
+}
+
+const dropEvent = (
+  dataTransfer: DataTransfer,
+  destinationCollectionIndex: string
+) => {
+  const folderPath = dataTransfer.getData("folderPath")
+  const requestIndex = dataTransfer.getData("requestIndex")
+  const collectionIndexDragged = dataTransfer.getData("collectionIndex")
+  if (folderPath && requestIndex) {
+    emit("drop-request", {
+      folderPath,
+      requestIndex,
+      destinationCollectionIndex,
+    })
+  } else {
+    emit("drop-collection", {
+      collectionIndexDragged,
+      destinationCollectionIndex,
+    })
+  }
+}
+
+const updateRequestOrder = (
+  dataTransfer: DataTransfer,
+  {
+    folderPath,
+    requestIndex,
+  }: { folderPath: string | null; requestIndex: string }
+) => {
+  if (!folderPath) return
+  const dragedRequestIndex = dataTransfer.getData("requestIndex")
+  const destinationRequestIndex = requestIndex
+  const destinationCollectionIndex = folderPath
+
+  emit("update-request-order", {
+    dragedRequestIndex,
+    destinationRequestIndex,
+    destinationCollectionIndex,
+  })
+}
+
+const updateCollectionOrder = (
+  dataTransfer: DataTransfer,
+  destinationCollectionIndex: string
+) => {
+  const dragedCollectionIndex = dataTransfer.getData("collectionIndex")
+
+  emit("update-collection-order", {
+    dragedCollectionIndex,
+    destinationCollectionIndex,
+  })
+}
+
 type TeamCollections = {
   type: "collections"
   data: {

+ 359 - 14
packages/hoppscotch-common/src/components/collections/index.vue

@@ -1,5 +1,14 @@
 <template>
-  <div :class="{ 'rounded border border-divider': saveRequest }">
+  <div
+    :class="{
+      'rounded border border-divider': saveRequest,
+      'bg-primaryDark': draggingToRoot,
+    }"
+    class="flex-1"
+    @drop.prevent="dropToRoot"
+    @dragover.prevent="draggingToRoot = true"
+    @dragend="draggingToRoot = false"
+  >
     <div
       class="sticky z-10 flex flex-col flex-shrink-0 overflow-x-auto rounded-t bg-primary"
       :style="
@@ -44,6 +53,9 @@
           @export-data="exportData"
           @remove-collection="removeCollection"
           @remove-folder="removeFolder"
+          @drop-collection="dropCollection"
+          @update-request-order="updateRequestOrder"
+          @update-collection-order="updateCollectionOrder"
           @edit-request="editRequest"
           @duplicate-request="duplicateRequest"
           @remove-request="removeRequest"
@@ -83,6 +95,8 @@
           :duplicate-loading="duplicateLoading"
           :save-request="saveRequest"
           :picked="picked"
+          :collection-move-loading="collectionMoveLoading"
+          :request-move-loading="requestMoveLoading"
           @add-request="addRequest"
           @add-folder="addFolder"
           @edit-collection="editCollection"
@@ -95,12 +109,22 @@
           @remove-request="removeRequest"
           @select-request="selectRequest"
           @select="selectPicked"
+          @drop-request="dropRequest"
+          @drop-collection="dropCollection"
+          @update-request-order="updateRequestOrder"
+          @update-collection-order="updateCollectionOrder"
           @expand-team-collection="expandTeamCollection"
           @display-modal-add="displayModalAdd(true)"
           @display-modal-import-export="displayModalImportExport(true)"
         />
       </HoppSmartTab>
     </HoppSmartTabs>
+    <div
+      class="hidden bg-primaryDark flex-col flex-1 items-center py-15 justify-center px-4 text-secondaryLight"
+      :class="{ '!flex': draggingToRoot }"
+    >
+      <component :is="IconListEnd" class="svg-icons !w-8 !h-8" />
+    </div>
     <CollectionsAdd
       :show="showModalAdd"
       :loading-state="modalLoadingState"
@@ -195,12 +219,15 @@ import {
   editRESTCollection,
   editRESTFolder,
   editRESTRequest,
+  moveRESTFolder,
   moveRESTRequest,
   removeRESTCollection,
   removeRESTFolder,
   removeRESTRequest,
   restCollections$,
   saveRESTRequestAs,
+  updateRESTRequestOrder,
+  updateRESTCollectionOrder,
 } from "~/newstore/collections"
 import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
 import {
@@ -226,11 +253,15 @@ import {
   renameCollection,
   deleteCollection,
   importJSONToTeam,
+  moveRESTTeamCollection,
+  updateOrderRESTTeamCollection,
 } from "~/helpers/backend/mutations/TeamCollection"
 import {
   updateTeamRequest,
   createRequestInCollection,
   deleteTeamRequest,
+  moveRESTTeamRequest,
+  updateOrderRESTTeamRequest,
 } from "~/helpers/backend/mutations/TeamRequest"
 import { TeamCollection } from "~/helpers/teams/TeamCollection"
 import { Collection as NodeCollection } from "./MyCollections.vue"
@@ -244,6 +275,7 @@ import * as E from "fp-ts/Either"
 import { platform } from "~/platform"
 import { createCollectionGists } from "~/helpers/gist"
 import { invokeAction } from "~/helpers/actions"
+import IconListEnd from "~icons/lucide/list-end"
 
 const t = useI18n()
 const toast = useToast()
@@ -324,6 +356,11 @@ const currentUser = useReadonlyStream(
 )
 const myCollections = useReadonlyStream(restCollections$, [], "deep")
 
+// Draging
+const draggingToRoot = ref(false)
+const collectionMoveLoading = ref<string[]>([])
+const requestMoveLoading = ref<string[]>([])
+
 // Export - Import refs
 const collectionJSON = ref("")
 const exportingTeamCollections = ref(false)
@@ -1333,16 +1370,314 @@ const discardRequestChange = () => {
   confirmChangeToRequest.value = false
 }
 
-// Drag and drop functions
+/**
+ * Used to get the index of the request from the path
+ * @param path The path of the request
+ * @returns The index of the request
+ */
+const pathToIndex = computed(() => {
+  return (path: string) => {
+    const pathArr = path.split("/")
+    return parseInt(pathArr[pathArr.length - 1])
+  }
+})
+
+/**
+ * This function is called when the user drops the request inside a collection
+ * @param payload Object that contains the folder path, request index and the destination collection index
+ */
 const dropRequest = (payload: {
-  folderPath: string
+  folderPath?: string | undefined
   requestIndex: string
-  collectionIndex: string
+  destinationCollectionIndex: string
+}) => {
+  const { folderPath, requestIndex, destinationCollectionIndex } = payload
+  if (!requestIndex || !destinationCollectionIndex) return
+  if (collectionsType.value.type === "my-collections" && folderPath) {
+    moveRESTRequest(
+      folderPath,
+      pathToIndex.value(requestIndex),
+      destinationCollectionIndex
+    )
+    toast.success(`${t("request.moved")}`)
+    draggingToRoot.value = false
+  } else if (hasTeamWriteAccess.value) {
+    // add the request index to the loading array
+    requestMoveLoading.value.push(requestIndex)
+
+    pipe(
+      moveRESTTeamRequest(destinationCollectionIndex, requestIndex),
+      TE.match(
+        (err: GQLError<string>) => {
+          toast.error(`${getErrorMessage(err)}`)
+          requestMoveLoading.value.splice(
+            requestMoveLoading.value.indexOf(requestIndex),
+            1
+          )
+        },
+        () => {
+          // remove the request index from the loading array
+          requestMoveLoading.value.splice(
+            requestMoveLoading.value.indexOf(requestIndex),
+            1
+          )
+          toast.success(`${t("request.moved")}`)
+        }
+      )
+    )()
+  }
+}
+
+/**
+ * This function is called when the user moves the collection
+ * to a different collection or folder
+ * @param payload - object containing the collection index dragged and the destination collection index
+ */
+const dropCollection = (payload: {
+  collectionIndexDragged: string
+  destinationCollectionIndex: string
+}) => {
+  const { collectionIndexDragged, destinationCollectionIndex } = payload
+  if (!collectionIndexDragged || !destinationCollectionIndex) return
+  if (collectionIndexDragged === destinationCollectionIndex) return
+  if (collectionsType.value.type === "my-collections") {
+    moveRESTFolder(collectionIndexDragged, destinationCollectionIndex)
+    draggingToRoot.value = false
+    toast.success(`${t("collection.moved")}`)
+  } else if (hasTeamWriteAccess.value) {
+    // add the collection index to the loading array
+    collectionMoveLoading.value.push(collectionIndexDragged)
+    pipe(
+      moveRESTTeamCollection(
+        collectionIndexDragged,
+        destinationCollectionIndex
+      ),
+      TE.match(
+        (err: GQLError<string>) => {
+          toast.error(`${getErrorMessage(err)}`)
+          collectionMoveLoading.value.splice(
+            collectionMoveLoading.value.indexOf(collectionIndexDragged),
+            1
+          )
+        },
+        () => {
+          toast.success(`${t("collection.moved")}`)
+          // remove the collection index from the loading array
+          collectionMoveLoading.value.splice(
+            collectionMoveLoading.value.indexOf(collectionIndexDragged),
+            1
+          )
+        }
+      )
+    )()
+  }
+}
+
+/**
+ * Checks if the collection is already in the root
+ * @param id - path of the collection
+ * @returns boolean - true if the collection is already in the root
+ */
+const isAlreadyInRoot = computed(() => {
+  return (id: string) => {
+    const indexPath = id.split("/").map((i) => parseInt(i))
+    return indexPath.length === 1
+  }
+})
+
+/**
+ * This function is called when the user drops the collection
+ * to the root
+ * @param payload - object containing the collection index dragged
+ */
+const dropToRoot = ({ dataTransfer }: DragEvent) => {
+  if (dataTransfer) {
+    const collectionIndexDragged = dataTransfer.getData("collectionIndex")
+    if (!collectionIndexDragged) return
+    if (collectionsType.value.type === "my-collections") {
+      // check if the collection is already in the root
+      if (isAlreadyInRoot.value(collectionIndexDragged)) {
+        toast.error(`${t("collection.invalid_root_move")}`)
+      } else {
+        moveRESTFolder(collectionIndexDragged, null)
+        toast.success(`${t("collection.moved")}`)
+      }
+
+      draggingToRoot.value = false
+    } else if (hasTeamWriteAccess.value) {
+      // add the collection index to the loading array
+      collectionMoveLoading.value.push(collectionIndexDragged)
+
+      // destination collection index is null since we are moving to root
+      pipe(
+        moveRESTTeamCollection(collectionIndexDragged, null),
+        TE.match(
+          (err: GQLError<string>) => {
+            collectionMoveLoading.value.splice(
+              collectionMoveLoading.value.indexOf(collectionIndexDragged),
+              1
+            )
+            toast.error(`${getErrorMessage(err)}`)
+          },
+          () => {
+            // remove the collection index from the loading array
+            collectionMoveLoading.value.splice(
+              collectionMoveLoading.value.indexOf(collectionIndexDragged),
+              1
+            )
+            toast.success(`${t("collection.moved")}`)
+          }
+        )
+      )()
+    }
+  }
+}
+
+/**
+ * Used to check if the request/collection is being moved to the same parent since reorder is only allowed within the same parent
+ * @param draggedReq - path index of the dragged request
+ * @param destinationReq - path index of the destination request
+ * @returns boolean - true if the request is being moved to the same parent
+ */
+const isSameSameParent = computed(
+  () => (draggedReq: string, destinationReq: string) => {
+    const draggedReqIndex = draggedReq.split("/").map((i) => parseInt(i))
+    const destinationReqIndex = destinationReq
+      .split("/")
+      .map((i) => parseInt(i))
+
+    // length of 1 means the request is in the root
+    if (draggedReqIndex.length === 1 && destinationReqIndex.length === 1) {
+      return true
+    } else if (
+      draggedReqIndex[draggedReqIndex.length - 2] ===
+      destinationReqIndex[destinationReqIndex.length - 2]
+    ) {
+      return true
+    } else {
+      return false
+    }
+  }
+)
+
+/**
+ * This function is called when the user updates the request order in a collection
+ * @param payload - object containing the request index dragged and the destination request index
+ *  with the destination collection index
+ */
+const updateRequestOrder = (payload: {
+  dragedRequestIndex: string
+  destinationRequestIndex: string
+  destinationCollectionIndex: string
 }) => {
-  const { folderPath, requestIndex, collectionIndex } = payload
-  moveRESTRequest(folderPath, parseInt(requestIndex), collectionIndex)
+  const {
+    dragedRequestIndex,
+    destinationRequestIndex,
+    destinationCollectionIndex,
+  } = payload
+
+  if (
+    !dragedRequestIndex ||
+    !destinationRequestIndex ||
+    !destinationCollectionIndex
+  )
+    return
+
+  if (dragedRequestIndex === destinationRequestIndex) return
+
+  if (collectionsType.value.type === "my-collections") {
+    if (!isSameSameParent.value(dragedRequestIndex, destinationRequestIndex)) {
+      toast.error(`${t("collection.different_parent")}`)
+    } else {
+      updateRESTRequestOrder(
+        pathToIndex.value(dragedRequestIndex),
+        pathToIndex.value(destinationRequestIndex),
+        destinationCollectionIndex
+      )
+      toast.success(`${t("request.order_changed")}`)
+    }
+  } else if (hasTeamWriteAccess.value) {
+    // add the request index to the loading array
+    requestMoveLoading.value.push(dragedRequestIndex)
+
+    pipe(
+      updateOrderRESTTeamRequest(
+        dragedRequestIndex,
+        destinationRequestIndex,
+        destinationCollectionIndex
+      ),
+      TE.match(
+        (err: GQLError<string>) => {
+          toast.error(`${getErrorMessage(err)}`)
+          requestMoveLoading.value.splice(
+            requestMoveLoading.value.indexOf(dragedRequestIndex),
+            1
+          )
+        },
+        () => {
+          toast.success(`${t("request.order_changed")}`)
+
+          // remove the request index from the loading array
+          requestMoveLoading.value.splice(
+            requestMoveLoading.value.indexOf(dragedRequestIndex),
+            1
+          )
+        }
+      )
+    )()
+  }
 }
 
+/**
+ * This function is called when the user updates the collection or folder order
+ * @param payload - object containing the collection index dragged and the destination collection index
+ */
+const updateCollectionOrder = (payload: {
+  dragedCollectionIndex: string
+  destinationCollectionIndex: string
+}) => {
+  const { dragedCollectionIndex, destinationCollectionIndex } = payload
+  if (!dragedCollectionIndex || !destinationCollectionIndex) return
+  if (dragedCollectionIndex === destinationCollectionIndex) return
+
+  if (collectionsType.value.type === "my-collections") {
+    if (
+      !isSameSameParent.value(dragedCollectionIndex, destinationCollectionIndex)
+    ) {
+      toast.error(`${t("collection.different_parent")}`)
+    } else {
+      updateRESTCollectionOrder(
+        dragedCollectionIndex,
+        destinationCollectionIndex
+      )
+      toast.success(`${t("collection.order_changed")}`)
+    }
+  } else if (hasTeamWriteAccess.value) {
+    collectionMoveLoading.value.push(dragedCollectionIndex)
+    pipe(
+      updateOrderRESTTeamCollection(
+        dragedCollectionIndex,
+        destinationCollectionIndex
+      ),
+      TE.match(
+        (err: GQLError<string>) => {
+          toast.error(`${getErrorMessage(err)}`)
+          collectionMoveLoading.value.splice(
+            collectionMoveLoading.value.indexOf(dragedCollectionIndex),
+            1
+          )
+        },
+        () => {
+          toast.success(`${t("collection.order_changed")}`)
+          collectionMoveLoading.value.splice(
+            collectionMoveLoading.value.indexOf(dragedCollectionIndex),
+            1
+          )
+        }
+      )
+    )()
+  }
+}
 // Import - Export Collection functions
 /**
  * Export the whole my collection or specific team collection to JSON
@@ -1525,28 +1860,38 @@ const resetSelectedData = () => {
 }
 
 const getErrorMessage = (err: GQLError<string>) => {
+  console.error(err)
   if (err.type === "network_error") {
-    console.error(err)
     return t("error.network_error")
   } else {
     switch (err.error) {
       case "team_coll/short_title":
-        console.error(err)
         return t("collection.name_length_insufficient")
       case "team/invalid_coll_id":
-        console.error(err)
-        return t("team.invalid_id")
+      case "bug/team_coll/no_coll_id":
+      case "team_req/invalid_target_id":
+        return t("team.invalid_coll_id")
       case "team/not_required_role":
-        console.error(err)
         return t("profile.no_permission")
       case "team_req/not_required_role":
-        console.error(err)
         return t("profile.no_permission")
       case "Forbidden resource":
-        console.error(err)
         return t("profile.no_permission")
+      case "team_req/not_found":
+        return t("team.no_request_found")
+      case "bug/team_req/no_req_id":
+        return t("team.no_request_found")
+      case "team/collection_is_parent_coll":
+        return t("team.parent_coll_move")
+      case "team/target_and_destination_collection_are_same":
+        return t("team.same_target_destination")
+      case "team/target_collection_is_already_root_collection":
+        return t("collection.invalid_root_move")
+      case "team_req/requests_not_from_same_collection":
+        return t("request.different_collection")
+      case "team/team_collections_have_different_parents":
+        return t("collection.different_parent")
       default:
-        console.error(err)
         return t("error.something_went_wrong")
     }
   }

+ 4 - 1
packages/hoppscotch-common/src/components/smart/Tree.vue

@@ -13,12 +13,15 @@
           :node-item="rootNode"
           :adapter="adapter as SmartTreeAdapter<T>"
         >
-          <template #default="{ node, toggleChildren, isOpen }">
+          <template
+            #default="{ node, toggleChildren, isOpen, highlightChildren }"
+          >
             <slot
               name="content"
               :node="node as TreeNode<T>"
               :toggle-children="toggleChildren as () => void"
               :is-open="isOpen as boolean"
+              :highlight-children="(id:string|null) => highlightChildren(id)"
             ></slot>
           </template>
           <template #emptyNode="{ node }">

+ 23 - 1
packages/hoppscotch-common/src/components/smart/TreeBranch.vue

@@ -3,6 +3,7 @@
     :node="nodeItem"
     :toggle-children="toggleNodeChildren"
     :is-open="isNodeOpen"
+    :highlight-children="(id:string|null) => highlightNodeChildren(id)"
   ></slot>
 
   <!-- This is a performance optimization trick -->
@@ -20,6 +21,9 @@
     <div
       v-if="childNodes.status === 'loaded' && childNodes.data.length > 0"
       class="flex flex-col flex-1 truncate"
+      :class="{
+        'bg-divider': highlightNode,
+      }"
     >
       <TreeBranch
         v-for="childNode in childNodes.data"
@@ -28,12 +32,20 @@
         :adapter="adapter"
       >
         <!-- The child slot is given a dynamic name in order to not break Volar -->
-        <template #[CHILD_SLOT_NAME]="{ node, toggleChildren, isOpen }">
+        <template
+          #[CHILD_SLOT_NAME]="{
+            node,
+            toggleChildren,
+            isOpen,
+            highlightChildren,
+          }"
+        >
           <!-- Casting to help with type checking -->
           <slot
             :node="node as TreeNode<T>"
             :toggle-children="toggleChildren as () => void"
             :is-open="isOpen as boolean"
+            :highlight-children="(id:string|null) => highlightChildren(id) as void"
           ></slot>
         </template>
         <template #emptyNode="{ node }">
@@ -87,6 +99,8 @@ const childrenRendered = ref(false)
 const showChildren = ref(false)
 const isNodeOpen = ref(false)
 
+const highlightNode = ref(false)
+
 /**
  * Fetch the child nodes from the adapter by passing the node id of the current node
  */
@@ -100,4 +114,12 @@ const toggleNodeChildren = () => {
   showChildren.value = !showChildren.value
   isNodeOpen.value = !isNodeOpen.value
 }
+
+const highlightNodeChildren = (id: string | null) => {
+  if (id) {
+    highlightNode.value = true
+  } else {
+    highlightNode.value = false
+  }
+}
 </script>

+ 8 - 0
packages/hoppscotch-common/src/helpers/backend/gql/mutations/MoveRESTTeamCollection.graphql

@@ -0,0 +1,8 @@
+mutation MoveRESTTeamCollection($collectionID: ID!, $parentCollectionID: ID) {
+  moveCollection(
+    collectionID: $collectionID
+    parentCollectionID: $parentCollectionID
+  ) {
+    id
+  }
+}

Some files were not shown because too many files changed in this diff