From c78b46d62bbe9c35f43c715a37a5277adbf71858 Mon Sep 17 00:00:00 2001
From: Ryan Laurie <30528226+iethree@users.noreply.github.com>
Date: Mon, 19 Aug 2024 08:48:54 -0600
Subject: [PATCH] Remove unnecessary calls to `/api/collection` (#46752)

* use ee api to find custom reports collection

* remove deprecated hook

* fix race condition

* add unit tests

* fix circular dependency

* fix unit tests

* another plugin strategy
---
 .../metabase-enterprise/collections/index.ts  |   5 +-
 .../use-get-default-collection-id/index.ts    |   1 +
 .../use-get-default-collection-id.ts          |  39 ++++
 ...se-get-default-collection-id.unit.spec.tsx | 191 ++++++++++++++++++
 .../metabase-enterprise/collections/utils.ts  |  16 +-
 .../CollectionContent/CollectionContent.tsx   |  18 +-
 frontend/src/metabase/collections/hooks.ts    |  29 +++
 .../metabase/collections/hooks.unit.spec.tsx  | 135 +++++++++++++
 frontend/src/metabase/collections/utils.ts    |   6 -
 .../common/hooks/entity-framework/index.ts    |   1 -
 .../use-collection-list-query/index.ts        |   1 -
 .../use-collection-list-query.ts              |  24 ---
 .../use-collection-list-query.unit.spec.tsx   |  81 --------
 .../components/SaveQuestionForm/context.tsx   |  18 +-
 .../components/SaveQuestionForm/util.ts       |  26 +--
 .../SaveQuestionModal.unit.spec.tsx           |  33 +--
 .../collections/getInitialCollectionId.ts     |   2 +-
 .../entities/containers/EntityCopyModal.tsx   |  70 +++----
 .../MainNavbarContainer.tsx                   |  20 +-
 frontend/src/metabase/plugins/index.ts        |   9 +-
 .../components/QueryModals/QueryModals.tsx    |   6 +-
 .../questions/components/CopyQuestionForm.tsx |   1 +
 22 files changed, 483 insertions(+), 249 deletions(-)
 create mode 100644 enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/index.ts
 create mode 100644 enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/use-get-default-collection-id.ts
 create mode 100644 enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/use-get-default-collection-id.unit.spec.tsx
 create mode 100644 frontend/src/metabase/collections/hooks.ts
 create mode 100644 frontend/src/metabase/collections/hooks.unit.spec.tsx
 delete mode 100644 frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/index.ts
 delete mode 100644 frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/use-collection-list-query.ts
 delete mode 100644 frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/use-collection-list-query.unit.spec.tsx

diff --git a/enterprise/frontend/src/metabase-enterprise/collections/index.ts b/enterprise/frontend/src/metabase-enterprise/collections/index.ts
index 6047f8a3a17..f2bf384a9bb 100644
--- a/enterprise/frontend/src/metabase-enterprise/collections/index.ts
+++ b/enterprise/frontend/src/metabase-enterprise/collections/index.ts
@@ -16,10 +16,10 @@ import {
   OFFICIAL_COLLECTION,
   CUSTOM_INSTANCE_ANALYTICS_COLLECTION_ENTITY_ID,
 } from "./constants";
+import { useGetDefaultCollectionId } from "./use-get-default-collection-id";
 import {
   getCollectionType,
   isRegularCollection,
-  getInstanceAnalyticsCustomCollection,
   getIcon,
   filterOutItemsFromInstanceAnalytics,
 } from "./utils";
@@ -77,8 +77,7 @@ if (hasPremiumFeature("audit_app")) {
     CollectionInstanceAnalyticsIcon;
 
   PLUGIN_COLLECTIONS.getCollectionType = getCollectionType;
-  PLUGIN_COLLECTIONS.getInstanceAnalyticsCustomCollection =
-    getInstanceAnalyticsCustomCollection;
+  PLUGIN_COLLECTIONS.useGetDefaultCollectionId = useGetDefaultCollectionId;
   PLUGIN_COLLECTIONS.CUSTOM_INSTANCE_ANALYTICS_COLLECTION_ENTITY_ID =
     CUSTOM_INSTANCE_ANALYTICS_COLLECTION_ENTITY_ID;
 
diff --git a/enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/index.ts b/enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/index.ts
new file mode 100644
index 00000000000..2e9640a8b32
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/index.ts
@@ -0,0 +1 @@
+export * from "./use-get-default-collection-id";
diff --git a/enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/use-get-default-collection-id.ts b/enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/use-get-default-collection-id.ts
new file mode 100644
index 00000000000..d9a8b950381
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/use-get-default-collection-id.ts
@@ -0,0 +1,39 @@
+import { skipToken, useGetCollectionQuery } from "metabase/api";
+import { _useGetDefaultCollectionId as useOSSGetDefaultCollectionId } from "metabase/collections/hooks";
+import { useGetAuditInfoQuery } from "metabase-enterprise/api";
+import { isInstanceAnalyticsCollection } from "metabase-enterprise/collections/utils";
+import type { CollectionId } from "metabase-types/api";
+
+/**
+ * if the source collection is in the instance analytics collection, the default save location
+ * should be in the custom reports collection
+ */
+export const useGetDefaultCollectionId = (
+  sourceCollectionId?: CollectionId | null,
+): CollectionId | null => {
+  const { data: auditInfo } = useGetAuditInfoQuery(
+    sourceCollectionId ? undefined : skipToken,
+  );
+
+  const { data: collectionInfo } = useGetCollectionQuery(
+    sourceCollectionId ? { id: sourceCollectionId } : skipToken,
+  );
+
+  const { data: customReportsCollectionInfo } = useGetCollectionQuery(
+    auditInfo?.custom_reports ? { id: auditInfo?.custom_reports } : skipToken,
+  );
+
+  const isIAcollection = isInstanceAnalyticsCollection(collectionInfo);
+
+  const initialCollectionId = useOSSGetDefaultCollectionId(sourceCollectionId);
+
+  if (
+    isIAcollection &&
+    auditInfo?.custom_reports &&
+    customReportsCollectionInfo?.can_write
+  ) {
+    return auditInfo.custom_reports;
+  }
+
+  return initialCollectionId;
+};
diff --git a/enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/use-get-default-collection-id.unit.spec.tsx b/enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/use-get-default-collection-id.unit.spec.tsx
new file mode 100644
index 00000000000..34b41dc26aa
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/collections/use-get-default-collection-id/use-get-default-collection-id.unit.spec.tsx
@@ -0,0 +1,191 @@
+import fetchMock from "fetch-mock";
+
+import { setupCollectionByIdEndpoint } from "__support__/server-mocks";
+import { createMockEntitiesState } from "__support__/store";
+import { renderWithProviders, screen } from "__support__/ui";
+import type { AuditInfo } from "metabase-enterprise/audit_app/types/state";
+import type { Collection, CollectionId } from "metabase-types/api";
+import { createMockCollection, createMockUser } from "metabase-types/api/mocks";
+import { createMockState } from "metabase-types/store/mocks";
+
+import { useGetDefaultCollectionId } from "./use-get-default-collection-id";
+
+const TestComponent = ({
+  collectionId,
+}: {
+  collectionId: CollectionId | null;
+}) => {
+  const defaultCollectionId = useGetDefaultCollectionId(collectionId);
+
+  return <div>id: {JSON.stringify(defaultCollectionId)}</div>;
+};
+
+const defaultAuditInfo: AuditInfo = {
+  dashboard_overview: 201,
+  question_overview: 202,
+  custom_reports: 203,
+};
+
+const user = createMockUser({
+  id: 801,
+  first_name: "Ash",
+  personal_collection_id: 301,
+});
+
+const defaultCollections: Collection[] = [
+  createMockCollection({ id: 101, name: "My Collection" }),
+  createMockCollection({ id: 102, name: "Other Collection" }),
+  createMockCollection({
+    id: 202,
+    name: "Instance Analytics",
+    type: "instance-analytics",
+    can_write: false,
+  }),
+  createMockCollection({ id: 203, name: "Custom Reports", can_write: true }),
+  createMockCollection({
+    id: 301,
+    name: "Ash's Personal Collection",
+    is_personal: true,
+  }),
+];
+
+const setup = ({
+  collectionId,
+  collections,
+  hasRootAccess = true,
+}: {
+  collectionId: CollectionId | null;
+  collections: Collection[];
+  hasRootAccess?: boolean;
+}) => {
+  setupCollectionByIdEndpoint({ collections });
+  fetchMock.get("path:/api/ee/audit-app/user/audit-info", defaultAuditInfo);
+
+  const entitiesState = createMockEntitiesState({
+    collections: [
+      createMockCollection({
+        id: "root",
+        name: "Our analytics",
+        can_write: hasRootAccess,
+      }),
+      ...collections,
+    ],
+  });
+  const state = createMockState({ currentUser: user, entities: entitiesState });
+
+  renderWithProviders(<TestComponent collectionId={collectionId} />, {
+    storeInitialState: state,
+  });
+};
+
+describe("enterprise > useGetDefaultCollectionId", () => {
+  describe("Regular Collection Source", () => {
+    it("should default to the root collection", async () => {
+      setup({
+        collectionId: null,
+        hasRootAccess: true,
+        collections: defaultCollections,
+      });
+
+      expect(await screen.findByText("id: null")).toBeInTheDocument();
+    });
+
+    it("should return the user's personal collection when the user lacks write access to our analytics", async () => {
+      setup({
+        collectionId: null,
+        hasRootAccess: false,
+        collections: defaultCollections,
+      });
+
+      expect(await screen.findByText("id: 301")).toBeInTheDocument();
+    });
+
+    it("should use the passed collection id if the user has access", async () => {
+      setup({
+        collectionId: 101,
+        hasRootAccess: false,
+        collections: defaultCollections,
+      });
+
+      expect(await screen.findByText("id: 101")).toBeInTheDocument();
+    });
+
+    it("should fall back to the root collection if the user doesn't have access to the passed collection", async () => {
+      setup({
+        collectionId: 101,
+        hasRootAccess: true,
+        collections: [
+          createMockCollection({
+            id: 101,
+            name: "My Collection",
+            can_write: false,
+          }),
+        ],
+      });
+
+      expect(await screen.findByText("id: null")).toBeInTheDocument();
+    });
+
+    it("should fall back to the personal collection if the user doesn't have access to the passed collection or the root collection", async () => {
+      setup({
+        collectionId: 101,
+        hasRootAccess: false,
+        collections: [
+          createMockCollection({
+            id: 101,
+            name: "My Collection",
+            can_write: false,
+          }),
+        ],
+      });
+
+      expect(await screen.findByText("id: 301")).toBeInTheDocument();
+    });
+  });
+
+  describe("Instance Analytics Source", () => {
+    it("should suggest the custom reports collection if the source id is the instance analytics collection", async () => {
+      setup({
+        collectionId: 202,
+        hasRootAccess: true,
+        collections: [
+          createMockCollection({
+            id: 202,
+            name: "Instance Analytics",
+            type: "instance-analytics",
+            can_write: false,
+          }),
+          createMockCollection({
+            id: 203,
+            name: "Custom Reports",
+            can_write: true,
+          }),
+        ],
+      });
+
+      expect(await screen.findByText("id: 203")).toBeInTheDocument();
+    });
+
+    it("should not return the custom reports collection if the user lacks write access to it", async () => {
+      setup({
+        collectionId: 202,
+        hasRootAccess: false,
+        collections: [
+          createMockCollection({
+            id: 202,
+            name: "Instance Analytics",
+            type: "instance-analytics",
+            can_write: false,
+          }),
+          createMockCollection({
+            id: 203,
+            name: "Custom Reports",
+            can_write: false,
+          }),
+        ],
+      });
+
+      expect(await screen.findByText("id: 301")).toBeInTheDocument();
+    });
+  });
+});
diff --git a/enterprise/frontend/src/metabase-enterprise/collections/utils.ts b/enterprise/frontend/src/metabase-enterprise/collections/utils.ts
index 12672891dbe..33551c7c2cd 100644
--- a/enterprise/frontend/src/metabase-enterprise/collections/utils.ts
+++ b/enterprise/frontend/src/metabase-enterprise/collections/utils.ts
@@ -1,4 +1,3 @@
-import { isInstanceAnalyticsCollection } from "metabase/collections/utils";
 import type { IconData, ObjectWithModel } from "metabase/lib/icon";
 import { getIconBase } from "metabase/lib/icon";
 import type { ItemWithCollection } from "metabase/plugins";
@@ -12,7 +11,6 @@ import type {
 
 import {
   COLLECTION_TYPES,
-  CUSTOM_INSTANCE_ANALYTICS_COLLECTION_ENTITY_ID,
   INSTANCE_ANALYTICS_COLLECTION,
   OFFICIAL_COLLECTION,
   REGULAR_COLLECTION,
@@ -40,13 +38,13 @@ export function getCollectionType({
   );
 }
 
-export const getInstanceAnalyticsCustomCollection = (
-  collections: Collection[],
-) =>
-  collections?.find?.(
-    collection =>
-      collection.entity_id === CUSTOM_INSTANCE_ANALYTICS_COLLECTION_ENTITY_ID,
-  ) ?? null;
+export function isInstanceAnalyticsCollection(
+  collection?: Pick<Collection, "type">,
+): boolean {
+  return (
+    !!collection && getCollectionType(collection).type === "instance-analytics"
+  );
+}
 
 export const getIcon = (item: ObjectWithModel): IconData => {
   if (getCollectionType({ type: item.type }).type === "instance-analytics") {
diff --git a/frontend/src/metabase/collections/components/CollectionContent/CollectionContent.tsx b/frontend/src/metabase/collections/components/CollectionContent/CollectionContent.tsx
index 3d1e5c1cbda..2b97deae3ba 100644
--- a/frontend/src/metabase/collections/components/CollectionContent/CollectionContent.tsx
+++ b/frontend/src/metabase/collections/components/CollectionContent/CollectionContent.tsx
@@ -1,8 +1,8 @@
 import { useCallback } from "react";
 
+import { useListCollectionsTreeQuery } from "metabase/api";
 import {
   useBookmarkListQuery,
-  useCollectionListQuery,
   useCollectionQuery,
   useDatabaseListQuery,
 } from "metabase/common/hooks";
@@ -29,15 +29,13 @@ export function CollectionContent({
 }) {
   const { data: bookmarks, error: bookmarksError } = useBookmarkListQuery();
   const { data: databases, error: databasesError } = useDatabaseListQuery();
-  const { data: collections, error: collectionsError } = useCollectionListQuery(
-    {
-      query: {
-        tree: true,
-        "exclude-other-user-collections": true,
-        "exclude-archived": true,
-      },
-    },
-  );
+
+  const { data: collections, error: collectionsError } =
+    useListCollectionsTreeQuery({
+      "exclude-other-user-collections": true,
+      "exclude-archived": true,
+    });
+
   const { data: collection, error: collectionError } = useCollectionQuery({
     id: collectionId,
   });
diff --git a/frontend/src/metabase/collections/hooks.ts b/frontend/src/metabase/collections/hooks.ts
new file mode 100644
index 00000000000..d5d9d817d46
--- /dev/null
+++ b/frontend/src/metabase/collections/hooks.ts
@@ -0,0 +1,29 @@
+import getInitialCollectionId from "metabase/entities/collections/getInitialCollectionId";
+import { useSelector } from "metabase/lib/redux";
+import { PLUGIN_COLLECTIONS } from "metabase/plugins";
+import type { CollectionId } from "metabase-types/api";
+
+export const _useGetDefaultCollectionId = (
+  sourceCollectionId?: CollectionId | null,
+): CollectionId | null => {
+  // TODO: refactor this selector to be this hook and fetch the necessary collections
+  // right now we assume that the root collection and any other relevant collections are already
+  // in the redux store
+  const initialCollectionId = useSelector(state =>
+    getInitialCollectionId(state, {
+      collectionId: sourceCollectionId ?? undefined,
+    }),
+  );
+
+  return initialCollectionId;
+};
+
+export const useGetDefaultCollectionId = (
+  sourceCollectionId?: CollectionId | null,
+): CollectionId | null => {
+  if (PLUGIN_COLLECTIONS.useGetDefaultCollectionId) {
+    // eslint-disable-next-line react-hooks/rules-of-hooks -- this won't change at runtime, so it's safe
+    return PLUGIN_COLLECTIONS.useGetDefaultCollectionId(sourceCollectionId);
+  }
+  return _useGetDefaultCollectionId(sourceCollectionId);
+};
diff --git a/frontend/src/metabase/collections/hooks.unit.spec.tsx b/frontend/src/metabase/collections/hooks.unit.spec.tsx
new file mode 100644
index 00000000000..de4c3e8f450
--- /dev/null
+++ b/frontend/src/metabase/collections/hooks.unit.spec.tsx
@@ -0,0 +1,135 @@
+import { setupCollectionByIdEndpoint } from "__support__/server-mocks";
+import { createMockEntitiesState } from "__support__/store";
+import { renderWithProviders, screen } from "__support__/ui";
+import type { Collection, CollectionId } from "metabase-types/api";
+import { createMockCollection, createMockUser } from "metabase-types/api/mocks";
+import { createMockState } from "metabase-types/store/mocks";
+
+import { useGetDefaultCollectionId } from "./hooks";
+
+const TestComponent = ({
+  collectionId,
+}: {
+  collectionId: CollectionId | null;
+}) => {
+  const defaultCollectionId = useGetDefaultCollectionId(collectionId);
+
+  return <div>id: {JSON.stringify(defaultCollectionId)}</div>;
+};
+
+const user = createMockUser({
+  id: 801,
+  first_name: "Ash",
+  personal_collection_id: 301,
+});
+
+const defaultCollections: Collection[] = [
+  createMockCollection({ id: 101, name: "My Collection" }),
+  createMockCollection({ id: 102, name: "Other Collection" }),
+  createMockCollection({
+    id: 202,
+    name: "Instance Analytics",
+    type: "instance-analytics",
+    can_write: false,
+  }),
+  createMockCollection({ id: 203, name: "Custom Reports", can_write: true }),
+  createMockCollection({
+    id: 301,
+    name: "Ash's Personal Collection",
+    is_personal: true,
+  }),
+];
+
+const setup = ({
+  collectionId,
+  collections,
+  hasRootAccess = true,
+}: {
+  collectionId: CollectionId | null;
+  collections: Collection[];
+  hasRootAccess?: boolean;
+}) => {
+  setupCollectionByIdEndpoint({ collections });
+
+  const entitiesState = createMockEntitiesState({
+    collections: [
+      createMockCollection({
+        id: "root",
+        name: "Our analytics",
+        can_write: hasRootAccess,
+      }),
+      ...collections,
+    ],
+  });
+  const state = createMockState({ currentUser: user, entities: entitiesState });
+
+  renderWithProviders(<TestComponent collectionId={collectionId} />, {
+    storeInitialState: state,
+  });
+};
+
+describe("enterprise > useGetDefaultCollectionId", () => {
+  describe("Regular Collection Source", () => {
+    it("should default to the root collection", async () => {
+      setup({
+        collectionId: null,
+        hasRootAccess: true,
+        collections: defaultCollections,
+      });
+
+      expect(await screen.findByText("id: null")).toBeInTheDocument();
+    });
+
+    it("should return the user's personal collection when the user lacks write access to our analytics", async () => {
+      setup({
+        collectionId: null,
+        hasRootAccess: false,
+        collections: defaultCollections,
+      });
+
+      expect(await screen.findByText("id: 301")).toBeInTheDocument();
+    });
+
+    it("should use the passed collection id if the user has access", async () => {
+      setup({
+        collectionId: 101,
+        hasRootAccess: false,
+        collections: defaultCollections,
+      });
+
+      expect(await screen.findByText("id: 101")).toBeInTheDocument();
+    });
+
+    it("should fall back to the root collection if the user doesn't have access to the passed collection", async () => {
+      setup({
+        collectionId: 101,
+        hasRootAccess: true,
+        collections: [
+          createMockCollection({
+            id: 101,
+            name: "My Collection",
+            can_write: false,
+          }),
+        ],
+      });
+
+      expect(await screen.findByText("id: null")).toBeInTheDocument();
+    });
+
+    it("should fall back to the personal collection if the user doesn't have access to the passed collection or the root collection", async () => {
+      setup({
+        collectionId: 101,
+        hasRootAccess: false,
+        collections: [
+          createMockCollection({
+            id: 101,
+            name: "My Collection",
+            can_write: false,
+          }),
+        ],
+      });
+
+      expect(await screen.findByText("id: 301")).toBeInTheDocument();
+    });
+  });
+});
diff --git a/frontend/src/metabase/collections/utils.ts b/frontend/src/metabase/collections/utils.ts
index 3c6db7b493d..0d30d91855b 100644
--- a/frontend/src/metabase/collections/utils.ts
+++ b/frontend/src/metabase/collections/utils.ts
@@ -63,12 +63,6 @@ export function isInstanceAnalyticsCollection(
   );
 }
 
-export function getInstanceAnalyticsCustomCollection(
-  collections: Collection[],
-): Collection | null {
-  return PLUGIN_COLLECTIONS.getInstanceAnalyticsCustomCollection(collections);
-}
-
 export function isInstanceAnalyticsCustomCollection(
   collection: Collection,
 ): boolean {
diff --git a/frontend/src/metabase/common/hooks/entity-framework/index.ts b/frontend/src/metabase/common/hooks/entity-framework/index.ts
index b0d5f3a246f..e508331ec7e 100644
--- a/frontend/src/metabase/common/hooks/entity-framework/index.ts
+++ b/frontend/src/metabase/common/hooks/entity-framework/index.ts
@@ -6,7 +6,6 @@
 export * from "./use-action-list-query";
 export * from "./use-action-query";
 export * from "./use-bookmark-list-query";
-export * from "./use-collection-list-query";
 export * from "./use-collection-query";
 export * from "./use-dashboard-query";
 export * from "./use-database-list-query";
diff --git a/frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/index.ts b/frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/index.ts
deleted file mode 100644
index 580bd133ad2..00000000000
--- a/frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./use-collection-list-query";
diff --git a/frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/use-collection-list-query.ts b/frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/use-collection-list-query.ts
deleted file mode 100644
index a221532d14c..00000000000
--- a/frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/use-collection-list-query.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import Collections from "metabase/entities/collections";
-import type { Collection, CollectionListQuery } from "metabase-types/api";
-
-import type {
-  UseEntityListQueryProps,
-  UseEntityListQueryResult,
-} from "../use-entity-list-query";
-import { useEntityListQuery } from "../use-entity-list-query";
-
-/**
- * @deprecated use "metabase/api" instead
- */
-export const useCollectionListQuery = (
-  props: UseEntityListQueryProps<CollectionListQuery> = {},
-): UseEntityListQueryResult<Collection> => {
-  return useEntityListQuery(props, {
-    fetchList: Collections.actions.fetchList,
-    getError: Collections.selectors.getError,
-    getList: Collections.selectors.getList,
-    getLoaded: Collections.selectors.getLoaded,
-    getLoading: Collections.selectors.getLoading,
-    getListMetadata: Collections.selectors.getListMetadata,
-  });
-};
diff --git a/frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/use-collection-list-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/use-collection-list-query.unit.spec.tsx
deleted file mode 100644
index e76543ed72f..00000000000
--- a/frontend/src/metabase/common/hooks/entity-framework/use-collection-list-query/use-collection-list-query.unit.spec.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import {
-  setupCollectionsEndpoints,
-  setupCollectionsWithError,
-} from "__support__/server-mocks";
-import {
-  renderWithProviders,
-  screen,
-  waitForLoaderToBeRemoved,
-  within,
-} from "__support__/ui";
-import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-import { createMockCollection } from "metabase-types/api/mocks";
-
-import { useCollectionListQuery } from "./use-collection-list-query";
-
-const TEST_COLLECTION = createMockCollection();
-
-const TestComponent = () => {
-  const { data = [], metadata, isLoading, error } = useCollectionListQuery();
-
-  if (isLoading || error) {
-    return <LoadingAndErrorWrapper loading={isLoading} error={error} />;
-  }
-
-  return (
-    <div>
-      {data.map(collection => (
-        <div key={collection.id}>{collection.name}</div>
-      ))}
-      <div data-testid="metadata">
-        {(!metadata || Object.keys(metadata).length === 0) && "No metadata"}
-      </div>
-    </div>
-  );
-};
-
-const setup = ({ error }: { error?: string } = {}) => {
-  if (error) {
-    setupCollectionsWithError({ error });
-  } else {
-    setupCollectionsEndpoints({ collections: [TEST_COLLECTION] });
-  }
-
-  renderWithProviders(<TestComponent />);
-};
-
-describe("useCollectionListQuery", () => {
-  it("should be initially loading", () => {
-    setup();
-
-    expect(screen.getByTestId("loading-indicator")).toBeInTheDocument();
-  });
-
-  it("should display error", async () => {
-    const ERROR = "Server error";
-
-    setup({ error: ERROR });
-
-    await waitForLoaderToBeRemoved();
-
-    expect(screen.getByText(ERROR)).toBeInTheDocument();
-  });
-
-  it("should show data from the response", async () => {
-    setup();
-
-    await waitForLoaderToBeRemoved();
-
-    expect(screen.getByText(TEST_COLLECTION.name)).toBeInTheDocument();
-  });
-
-  it("should not have any metadata in the response", async () => {
-    setup();
-
-    await waitForLoaderToBeRemoved();
-
-    expect(
-      within(screen.getByTestId("metadata")).getByText("No metadata"),
-    ).toBeInTheDocument();
-  });
-});
diff --git a/frontend/src/metabase/components/SaveQuestionForm/context.tsx b/frontend/src/metabase/components/SaveQuestionForm/context.tsx
index 67ab261bef1..d99a5e5f9a4 100644
--- a/frontend/src/metabase/components/SaveQuestionForm/context.tsx
+++ b/frontend/src/metabase/components/SaveQuestionForm/context.tsx
@@ -7,7 +7,7 @@ import {
   useState,
 } from "react";
 
-import { useListCollectionsQuery } from "metabase/api";
+import { useGetDefaultCollectionId } from "metabase/collections/hooks";
 import { FormProvider } from "metabase/forms";
 import { useSelector } from "metabase/lib/redux";
 import { getIsSavedQuestionChanged } from "metabase/query_builder/selectors";
@@ -37,21 +37,17 @@ export const SaveQuestionProvider = ({
   onCreate,
   onSave,
   multiStep = false,
-  initialCollectionId,
   children,
 }: PropsWithChildren<SaveQuestionProps>) => {
-  const { data: collections = [] } = useListCollectionsQuery({});
   const [originalQuestion] = useState(latestOriginalQuestion); // originalQuestion from props changes during saving
 
+  const defaultCollectionId = useGetDefaultCollectionId(
+    originalQuestion?.collectionId(),
+  );
+
   const initialValues: FormValues = useMemo(
-    () =>
-      getInitialValues(
-        collections,
-        originalQuestion,
-        question,
-        initialCollectionId,
-      ),
-    [collections, initialCollectionId, originalQuestion, question],
+    () => getInitialValues(originalQuestion, question, defaultCollectionId),
+    [originalQuestion, defaultCollectionId, question],
   );
 
   const handleSubmit = useCallback(
diff --git a/frontend/src/metabase/components/SaveQuestionForm/util.ts b/frontend/src/metabase/components/SaveQuestionForm/util.ts
index aee1aa7f1c9..f059d99960d 100644
--- a/frontend/src/metabase/components/SaveQuestionForm/util.ts
+++ b/frontend/src/metabase/components/SaveQuestionForm/util.ts
@@ -1,13 +1,9 @@
 import { match, P } from "ts-pattern";
 import { t } from "ttag";
 
-import {
-  canonicalCollectionId,
-  getInstanceAnalyticsCustomCollection,
-  isInstanceAnalyticsCollection,
-} from "metabase/collections/utils";
+import { canonicalCollectionId } from "metabase/collections/utils";
 import type Question from "metabase-lib/v1/Question";
-import type { CardType, Collection } from "metabase-types/api";
+import type { CardType } from "metabase-types/api";
 
 import type { FormValues } from "./types";
 
@@ -64,26 +60,12 @@ export async function submitQuestion(
 }
 
 export const getInitialValues = (
-  collections: Collection[],
   originalQuestion: Question | null,
   question: Question,
   initialCollectionId: FormValues["collection_id"],
 ): FormValues => {
   const isReadonly = originalQuestion != null && !originalQuestion.canWrite();
 
-  // we can't use null because that can be ID of the root collection
-  const instanceAnalyticsCollectionId =
-    collections?.find(isInstanceAnalyticsCollection)?.id ?? "not found";
-  const isInInstanceAnalyticsQuestion =
-    originalQuestion?.collectionId() === instanceAnalyticsCollectionId;
-
-  if (collections && isInInstanceAnalyticsQuestion) {
-    const customCollection = getInstanceAnalyticsCustomCollection(collections);
-    if (customCollection) {
-      initialCollectionId = customCollection.id;
-    }
-  }
-
   const getOriginalNameModification = (originalQuestion: Question | null) =>
     originalQuestion
       ? t`${originalQuestion.displayName()} - Modified`
@@ -99,9 +81,7 @@ export const getInitialValues = (
     description:
       originalQuestion?.description() || question.description() || "",
     collection_id:
-      question.collectionId() === undefined ||
-      isReadonly ||
-      isInInstanceAnalyticsQuestion
+      question.collectionId() === undefined || isReadonly
         ? initialCollectionId
         : question.collectionId(),
     saveType:
diff --git a/frontend/src/metabase/containers/SaveQuestionModal/SaveQuestionModal.unit.spec.tsx b/frontend/src/metabase/containers/SaveQuestionModal/SaveQuestionModal.unit.spec.tsx
index 21ba22884a4..a85ac86d62e 100644
--- a/frontend/src/metabase/containers/SaveQuestionModal/SaveQuestionModal.unit.spec.tsx
+++ b/frontend/src/metabase/containers/SaveQuestionModal/SaveQuestionModal.unit.spec.tsx
@@ -1,5 +1,4 @@
 import userEvent from "@testing-library/user-event";
-import fetchMock from "fetch-mock";
 
 import { setupEnterpriseTest } from "__support__/enterprise";
 import { createMockMetadata } from "__support__/metadata";
@@ -77,17 +76,23 @@ const setup = async (
   const onCloseMock = jest.fn();
 
   if (collectionEndpoints) {
+    setupCollectionByIdEndpoint({
+      collections: collectionEndpoints.collections,
+    });
     setupCollectionsEndpoints(collectionEndpoints);
   } else {
-    fetchMock.get("path:/api/collection", TEST_COLLECTIONS);
-    fetchMock.get("path:/api/collection/root", ROOT_TEST_COLLECTION);
     setupCollectionByIdEndpoint({ collections: [BOBBY_TEST_COLLECTION] });
-    setupCollectionItemsEndpoint({
-      collection: BOBBY_TEST_COLLECTION,
-      collectionItems: [],
+    setupCollectionsEndpoints({
+      collections: TEST_COLLECTIONS,
+      rootCollection: ROOT_TEST_COLLECTION,
     });
   }
 
+  setupCollectionItemsEndpoint({
+    collection: BOBBY_TEST_COLLECTION,
+    collectionItems: [],
+  });
+
   setupRecentViewsAndSelectionsEndpoints([]);
 
   const settings = mockSettings();
@@ -253,7 +258,7 @@ describe("SaveQuestionModal", () => {
       expect(newQuestion.id()).toBeUndefined();
       expect(newQuestion.displayName()).toBe(EXPECTED_SUGGESTED_NAME);
       expect(newQuestion.description()).toBe(null);
-      expect(newQuestion.collectionId()).toBe(null);
+      expect(newQuestion.collectionId()).toBe(1);
     });
 
     it("should call onCreate correctly with edited form", async () => {
@@ -277,7 +282,7 @@ describe("SaveQuestionModal", () => {
       expect(newQuestion.id()).toBeUndefined();
       expect(newQuestion.displayName()).toBe("My favorite orders");
       expect(newQuestion.description()).toBe("So many of them");
-      expect(newQuestion.collectionId()).toBe(null);
+      expect(newQuestion.collectionId()).toBe(1);
     });
 
     it("should trim name and description", async () => {
@@ -301,7 +306,7 @@ describe("SaveQuestionModal", () => {
       expect(newQuestion.id()).toBeUndefined();
       expect(newQuestion.displayName()).toBe("My favorite orders");
       expect(newQuestion.description()).toBe("So many of them");
-      expect(newQuestion.collectionId()).toBe(null);
+      expect(newQuestion.collectionId()).toBe(1);
     });
 
     it('should correctly handle saving a question in the "root" collection', async () => {
@@ -324,7 +329,7 @@ describe("SaveQuestionModal", () => {
       expect(newQuestion.id()).toBeUndefined();
       expect(newQuestion.displayName()).toBe("foo");
       expect(newQuestion.description()).toBe("bar");
-      expect(newQuestion.collectionId()).toBe(null);
+      expect(newQuestion.collectionId()).toBe(1);
     });
 
     it("shouldn't call onSave when form is submitted", async () => {
@@ -723,6 +728,7 @@ describe("SaveQuestionModal", () => {
     const cancelBtn = () => screen.getByRole("button", { name: /cancel/i });
 
     const COLLECTION = {
+      USER: BOBBY_TEST_COLLECTION,
       ROOT: createMockCollection({
         ...ROOT_COLLECTION,
         can_write: true,
@@ -797,18 +803,13 @@ describe("SaveQuestionModal", () => {
             rootCollection: COLLECTION.ROOT,
           },
         });
-        setupCollectionByIdEndpoint({ collections: [COLLECTION.PARENT] });
-        setupCollectionItemsEndpoint({
-          collection: BOBBY_TEST_COLLECTION,
-          collectionItems: [],
-        });
       });
       it("should create collection inside nested folder", async () => {
         await userEvent.click(collDropdown());
         await waitFor(() => expect(newCollBtn()).toBeInTheDocument());
         await userEvent.click(
           await screen.findByRole("button", {
-            name: new RegExp(COLLECTION.PARENT.name),
+            name: new RegExp(BOBBY_TEST_COLLECTION.name),
           }),
         );
         await userEvent.click(newCollBtn());
diff --git a/frontend/src/metabase/entities/collections/getInitialCollectionId.ts b/frontend/src/metabase/entities/collections/getInitialCollectionId.ts
index e87a25574ba..f5a3db4390f 100644
--- a/frontend/src/metabase/entities/collections/getInitialCollectionId.ts
+++ b/frontend/src/metabase/entities/collections/getInitialCollectionId.ts
@@ -51,7 +51,7 @@ const getInitialCollectionId = createSelector(
     byCollectionIdProp,
     byCollectionIdNavParam,
     byCollectionUrlId,
-    byCollectionQueryParameter,
+    byCollectionQueryParameter, // used by new model flow
   ],
   (collections, personalCollectionId, ...collectionIds) => {
     const rootCollectionId = ROOT_COLLECTION.id as CollectionId;
diff --git a/frontend/src/metabase/entities/containers/EntityCopyModal.tsx b/frontend/src/metabase/entities/containers/EntityCopyModal.tsx
index 6bde838f143..8f0aedc06a6 100644
--- a/frontend/src/metabase/entities/containers/EntityCopyModal.tsx
+++ b/frontend/src/metabase/entities/containers/EntityCopyModal.tsx
@@ -1,15 +1,10 @@
 import { dissoc } from "icepick";
 import { t } from "ttag";
 
-import {
-  getInstanceAnalyticsCustomCollection,
-  isInstanceAnalyticsCollection,
-} from "metabase/collections/utils";
-import { useCollectionListQuery } from "metabase/common/hooks";
+import { useGetDefaultCollectionId } from "metabase/collections/hooks";
 import ModalContent from "metabase/components/ModalContent";
 import { CopyDashboardFormConnected } from "metabase/dashboard/containers/CopyDashboardForm";
 import { CopyQuestionForm } from "metabase/questions/components/CopyQuestionForm";
-import { Flex, Loader } from "metabase/ui";
 
 interface EntityCopyModalProps {
   entityType: string;
@@ -17,7 +12,7 @@ interface EntityCopyModalProps {
   copy: (data: any) => void;
   title?: string;
   onClose: () => void;
-  onSaved: (newEntityObject: any) => void;
+  onSaved: (newEntityObject?: any) => void;
   overwriteOnInitialValuesChange?: boolean;
   onValuesChange?: (values: Record<string, unknown>) => void;
   form?: any;
@@ -32,18 +27,17 @@ const EntityCopyModal = ({
   onSaved,
   ...props
 }: EntityCopyModalProps) => {
-  const { data: collections = [] } = useCollectionListQuery();
-
   const resolvedObject =
     typeof entityObject?.getPlainObject === "function"
       ? entityObject.getPlainObject()
       : entityObject;
 
-  if (isInstanceAnalyticsCollection(resolvedObject?.collection)) {
-    const customCollection = getInstanceAnalyticsCustomCollection(collections);
-    if (customCollection) {
-      resolvedObject.collection_id = customCollection.id;
-    }
+  const defaultCollectionId = useGetDefaultCollectionId(
+    resolvedObject?.collection?.id,
+  );
+
+  if (defaultCollectionId) {
+    resolvedObject.collection_id = defaultCollectionId;
   }
 
   const initialValues = {
@@ -51,42 +45,28 @@ const EntityCopyModal = ({
     name: resolvedObject.name + " - " + t`Duplicate`,
   };
 
-  const renderForm = (props: any) => {
-    switch (entityType) {
-      case "dashboards":
-        return (
-          <CopyDashboardFormConnected
-            onSubmit={copy}
-            onClose={onClose}
-            onSaved={onSaved}
-            collections={collections}
-            {...props}
-          />
-        );
-      case "questions":
-        return (
-          <CopyQuestionForm
-            onSubmit={copy}
-            onClose={onClose}
-            onSaved={onSaved}
-            collections={collections}
-            {...props}
-          />
-        );
-    }
-  };
-
   return (
     <ModalContent
       title={title || t`Duplicate "${resolvedObject.name}"`}
       onClose={onClose}
     >
-      {!collections?.length ? (
-        <Flex justify="center" p="lg">
-          <Loader />
-        </Flex>
-      ) : (
-        renderForm({ ...props, initialValues })
+      {entityType === "dashboards" && (
+        <CopyDashboardFormConnected
+          onSubmit={copy}
+          onClose={onClose}
+          onSaved={onSaved}
+          initialValues={initialValues}
+          {...props}
+        />
+      )}
+      {entityType === "questions" && (
+        <CopyQuestionForm
+          onSubmit={copy}
+          onCancel={onClose}
+          onSaved={onSaved}
+          initialValues={initialValues}
+          {...props}
+        />
       )}
     </ModalContent>
   );
diff --git a/frontend/src/metabase/nav/containers/MainNavbar/MainNavbarContainer/MainNavbarContainer.tsx b/frontend/src/metabase/nav/containers/MainNavbar/MainNavbarContainer/MainNavbarContainer.tsx
index 5826b02857c..215f236fd67 100644
--- a/frontend/src/metabase/nav/containers/MainNavbar/MainNavbarContainer/MainNavbarContainer.tsx
+++ b/frontend/src/metabase/nav/containers/MainNavbar/MainNavbarContainer/MainNavbarContainer.tsx
@@ -3,7 +3,10 @@ import { useCallback, useMemo, useState, memo } from "react";
 import { connect } from "react-redux";
 import _ from "underscore";
 
-import { useGetCollectionQuery } from "metabase/api";
+import {
+  useGetCollectionQuery,
+  useListCollectionsTreeQuery,
+} from "metabase/api";
 import { logout } from "metabase/auth/actions";
 import CreateCollectionModal from "metabase/collections/containers/CreateCollectionModal";
 import {
@@ -54,7 +57,6 @@ interface Props extends MainNavbarProps {
   currentUser: User;
   selectedItems: SelectedItem[];
   bookmarks: Bookmark[];
-  collections: Collection[];
   rootCollection: Collection;
   hasDataAccess: boolean;
   hasOwnDatabase: boolean;
@@ -76,7 +78,6 @@ function MainNavbarContainer({
   isOpen,
   currentUser,
   hasOwnDatabase,
-  collections = [],
   rootCollection,
   hasDataAccess,
   location,
@@ -96,6 +97,11 @@ function MainNavbarContainer({
     error,
   } = useGetCollectionQuery({ id: "trash" });
 
+  const { data: collections = [] } = useListCollectionsTreeQuery({
+    "exclude-other-user-collections": true,
+    "exclude-archived": true,
+  });
+
   const collectionTree = useMemo<CollectionTreeItem[]>(() => {
     const preparedCollections = [];
     const userPersonalCollections = currentUserPersonalCollections(
@@ -209,14 +215,6 @@ export default _.compose(
     entityAlias: "rootCollection",
     loadingAndErrorWrapper: false,
   }),
-  Collections.loadList({
-    query: () => ({
-      tree: true,
-      "exclude-other-user-collections": true,
-      "exclude-archived": true,
-    }),
-    loadingAndErrorWrapper: false,
-  }),
   Databases.loadList({
     loadingAndErrorWrapper: false,
   }),
diff --git a/frontend/src/metabase/plugins/index.ts b/frontend/src/metabase/plugins/index.ts
index a1828a85740..0d8a51990e9 100644
--- a/frontend/src/metabase/plugins/index.ts
+++ b/frontend/src/metabase/plugins/index.ts
@@ -49,6 +49,7 @@ import type {
   SearchResult,
   User,
   UserListResult,
+  CollectionId,
 } from "metabase-types/api";
 import type { AdminPathKey, State } from "metabase-types/store";
 
@@ -265,6 +266,10 @@ type CleanUpMenuItem = {
 
 export type ItemWithCollection = { collection: CollectionEssentials };
 
+type GetCollectionIdType = (
+  sourceCollectionId?: CollectionId | null,
+) => CollectionId | null;
+
 export const PLUGIN_COLLECTIONS = {
   AUTHORITY_LEVEL: {
     [JSON.stringify(AUTHORITY_LEVEL_REGULAR.type)]: AUTHORITY_LEVEL_REGULAR,
@@ -278,9 +283,7 @@ export const PLUGIN_COLLECTIONS = {
     _: Partial<Collection>,
   ): CollectionAuthorityLevelConfig | CollectionInstanceAnaltyicsConfig =>
     AUTHORITY_LEVEL_REGULAR,
-  getInstanceAnalyticsCustomCollection: (
-    _collections: Collection[],
-  ): Collection | null => null,
+  useGetDefaultCollectionId: null as GetCollectionIdType | null,
   CUSTOM_INSTANCE_ANALYTICS_COLLECTION_ENTITY_ID: "",
   INSTANCE_ANALYTICS_ADMIN_READONLY_MESSAGE: UNABLE_TO_CHANGE_ADMIN_PERMISSIONS,
   getAuthorityLevelMenuItems: (
diff --git a/frontend/src/metabase/query_builder/components/QueryModals/QueryModals.tsx b/frontend/src/metabase/query_builder/components/QueryModals/QueryModals.tsx
index 119e7e4adf1..93dd4a646f0 100644
--- a/frontend/src/metabase/query_builder/components/QueryModals/QueryModals.tsx
+++ b/frontend/src/metabase/query_builder/components/QueryModals/QueryModals.tsx
@@ -2,13 +2,13 @@ import { useCallback } from "react";
 import { t } from "ttag";
 import _ from "underscore";
 
+import { useGetDefaultCollectionId } from "metabase/collections/hooks";
 import Modal from "metabase/components/Modal";
 import QuestionSavedModal from "metabase/components/QuestionSavedModal";
 import { AddToDashSelectDashModal } from "metabase/containers/AddToDashSelectDashModal";
 import { MoveModal } from "metabase/containers/MoveModal";
 import { SaveQuestionModal } from "metabase/containers/SaveQuestionModal";
 import { ROOT_COLLECTION } from "metabase/entities/collections/constants";
-import getInitialCollectionId from "metabase/entities/collections/getInitialCollectionId";
 import EntityCopyModal from "metabase/entities/containers/EntityCopyModal";
 import Questions from "metabase/entities/questions";
 import { useDispatch, useSelector } from "metabase/lib/redux";
@@ -74,9 +74,7 @@ export function QueryModals({
 }: QueryModalsProps) {
   const dispatch = useDispatch();
 
-  const initialCollectionId = useSelector(state =>
-    getInitialCollectionId(state, {}),
-  );
+  const initialCollectionId = useGetDefaultCollectionId();
   const questionWithParameters = useSelector(getQuestionWithParameters);
 
   const showAlertsAfterQuestionSaved = useCallback(() => {
diff --git a/frontend/src/metabase/questions/components/CopyQuestionForm.tsx b/frontend/src/metabase/questions/components/CopyQuestionForm.tsx
index 12b0d33820b..673a97362a4 100644
--- a/frontend/src/metabase/questions/components/CopyQuestionForm.tsx
+++ b/frontend/src/metabase/questions/components/CopyQuestionForm.tsx
@@ -62,6 +62,7 @@ export const CopyQuestionForm = ({
       initialValues={computedInitialValues}
       validationSchema={QUESTION_SCHEMA}
       onSubmit={handleDuplicate}
+      enableReinitialize
     >
       <Form>
         <FormTextInput
-- 
GitLab