From 4a717723ae7f5778b9e30e470f28e5806f301994 Mon Sep 17 00:00:00 2001
From: Dalton <daltojohnso@users.noreply.github.com>
Date: Tue, 30 Aug 2022 13:35:23 -0400
Subject: [PATCH] Use the `Questions` entity to fetch cards/questions for the
 query builder (#25102)

* Make QB use Questions entity to fetch

* Pass down 'getState' to avoid HACK function

* Fix tests
---
 .../containers/SavedQuestionLoader.jsx        | 15 +++++---
 frontend/src/metabase/lib/card.js             | 10 ++++--
 .../query_builder/actions/core/core.js        |  6 ++--
 .../actions/core/initializeQB.ts              | 34 +++++++++++++++----
 .../actions/core/initializeQB.unit.spec.ts    |  6 ++++
 .../SavedQuestionLoader.unit.spec.js          |  4 +--
 6 files changed, 57 insertions(+), 18 deletions(-)

diff --git a/frontend/src/metabase/containers/SavedQuestionLoader.jsx b/frontend/src/metabase/containers/SavedQuestionLoader.jsx
index 4ec739c4d12..3fcd9e813f1 100644
--- a/frontend/src/metabase/containers/SavedQuestionLoader.jsx
+++ b/frontend/src/metabase/containers/SavedQuestionLoader.jsx
@@ -2,12 +2,11 @@
 import React from "react";
 import { connect } from "react-redux";
 
-// things that will eventually load the quetsion
-import { CardApi } from "metabase/services";
 import { loadMetadataForCard } from "metabase/query_builder/actions";
 import { getMetadata } from "metabase/selectors/metadata";
 
 import Question from "metabase-lib/lib/Question";
+import Questions from "metabase/entities/questions";
 
 // type annotations
 
@@ -91,7 +90,7 @@ export class SavedQuestionLoader extends React.Component {
     try {
       this.setState({ loading: true, error: null });
       // get the saved question via the card API
-      const card = await CardApi.get({ cardId: questionId });
+      const card = await this.props.fetchQuestion(questionId);
 
       // pass the retrieved card to load any necessary metadata
       // (tables, source db, segments, etc) into
@@ -129,8 +128,14 @@ function mapStateToProps(state) {
   };
 }
 
-const mapDispatchToProps = {
-  loadMetadataForCard,
+const mapDispatchToProps = dispatch => {
+  return {
+    loadMetadataForCard: card => dispatch(loadMetadataForCard(card)),
+    fetchQuestion: async id => {
+      const action = await dispatch(Questions.actions.fetch({ id }));
+      return Questions.HACK_getObjectFromAction(action);
+    },
+  };
 };
 
 export default connect(
diff --git a/frontend/src/metabase/lib/card.js b/frontend/src/metabase/lib/card.js
index 7af2667ef8e..469cd855782 100644
--- a/frontend/src/metabase/lib/card.js
+++ b/frontend/src/metabase/lib/card.js
@@ -2,8 +2,8 @@ import _ from "underscore";
 import * as Q_DEPRECATED from "metabase/lib/query";
 import Utils from "metabase/lib/utils";
 
-import { CardApi } from "metabase/services";
 import { b64hash_to_utf8, utf8_to_b64url } from "metabase/lib/encoding";
+import Questions from "metabase/entities/questions";
 
 export function createCard(name = null) {
   return {
@@ -25,9 +25,13 @@ export function startNewCard(type, databaseId, tableId) {
 
 // load a card either by ID or from a base64 serialization.  if both are present then they are merged, which the serialized version taking precedence
 // TODO: move to redux
-export async function loadCard(cardId) {
+export async function loadCard(cardId, { dispatch, getState }) {
   try {
-    return await CardApi.get({ cardId: cardId });
+    await dispatch(Questions.actions.fetch({ id: cardId }, { reload: true }));
+    const card = Questions.selectors.getObject(getState(), {
+      entityId: cardId,
+    });
+    return card;
   } catch (error) {
     console.log("error loading card", error);
     throw error;
diff --git a/frontend/src/metabase/query_builder/actions/core/core.js b/frontend/src/metabase/query_builder/actions/core/core.js
index e2b7e1eda9f..9bcdbb5e0bd 100644
--- a/frontend/src/metabase/query_builder/actions/core/core.js
+++ b/frontend/src/metabase/query_builder/actions/core/core.js
@@ -96,7 +96,7 @@ export const setCardAndRun = (nextCard, shouldUpdateUrl = true) => {
 
     const originalCard = card.original_card_id
       ? // If the original card id is present, dynamically load its information for showing lineage
-        await loadCard(card.original_card_id)
+        await loadCard(card.original_card_id, { dispatch, getState })
       : // Otherwise, use a current card as the original card if the card has been saved
       // This is needed for checking whether the card is in dirty state or not
       card.id
@@ -131,7 +131,9 @@ export const navigateToNewCardInsideQB = createThunkAction(
         // Do not reload questions with breakouts when clicked on a legend item
       } else if (cardIsEquivalent(previousCard, nextCard)) {
         // This is mainly a fallback for scenarios where a visualization legend is clicked inside QB
-        dispatch(setCardAndRun(await loadCard(nextCard.id)));
+        dispatch(
+          setCardAndRun(await loadCard(nextCard.id, { dispatch, getState })),
+        );
       } else {
         const card = getCardAfterVisualizationClick(nextCard, previousCard);
         const url = Urls.serializedQuestion(card);
diff --git a/frontend/src/metabase/query_builder/actions/core/initializeQB.ts b/frontend/src/metabase/query_builder/actions/core/initializeQB.ts
index 0085108ace3..477e10f6d9b 100644
--- a/frontend/src/metabase/query_builder/actions/core/initializeQB.ts
+++ b/frontend/src/metabase/query_builder/actions/core/initializeQB.ts
@@ -14,6 +14,7 @@ import { getMetadata } from "metabase/selectors/metadata";
 import { getUser } from "metabase/selectors/user";
 
 import Snippets from "metabase/entities/snippets";
+import Questions from "metabase/entities/questions";
 import { fetchAlertsForQuestion } from "metabase/alert/alert";
 
 import Question from "metabase-lib/lib/Question";
@@ -103,8 +104,12 @@ function deserializeCard(serializedCard: string) {
   return card;
 }
 
-async function fetchAndPrepareSavedQuestionCards(cardId: number) {
-  const card = await loadCard(cardId);
+async function fetchAndPrepareSavedQuestionCards(
+  cardId: number,
+  dispatch: Dispatch,
+  getState: GetState,
+) {
+  const card = await loadCard(cardId, { dispatch, getState });
   const originalCard = { ...card };
 
   // for showing the "started from" lineage correctly when adding filters/breakouts and when going back and forth
@@ -114,7 +119,11 @@ async function fetchAndPrepareSavedQuestionCards(cardId: number) {
   return { card, originalCard };
 }
 
-async function fetchAndPrepareAdHocQuestionCards(deserializedCard: Card) {
+async function fetchAndPrepareAdHocQuestionCards(
+  deserializedCard: Card,
+  dispatch: Dispatch,
+  getState: GetState,
+) {
   if (!deserializedCard.original_card_id) {
     return {
       card: deserializedCard,
@@ -122,7 +131,10 @@ async function fetchAndPrepareAdHocQuestionCards(deserializedCard: Card) {
     };
   }
 
-  const originalCard = await loadCard(deserializedCard.original_card_id);
+  const originalCard = await loadCard(deserializedCard.original_card_id, {
+    dispatch,
+    getState,
+  });
 
   if (cardIsEquivalent(deserializedCard, originalCard)) {
     return {
@@ -146,10 +158,14 @@ async function resolveCards({
   cardId,
   deserializedCard,
   options,
+  dispatch,
+  getState,
 }: {
   cardId?: number;
   deserializedCard?: Card;
   options: BlankQueryOptions;
+  dispatch: Dispatch;
+  getState: GetState;
 }): Promise<ResolveCardsResult> {
   if (!cardId && !deserializedCard) {
     return {
@@ -157,8 +173,12 @@ async function resolveCards({
     };
   }
   return cardId
-    ? fetchAndPrepareSavedQuestionCards(cardId)
-    : fetchAndPrepareAdHocQuestionCards(deserializedCard as Card);
+    ? fetchAndPrepareSavedQuestionCards(cardId, dispatch, getState)
+    : fetchAndPrepareAdHocQuestionCards(
+        deserializedCard as Card,
+        dispatch,
+        getState,
+      );
 }
 
 function parseHash(hash?: string) {
@@ -209,6 +229,8 @@ async function handleQBInit(
     cardId,
     deserializedCard,
     options,
+    dispatch,
+    getState,
   });
 
   if (isSavedCard(card) && card.archived) {
diff --git a/frontend/src/metabase/query_builder/actions/core/initializeQB.unit.spec.ts b/frontend/src/metabase/query_builder/actions/core/initializeQB.unit.spec.ts
index e6a7f460c66..6a933ae58b6 100644
--- a/frontend/src/metabase/query_builder/actions/core/initializeQB.unit.spec.ts
+++ b/frontend/src/metabase/query_builder/actions/core/initializeQB.unit.spec.ts
@@ -123,6 +123,8 @@ async function setup({
     });
   }
 
+  jest.spyOn(CardLib, "loadCard").mockReturnValue(Promise.resolve({ ...card }));
+
   return baseSetup({ location, params, ...opts });
 }
 
@@ -438,6 +440,10 @@ describe("QB Actions > initializeQB", () => {
           body: JSON.stringify(originalQuestion.card()),
         });
 
+        jest
+          .spyOn(CardLib, "loadCard")
+          .mockReturnValueOnce(Promise.resolve({ ...originalQuestion.card() }));
+
         return setup({ question: q, ...opts });
       }
 
diff --git a/frontend/test/metabase/containers/SavedQuestionLoader.unit.spec.js b/frontend/test/metabase/containers/SavedQuestionLoader.unit.spec.js
index f41654189f9..2fed8f722fb 100644
--- a/frontend/test/metabase/containers/SavedQuestionLoader.unit.spec.js
+++ b/frontend/test/metabase/containers/SavedQuestionLoader.unit.spec.js
@@ -3,7 +3,6 @@ import { render } from "@testing-library/react";
 
 import Question from "metabase-lib/lib/Question";
 import { delay } from "metabase/lib/promise";
-import { CardApi } from "metabase/services";
 
 // import the un-connected component so we can test its internal logic sans
 // redux
@@ -25,12 +24,13 @@ describe("SavedQuestionLoader", () => {
   it("should load a question given a questionId", async () => {
     const questionId = 1;
     const q = Question.create({ databaseId: 1, tableId: 2 });
-    jest.spyOn(CardApi, "get").mockReturnValue(q.card());
+    const mockFetchQuestion = jest.fn().mockResolvedValue(q.card());
 
     render(
       <SavedQuestionLoader
         questionId={questionId}
         loadMetadataForCard={loadMetadataSpy}
+        fetchQuestion={mockFetchQuestion}
       >
         {mockChild}
       </SavedQuestionLoader>,
-- 
GitLab