diff --git a/frontend/src/metabase/containers/SavedQuestionLoader.jsx b/frontend/src/metabase/containers/SavedQuestionLoader.jsx index 4ec739c4d12739f34814023a36d368de1046a859..3fcd9e813f1517048cdf2e64a12f487a9109e167 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 7af2667ef8eb60cfc5bdb8f6f48933a94b72dab0..469cd855782efda983329e1c0102aa59629a5b97 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 e2b7e1eda9f1e63c38b1d13cedcfe453be65ca7a..9bcdbb5e0bde12af87ae1a04f38a9294665f54aa 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 0085108ace3a139c3fe658aca98dd9966c88c63b..477e10f6d9b8e7d54aa983ddd530519d65215a1f 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 e6a7f460c666ea49d5b924358b4d3e4a7ea21f82..6a933ae58b6b2a673f8327d6bf36eda6a2d9d5bc 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 f41654189f93c6ed65fdadd402de9fd78cb1e466..2fed8f722fbfef87da7749280da4337070c9004f 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>,