From e99c67a1532b34f0396c5e9f4a7cc30c1c731858 Mon Sep 17 00:00:00 2001
From: Phoomparin Mano <poom@metabase.com>
Date: Fri, 25 Oct 2024 03:49:38 +0700
Subject: [PATCH] fix(sdk): ability to save questions in interactive question
 (#48866)

* implement saving questions in interactive question

* close modal on create question

* add e2e test for saving questions
---
 .../interactive-question.cy.spec.ts           | 26 ++++++++++++++++
 .../InteractiveQuestionResult.tsx             | 31 +++++++++++++++++--
 .../InteractiveQuestion.stories.tsx           |  1 +
 .../SaveQuestionModal/SaveQuestionModal.tsx   |  8 ++++-
 4 files changed, 63 insertions(+), 3 deletions(-)

diff --git a/e2e/test/scenarios/embedding-sdk/interactive-question.cy.spec.ts b/e2e/test/scenarios/embedding-sdk/interactive-question.cy.spec.ts
index 21590930a2d..0d81769206e 100644
--- a/e2e/test/scenarios/embedding-sdk/interactive-question.cy.spec.ts
+++ b/e2e/test/scenarios/embedding-sdk/interactive-question.cy.spec.ts
@@ -1,6 +1,7 @@
 import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
 import {
   createQuestion,
+  modal,
   popover,
   restore,
   setTokenFeatures,
@@ -98,4 +99,29 @@ describeSDK("scenarios > embedding-sdk > interactive-question", () => {
 
     cy.icon("warning").should("not.exist");
   });
+
+  it("can save a question", () => {
+    cy.intercept("POST", "/api/card").as("createCard");
+
+    cy.findAllByTestId("cell-data").last().click();
+
+    popover().findByText("See these Orders").click();
+
+    cy.findByRole("button", { name: "Save" }).click();
+
+    modal().within(() => {
+      cy.findByRole("radiogroup").findByText("Save as new question").click();
+
+      cy.findByPlaceholderText("What is the name of your question?")
+        .clear()
+        .type("Foo Bar Orders");
+
+      cy.findByRole("button", { name: "Save" }).click();
+    });
+
+    cy.wait("@createCard").then(({ response }) => {
+      expect(response?.statusCode).to.equal(200);
+      expect(response?.body.name).to.equal("Foo Bar Orders");
+    });
+  });
 });
diff --git a/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestionResult/InteractiveQuestionResult.tsx b/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestionResult/InteractiveQuestionResult.tsx
index da87f63870e..1d41e25b804 100644
--- a/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestionResult/InteractiveQuestionResult.tsx
+++ b/enterprise/frontend/src/embedding-sdk/components/private/InteractiveQuestionResult/InteractiveQuestionResult.tsx
@@ -8,6 +8,7 @@ import {
   SdkError,
   SdkLoader,
 } from "embedding-sdk/components/private/PublicComponentWrapper";
+import { SaveQuestionModal } from "metabase/containers/SaveQuestionModal";
 import { Box, Button, Group, Icon } from "metabase/ui";
 
 import { InteractiveQuestion } from "../../public/InteractiveQuestion";
@@ -58,12 +59,22 @@ export const InteractiveQuestionResult = ({
   const [questionView, setQuestionView] =
     useState<QuestionView>("visualization");
 
-  const { question, queryResults, isQuestionLoading } =
-    useInteractiveQuestionContext();
+  const {
+    question,
+    queryResults,
+    isQuestionLoading,
+    isSaveEnabled,
+    originalQuestion,
+    onCreate,
+    onSave,
+  } = useInteractiveQuestionContext();
 
   const [isChartSelectorOpen, { toggle: toggleChartTypeSelector }] =
     useDisclosure(false);
 
+  const [isSaveModalOpen, { open: openSaveModal, close: closeSaveModal }] =
+    useDisclosure(false);
+
   if (isQuestionLoading) {
     return <SdkLoader />;
   }
@@ -104,6 +115,9 @@ export const InteractiveQuestionResult = ({
               )
             }
           />
+          {isSaveEnabled && !isSaveModalOpen && (
+            <InteractiveQuestion.SaveButton onClick={openSaveModal} />
+          )}
         </Group>
       </Group>
 
@@ -147,6 +161,19 @@ export const InteractiveQuestionResult = ({
           />
         </Box>
       </Box>
+
+      {/* Refer to the SaveQuestionProvider for context on why we have to do it like this */}
+      {isSaveModalOpen && question && (
+        <SaveQuestionModal
+          question={question}
+          originalQuestion={originalQuestion ?? null}
+          opened
+          closeOnSuccess
+          onClose={closeSaveModal}
+          onCreate={onCreate}
+          onSave={onSave}
+        />
+      )}
     </FlexibleSizeComponent>
   );
 };
diff --git a/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion/InteractiveQuestion.stories.tsx b/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion/InteractiveQuestion.stories.tsx
index a9c05f1ea0e..fd466954d9d 100644
--- a/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion/InteractiveQuestion.stories.tsx
+++ b/enterprise/frontend/src/embedding-sdk/components/public/InteractiveQuestion/InteractiveQuestion.stories.tsx
@@ -29,5 +29,6 @@ export const Default = {
 
   args: {
     questionId: QUESTION_ID,
+    isSaveEnabled: true,
   },
 };
diff --git a/frontend/src/metabase/containers/SaveQuestionModal/SaveQuestionModal.tsx b/frontend/src/metabase/containers/SaveQuestionModal/SaveQuestionModal.tsx
index 56ad5a0e72f..b7ce3a25273 100644
--- a/frontend/src/metabase/containers/SaveQuestionModal/SaveQuestionModal.tsx
+++ b/frontend/src/metabase/containers/SaveQuestionModal/SaveQuestionModal.tsx
@@ -20,7 +20,13 @@ export const SaveQuestionModal = ({
   <SaveQuestionProvider
     question={question}
     originalQuestion={originalQuestion}
-    onCreate={onCreate}
+    onCreate={async question => {
+      await onCreate(question);
+
+      if (closeOnSuccess) {
+        modalProps.onClose();
+      }
+    }}
     onSave={onSave}
     multiStep={multiStep}
     initialCollectionId={initialCollectionId}
-- 
GitLab