From 60cb22c438e3a2545090c9ae76cf2b744e9fb12e Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Wed, 27 Jul 2022 18:13:16 +0300
Subject: [PATCH] Do not offer to save questions to view-only collections
 (#24307)

---
 frontend/src/metabase/containers/SaveQuestionModal.jsx    | 3 ++-
 frontend/src/metabase/entities/groups.js                  | 6 +++---
 frontend/src/metabase/entities/persisted-models.js        | 4 ++--
 frontend/src/metabase/entities/schemas.js                 | 6 +++---
 frontend/src/metabase/entities/tables.js                  | 8 ++++----
 frontend/src/metabase/entities/timelines.js               | 6 +++---
 .../22727-readonly-collection-offered-on-save.cy.spec.js  | 2 +-
 7 files changed, 18 insertions(+), 17 deletions(-)

diff --git a/frontend/src/metabase/containers/SaveQuestionModal.jsx b/frontend/src/metabase/containers/SaveQuestionModal.jsx
index b5dbc97b728..0d1eefceb14 100644
--- a/frontend/src/metabase/containers/SaveQuestionModal.jsx
+++ b/frontend/src/metabase/containers/SaveQuestionModal.jsx
@@ -79,6 +79,7 @@ export default class SaveQuestionModal extends Component {
       this.props;
 
     const isStructured = Q_DEPRECATED.isStructured(card.dataset_query);
+    const isReadonly = originalCard != null && !originalCard.can_write;
 
     const initialValues = {
       name:
@@ -87,7 +88,7 @@ export default class SaveQuestionModal extends Component {
           : "",
       description: card.description || "",
       collection_id:
-        card.collection_id === undefined
+        card.collection_id === undefined || isReadonly
           ? initialCollectionId
           : card.collection_id,
       saveType:
diff --git a/frontend/src/metabase/entities/groups.js b/frontend/src/metabase/entities/groups.js
index 0b73937f51e..dfbd576c0a0 100644
--- a/frontend/src/metabase/entities/groups.js
+++ b/frontend/src/metabase/entities/groups.js
@@ -13,8 +13,8 @@ const Groups = createEntity({
     fields: [{ name: "name" }],
   },
 
-  reducer: (state = {}, { type, payload }) => {
-    if (type === CREATE_MEMBERSHIP) {
+  reducer: (state = {}, { type, payload, error }) => {
+    if (type === CREATE_MEMBERSHIP && !error) {
       const { membership, group_id } = payload;
       const members = state[group_id]?.members;
       if (members) {
@@ -25,7 +25,7 @@ const Groups = createEntity({
       }
     }
 
-    if (type === DELETE_MEMBERSHIP) {
+    if (type === DELETE_MEMBERSHIP && !error) {
       const { membershipId, groupId } = payload;
       const members = state[groupId]?.members;
       if (members) {
diff --git a/frontend/src/metabase/entities/persisted-models.js b/frontend/src/metabase/entities/persisted-models.js
index 462aa5dc2ca..f2267f339c9 100644
--- a/frontend/src/metabase/entities/persisted-models.js
+++ b/frontend/src/metabase/entities/persisted-models.js
@@ -36,8 +36,8 @@ const PersistedModels = createEntity({
     getByModelId: getPersistedModelInfoByModelId,
   },
 
-  reducer: (state = {}, { type, payload }) => {
-    if (type === REFRESH_CACHE) {
+  reducer: (state = {}, { type, payload, error }) => {
+    if (type === REFRESH_CACHE && !error) {
       return {
         ...state,
         [payload.id]: {
diff --git a/frontend/src/metabase/entities/schemas.js b/frontend/src/metabase/entities/schemas.js
index 50fde6e7e06..26319561196 100644
--- a/frontend/src/metabase/entities/schemas.js
+++ b/frontend/src/metabase/entities/schemas.js
@@ -50,8 +50,8 @@ export default createEntity({
     },
   },
 
-  reducer: (state = {}, { type, payload }) => {
-    if (type === Questions.actionTypes.CREATE) {
+  reducer: (state = {}, { type, payload, error }) => {
+    if (type === Questions.actionTypes.CREATE && !error) {
       const { question, status, data } = payload;
       if (question) {
         const schema = getCollectionVirtualSchemaId(question.collection);
@@ -71,7 +71,7 @@ export default createEntity({
       }
     }
 
-    if (type === Questions.actionTypes.UPDATE) {
+    if (type === Questions.actionTypes.UPDATE && !error) {
       const { question } = payload;
       const schemaId = getCollectionVirtualSchemaId(question.collection);
 
diff --git a/frontend/src/metabase/entities/tables.js b/frontend/src/metabase/entities/tables.js
index 3bed285adc3..faf04c12986 100644
--- a/frontend/src/metabase/entities/tables.js
+++ b/frontend/src/metabase/entities/tables.js
@@ -144,7 +144,7 @@ const Tables = createEntity({
   },
 
   reducer: (state = {}, { type, payload, error }) => {
-    if (type === Questions.actionTypes.CREATE) {
+    if (type === Questions.actionTypes.CREATE && !error) {
       const card = payload.question;
       const virtualQuestionTable = convertSavedQuestionToVirtualTable(card);
 
@@ -158,7 +158,7 @@ const Tables = createEntity({
       };
     }
 
-    if (type === Questions.actionTypes.UPDATE) {
+    if (type === Questions.actionTypes.UPDATE && !error) {
       const card = payload.question;
       const virtualQuestionId = getQuestionVirtualTableId(card);
 
@@ -202,7 +202,7 @@ const Tables = createEntity({
       }
     }
 
-    if (type === Metrics.actionTypes.CREATE) {
+    if (type === Metrics.actionTypes.CREATE && !error) {
       const { table_id: tableId, id: metricId } = payload.metric;
       const table = state[tableId];
       if (table) {
@@ -213,7 +213,7 @@ const Tables = createEntity({
       }
     }
 
-    if (type === Segments.actionTypes.UPDATE) {
+    if (type === Segments.actionTypes.UPDATE && !error) {
       const { table_id: tableId, archived, id: segmentId } = payload.segment;
       const table = state[tableId];
       if (archived && table && table.segments) {
diff --git a/frontend/src/metabase/entities/timelines.js b/frontend/src/metabase/entities/timelines.js
index 6235e457eef..12c82472201 100644
--- a/frontend/src/metabase/entities/timelines.js
+++ b/frontend/src/metabase/entities/timelines.js
@@ -57,7 +57,7 @@ const Timelines = createEntity({
   },
 
   reducer: (state = {}, action) => {
-    if (action.type === TimelineEvents.actionTypes.CREATE) {
+    if (action.type === TimelineEvents.actionTypes.CREATE && !action.error) {
       const event = TimelineEvents.HACK_getObjectFromAction(action);
 
       return updateIn(state, [event.timeline_id, "events"], (eventIds = []) => {
@@ -65,7 +65,7 @@ const Timelines = createEntity({
       });
     }
 
-    if (action.type === TimelineEvents.actionTypes.UPDATE) {
+    if (action.type === TimelineEvents.actionTypes.UPDATE && !action.error) {
       const event = TimelineEvents.HACK_getObjectFromAction(action);
 
       return _.mapObject(state, timeline => {
@@ -84,7 +84,7 @@ const Timelines = createEntity({
       });
     }
 
-    if (action.type === TimelineEvents.actionTypes.DELETE) {
+    if (action.type === TimelineEvents.actionTypes.DELETE && !action.error) {
       const eventId = action.payload.result;
 
       return _.mapObject(state, timeline => {
diff --git a/frontend/test/metabase/scenarios/permissions/reproductions/22727-readonly-collection-offered-on-save.cy.spec.js b/frontend/test/metabase/scenarios/permissions/reproductions/22727-readonly-collection-offered-on-save.cy.spec.js
index 0b99cb5401a..9b08f905eaa 100644
--- a/frontend/test/metabase/scenarios/permissions/reproductions/22727-readonly-collection-offered-on-save.cy.spec.js
+++ b/frontend/test/metabase/scenarios/permissions/reproductions/22727-readonly-collection-offered-on-save.cy.spec.js
@@ -3,7 +3,7 @@ import { USER_GROUPS } from "__support__/e2e/cypress_data";
 
 const { ALL_USERS_GROUP } = USER_GROUPS;
 
-describe.skip("issue 22727", () => {
+describe("issue 22727", () => {
   beforeEach(() => {
     cy.intercept("POST", "/api/dataset").as("dataset");
 
-- 
GitLab