From 4c8fa7782feb014775af82d3a49db742e5d8fbe4 Mon Sep 17 00:00:00 2001
From: "Mahatthana (Kelvin) Nomsawadi" <mahatthana.n@gmail.com>
Date: Mon, 11 Jul 2022 17:33:22 +0700
Subject: [PATCH] Add toast after moving questions and models (#23756)

* Add toast message after moving questions or models

* Add test

* Address feedback: extract component into its own file

* Remove undo action on question move because it doesn't work in all cases

* Fix failed test

nodata user can't turn a question into a model

* Remove commented code that could confuses more people
---
 frontend/src/metabase/entities/questions.js   |  7 ++
 .../SidebarItems/SidebarCollectionLink.tsx    |  1 +
 .../query_builder/components/QueryModals.jsx  | 33 +++++--
 .../QuestionMoveToast.styled.tsx              | 13 +++
 .../QuestionMoveToast/QuestionMoveToast.tsx   | 40 ++++++++
 .../components/QuestionMoveToast/index.ts     |  1 +
 .../question/question-management.cy.spec.js   | 98 ++++++++++++++++---
 7 files changed, 176 insertions(+), 17 deletions(-)
 create mode 100644 frontend/src/metabase/query_builder/components/QuestionMoveToast/QuestionMoveToast.styled.tsx
 create mode 100644 frontend/src/metabase/query_builder/components/QuestionMoveToast/QuestionMoveToast.tsx
 create mode 100644 frontend/src/metabase/query_builder/components/QuestionMoveToast/index.ts

diff --git a/frontend/src/metabase/entities/questions.js b/frontend/src/metabase/entities/questions.js
index f85544d3183..65b108e0f82 100644
--- a/frontend/src/metabase/entities/questions.js
+++ b/frontend/src/metabase/entities/questions.js
@@ -2,6 +2,8 @@ import { createEntity, undo } from "metabase/lib/entities";
 import * as Urls from "metabase/lib/urls";
 import { color } from "metabase/lib/colors";
 
+import { API_UPDATE_QUESTION } from "metabase/query_builder/actions";
+
 import Collections, {
   getCollectionType,
   normalizedCollection,
@@ -42,6 +44,11 @@ const Questions = createEntity({
           Collections.actions.fetchList({ tree: true }, { reload: true }),
         );
 
+        const card = result?.payload?.question;
+        if (card) {
+          dispatch.action(API_UPDATE_QUESTION, card);
+        }
+
         return result;
       };
     },
diff --git a/frontend/src/metabase/nav/containers/MainNavbar/SidebarItems/SidebarCollectionLink.tsx b/frontend/src/metabase/nav/containers/MainNavbar/SidebarItems/SidebarCollectionLink.tsx
index b37f3298058..bdb2d1ea752 100644
--- a/frontend/src/metabase/nav/containers/MainNavbar/SidebarItems/SidebarCollectionLink.tsx
+++ b/frontend/src/metabase/nav/containers/MainNavbar/SidebarItems/SidebarCollectionLink.tsx
@@ -92,6 +92,7 @@ const SidebarCollectionLink = React.forwardRef<HTMLLIElement, Props>(
       <CollectionNodeRoot
         role="treeitem"
         depth={depth}
+        aria-selected={isSelected}
         isSelected={isSelected}
         hovered={isHovered}
         onClick={onToggleExpand}
diff --git a/frontend/src/metabase/query_builder/components/QueryModals.jsx b/frontend/src/metabase/query_builder/components/QueryModals.jsx
index 07051a2e737..212f24d0be5 100644
--- a/frontend/src/metabase/query_builder/components/QueryModals.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryModals.jsx
@@ -1,9 +1,13 @@
 /* eslint-disable react/prop-types */
 import React from "react";
+import { connect } from "react-redux";
 
 import { t } from "ttag";
 import _ from "underscore";
 
+import Questions from "metabase/entities/questions";
+import { ROOT_COLLECTION } from "metabase/entities/collections";
+
 import { MODAL_TYPES } from "metabase/query_builder/constants";
 
 import Modal from "metabase/components/Modal";
@@ -25,8 +29,13 @@ import BulkFilterModal from "metabase/query_builder/components/filters/modals/Bu
 import NewEventModal from "metabase/timelines/questions/containers/NewEventModal";
 import EditEventModal from "metabase/timelines/questions/containers/EditEventModal";
 import MoveEventModal from "metabase/timelines/questions/containers/MoveEventModal";
+import QuestionMoveToast from "./QuestionMoveToast";
+
+const mapDispatchToProps = {
+  setQuestionCollection: Questions.actions.setCollection,
+};
 
-export default class QueryModals extends React.Component {
+class QueryModals extends React.Component {
   showAlertsAfterQuestionSaved = () => {
     const { questionAlerts, user, onCloseModal, onOpenModal } = this.props;
 
@@ -171,11 +180,21 @@ export default class QueryModals extends React.Component {
           initialCollectionId={question.collectionId()}
           onClose={onCloseModal}
           onMove={collection => {
-            const card = question
-              .setCollectionId(collection && collection.id)
-              .card();
-
-            this.props.onSave(card);
+            this.props.setQuestionCollection(
+              { id: question.id() },
+              collection,
+              {
+                notify: {
+                  message: (
+                    <QuestionMoveToast
+                      isModel={question.isDataset()}
+                      collectionId={collection.id || ROOT_COLLECTION.id}
+                    />
+                  ),
+                  undo: false,
+                },
+              },
+            );
             onCloseModal();
           }}
         />
@@ -237,3 +256,5 @@ export default class QueryModals extends React.Component {
     ) : null;
   }
 }
+
+export default connect(null, mapDispatchToProps)(QueryModals);
diff --git a/frontend/src/metabase/query_builder/components/QuestionMoveToast/QuestionMoveToast.styled.tsx b/frontend/src/metabase/query_builder/components/QuestionMoveToast/QuestionMoveToast.styled.tsx
new file mode 100644
index 00000000000..dd547f8d0a9
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/QuestionMoveToast/QuestionMoveToast.styled.tsx
@@ -0,0 +1,13 @@
+import styled from "@emotion/styled";
+import { space } from "metabase/styled-components/theme";
+
+import Icon from "metabase/components/Icon";
+
+export const ToastRoot = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+export const StyledIcon = styled(Icon)`
+  margin-right: ${space(1)};
+`;
diff --git a/frontend/src/metabase/query_builder/components/QuestionMoveToast/QuestionMoveToast.tsx b/frontend/src/metabase/query_builder/components/QuestionMoveToast/QuestionMoveToast.tsx
new file mode 100644
index 00000000000..8faf35fe53e
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/QuestionMoveToast/QuestionMoveToast.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+import { jt } from "ttag";
+
+import Collections from "metabase/entities/collections";
+import { color } from "metabase/lib/colors";
+
+import { StyledIcon, ToastRoot } from "./QuestionMoveToast.styled";
+
+interface QuestionMoveToastProps {
+  isModel: boolean;
+  collectionId: number;
+}
+
+export default function QuestionMoveToast({
+  isModel,
+  collectionId,
+}: QuestionMoveToastProps) {
+  return (
+    <ToastRoot>
+      <StyledIcon name="all" color="white" />
+      {isModel
+        ? jt`Model moved to ${(
+            <Collections.Link
+              key="collection-link"
+              id={collectionId}
+              ml={1}
+              color={color("brand")}
+            />
+          )}`
+        : jt`Question moved to ${(
+            <Collections.Link
+              key="collection-link"
+              id={collectionId}
+              ml={1}
+              color={color("brand")}
+            />
+          )}`}
+    </ToastRoot>
+  );
+}
diff --git a/frontend/src/metabase/query_builder/components/QuestionMoveToast/index.ts b/frontend/src/metabase/query_builder/components/QuestionMoveToast/index.ts
new file mode 100644
index 00000000000..f3008df3e7f
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/QuestionMoveToast/index.ts
@@ -0,0 +1 @@
+export { default } from "./QuestionMoveToast";
diff --git a/frontend/test/metabase/scenarios/question/question-management.cy.spec.js b/frontend/test/metabase/scenarios/question/question-management.cy.spec.js
index db5a2018fb4..fd40894a72a 100644
--- a/frontend/test/metabase/scenarios/question/question-management.cy.spec.js
+++ b/frontend/test/metabase/scenarios/question/question-management.cy.spec.js
@@ -3,6 +3,8 @@ import {
   visitQuestion,
   saveDashboard,
   popover,
+  openNavigationSidebar,
+  navigationSidebar,
   openQuestionActions,
   questionInfoButton,
 } from "__support__/e2e/helpers";
@@ -35,7 +37,6 @@ describe("managing question from the question's details sidebar", () => {
             });
 
             it("should be able to edit question details (metabase#11719-1)", () => {
-              // cy.skipOn(user === "nodata");
               cy.findByTestId("saved-question-header-title")
                 .click()
                 .type("1")
@@ -45,8 +46,6 @@ describe("managing question from the question's details sidebar", () => {
             });
 
             it("should be able to edit a question's description", () => {
-              // cy.skipOn(user === "nodata");
-
               questionInfoButton().click();
 
               cy.findByPlaceholderText("Add description")
@@ -58,14 +57,79 @@ describe("managing question from the question's details sidebar", () => {
               cy.findByText("foo");
             });
 
-            it("should be able to move the question (metabase#11719-2)", () => {
-              // cy.skipOn(user === "nodata");
-              openQuestionActions();
-              cy.findByTestId("move-button").click();
-              cy.findByText("My personal collection").click();
-              clickButton("Move");
-              assertOnRequest("updateQuestion");
-              cy.contains("37.65");
+            describe("move", () => {
+              it("should be able to move the question (metabase#11719-2)", () => {
+                openNavigationSidebar();
+                navigationSidebar().within(() => {
+                  // Highlight "Our analytics"
+                  cy.findByText("Our analytics")
+                    .parents("li")
+                    .should("have.attr", "aria-selected", "true");
+                  cy.findByText("Your personal collection")
+                    .parents("li")
+                    .should("have.attr", "aria-selected", "false");
+                });
+
+                openQuestionActions();
+                cy.findByTestId("move-button").click();
+                cy.findByText("My personal collection").click();
+                clickButton("Move");
+                assertOnRequest("updateQuestion");
+                cy.contains("37.65");
+
+                cy.contains(
+                  `Question moved to ${getPersonalCollectionName(USERS[user])}`,
+                );
+
+                navigationSidebar().within(() => {
+                  // Highlight "Your personal collection" after move
+                  cy.findByText("Our analytics")
+                    .parents("li")
+                    .should("have.attr", "aria-selected", "false");
+                  cy.findByText("Your personal collection")
+                    .parents("li")
+                    .should("have.attr", "aria-selected", "true");
+                });
+              });
+
+              it("should be able to move models", () => {
+                // TODO: Currently nodata users can't turn a question into a model
+                cy.skipOn(user === "nodata");
+
+                turnIntoModel();
+
+                openNavigationSidebar();
+                navigationSidebar().within(() => {
+                  // Highlight "Our analytics"
+                  cy.findByText("Our analytics")
+                    .parents("li")
+                    .should("have.attr", "aria-selected", "true");
+                  cy.findByText("Your personal collection")
+                    .parents("li")
+                    .should("have.attr", "aria-selected", "false");
+                });
+
+                openQuestionActions();
+                cy.findByTestId("move-button").click();
+                cy.findByText("My personal collection").click();
+                clickButton("Move");
+                assertOnRequest("updateQuestion");
+                cy.contains("37.65");
+
+                cy.contains(
+                  `Model moved to ${getPersonalCollectionName(USERS[user])}`,
+                );
+
+                navigationSidebar().within(() => {
+                  // Highlight "Your personal collection" after move
+                  cy.findByText("Our analytics")
+                    .parents("li")
+                    .should("have.attr", "aria-selected", "false");
+                  cy.findByText("Your personal collection")
+                    .parents("li")
+                    .should("have.attr", "aria-selected", "true");
+                });
+              });
             });
 
             it("should be able to archive the question (metabase#11719-3, metabase#16512, metabase#20133)", () => {
@@ -182,3 +246,15 @@ function assertOnRequest(xhr_alias) {
     "not.exist",
   );
 }
+
+function getPersonalCollectionName(user) {
+  const name = [user.first_name, user.last_name].join(" ");
+
+  return `${name}'s Personal Collection`;
+}
+
+function turnIntoModel() {
+  openQuestionActions();
+  cy.findByText("Turn into a model").click();
+  cy.findByText("Turn this into a model").click();
+}
-- 
GitLab