From 6ffd3c1b63ea08a961f4eb1640799a2acaba7937 Mon Sep 17 00:00:00 2001
From: Cal Herries <39073188+calherries@users.noreply.github.com>
Date: Tue, 17 Jan 2023 20:47:49 +0200
Subject: [PATCH] Fix for #6884: Update dashboard save to create only one
 revision when you add a card (#27708)

* Update dashboard save to save all dashcard data on create, not update

* Remove repro e2e test

* Add e2e test

* Add series to POST request, missing by mistake

* Formatting

* Rename DashboardApi methods for consistent names and cases
---
 .../src/metabase/dashboard/actions/save.js    | 86 ++++++++-----------
 frontend/src/metabase/services.js             |  6 +-
 .../collections/revision-history.cy.spec.js   | 37 ++++----
 3 files changed, 54 insertions(+), 75 deletions(-)

diff --git a/frontend/src/metabase/dashboard/actions/save.js b/frontend/src/metabase/dashboard/actions/save.js
index c73c6f3c591..98cd8775b17 100644
--- a/frontend/src/metabase/dashboard/actions/save.js
+++ b/frontend/src/metabase/dashboard/actions/save.js
@@ -54,28 +54,38 @@ export const saveDashboardAndCards = createThunkAction(
         dashboard.ordered_cards
           .filter(dc => dc.isRemoved && !dc.isAdded)
           .map(dc =>
-            DashboardApi.removecard({
+            DashboardApi.removeCard({
               dashId: dashboard.id,
               dashcardId: dc.id,
             }),
           ),
       );
 
-      // add isAdded dashboards
+      // update parameter mappings
+      dashboard.ordered_cards = dashboard.ordered_cards.map(dc => ({
+        ...dc,
+        parameter_mappings: dc.parameter_mappings.filter(
+          mapping =>
+            // filter out mappings for deleted parameters
+            _.findWhere(dashboard.parameters, {
+              id: mapping.parameter_id,
+            }) &&
+            // filter out mappings for deleted series
+            (!dc.card_id ||
+              dc.card_id === mapping.card_id ||
+              _.findWhere(dc.series, { id: mapping.card_id })),
+        ),
+      }));
+
+      // add new cards to dashboard
       const updatedDashcards = await Promise.all(
         dashboard.ordered_cards
           .filter(dc => !dc.isRemoved)
           .map(async dc => {
             if (dc.isAdded) {
-              const result = await DashboardApi.addcard({
+              const result = await DashboardApi.addCard({
                 dashId: dashboard.id,
                 cardId: dc.card_id,
-              });
-              dispatch(updateDashcardId(dc.id, result.id));
-
-              // mark isAdded because addcard doesn't record the position
-              return {
-                ...result,
                 col: dc.col,
                 row: dc.row,
                 size_x: dc.size_x,
@@ -83,8 +93,9 @@ export const saveDashboardAndCards = createThunkAction(
                 series: dc.series,
                 parameter_mappings: dc.parameter_mappings,
                 visualization_settings: dc.visualization_settings,
-                isAdded: true,
-              };
+              });
+              dispatch(updateDashcardId(dc.id, result.id));
+              return result;
             } else {
               return dc;
             }
@@ -106,45 +117,20 @@ export const saveDashboardAndCards = createThunkAction(
         );
       }
 
-      // reposition the cards
-      if (_.some(updatedDashcards, dc => dc.isDirty || dc.isAdded)) {
-        const cards = updatedDashcards.map(
-          ({
-            id,
-            card_id,
-            row,
-            col,
-            size_x,
-            size_y,
-            series,
-            parameter_mappings,
-            visualization_settings,
-          }) => ({
-            id,
-            card_id,
-            row,
-            col,
-            size_x,
-            size_y,
-            series,
-            visualization_settings,
-            parameter_mappings:
-              parameter_mappings &&
-              parameter_mappings.filter(
-                mapping =>
-                  // filter out mappings for deleted parameters
-                  _.findWhere(dashboard.parameters, {
-                    id: mapping.parameter_id,
-                  }) &&
-                  // filter out mappings for deleted series
-                  (!card_id ||
-                    card_id === mapping.card_id ||
-                    _.findWhere(series, { id: mapping.card_id })),
-              ),
-          }),
-        );
-
-        const result = await DashboardApi.reposition_cards({
+      // update the dashboard cards
+      if (_.some(updatedDashcards, dc => dc.isDirty)) {
+        const cards = updatedDashcards.map(dc => ({
+          id: dc.id,
+          card_id: dc.card_id,
+          row: dc.row,
+          col: dc.col,
+          size_x: dc.size_x,
+          size_y: dc.size_y,
+          series: dc.series,
+          visualization_settings: dc.visualization_settings,
+          parameter_mappings: dc.parameter_mappings,
+        }));
+        const result = await DashboardApi.updateCards({
           dashId: dashboard.id,
           cards,
         });
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index 7d20d3788f4..597f6958ebe 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -119,9 +119,9 @@ export const DashboardApi = {
   get: GET("/api/dashboard/:dashId"),
   update: PUT("/api/dashboard/:id"),
   delete: DELETE("/api/dashboard/:dashId"),
-  addcard: POST("/api/dashboard/:dashId/cards"),
-  removecard: DELETE("/api/dashboard/:dashId/cards"),
-  reposition_cards: PUT("/api/dashboard/:dashId/cards"),
+  addCard: POST("/api/dashboard/:dashId/cards"),
+  removeCard: DELETE("/api/dashboard/:dashId/cards"),
+  updateCards: PUT("/api/dashboard/:dashId/cards"),
   favorite: POST("/api/dashboard/:dashId/favorite"),
   unfavorite: DELETE("/api/dashboard/:dashId/favorite"),
   parameterValues: GET("/api/dashboard/:dashId/params/:paramId/values"),
diff --git a/frontend/test/metabase/scenarios/collections/revision-history.cy.spec.js b/frontend/test/metabase/scenarios/collections/revision-history.cy.spec.js
index bd6bfc18ad0..7750242aec4 100644
--- a/frontend/test/metabase/scenarios/collections/revision-history.cy.spec.js
+++ b/frontend/test/metabase/scenarios/collections/revision-history.cy.spec.js
@@ -42,27 +42,6 @@ describe("revision history", () => {
 
       cy.findAllByText("Revert").should("not.exist");
     });
-
-    it.skip("dashboard should update properly on revert (metabase#6884)", () => {
-      visitAndEditDashboard(1);
-      // Add another question without changing its size or moving it afterwards
-      cy.icon("add").last().click();
-      cy.findByText("Orders, Count").click();
-      saveDashboard();
-      // Revert the card to the state when the second card was added
-      cy.icon("ellipsis").click();
-      cy.findByText("Revision history").click();
-      clickRevert("added a card.", 0); // the top-most string or the latest card addition
-      cy.wait("@revert");
-      cy.request("GET", "/api/dashboard/1").then(xhr => {
-        const SECOND_CARD = xhr.body.ordered_cards[1];
-        const { col, size_x, size_y } = SECOND_CARD;
-        // The second card shrunk its size and changed the position completely to the left covering the first one
-        expect(col).not.to.eq(0);
-        expect(size_x).to.eq(4);
-        expect(size_y).to.eq(4);
-      });
-    });
   });
 
   Object.entries(PERMISSIONS).forEach(([permission, userGroup]) => {
@@ -84,6 +63,20 @@ describe("revision history", () => {
               }
             });
 
+            it("shouldn't create a rearrange revision when adding a card (metabase#6884)", () => {
+              cy.createDashboard().then(({ body }) => {
+                visitAndEditDashboard(body.id);
+              });
+              cy.icon("add").last().click();
+              cy.findByText("Orders, Count").click();
+              saveDashboard();
+              openRevisionHistory();
+              cy.findByText(/added a card/)
+                .siblings("button")
+                .should("not.exist");
+              cy.findByText(/rearranged the cards/).should("not.exist");
+            });
+
             it("should be able to revert a dashboard (metabase#15237)", () => {
               visitDashboard(1);
               openRevisionHistory();
@@ -99,7 +92,7 @@ describe("revision history", () => {
 
               // Should be able to revert back again
               cy.findByText("History");
-              clickRevert(/rearranged the cards/);
+              clickRevert(/added a card/);
 
               cy.wait("@revert").then(({ response: { statusCode, body } }) => {
                 expect(statusCode).to.eq(200);
-- 
GitLab