From 88aa0fab6f0542c4c7503c00c53b2cc04272a4f8 Mon Sep 17 00:00:00 2001
From: Ryan Laurie <30528226+iethree@users.noreply.github.com>
Date: Fri, 24 Feb 2023 03:22:41 -0700
Subject: [PATCH] Fallback to generating action form fields from parameters
 (#28512)

* fallback to generating form metadata from parameters

* fix actions test

* update unit tests

* update e2e test strings
---
 .../components/ActionViz/Action.unit.spec.tsx |  14 +--
 .../ActionParametersInputForm.unit.spec.tsx   |  20 ++--
 frontend/src/metabase/actions/utils.ts        |  12 +-
 .../src/metabase/actions/utils.unit.spec.ts   | 113 +++++++++++++++++-
 .../actions-on-dashboards.cy.spec.js          |  10 +-
 .../scenarios/models/model-actions.cy.spec.js |   2 +-
 6 files changed, 140 insertions(+), 31 deletions(-)

diff --git a/frontend/src/metabase/actions/components/ActionViz/Action.unit.spec.tsx b/frontend/src/metabase/actions/components/ActionViz/Action.unit.spec.tsx
index 536365f1d68..95d783d12d9 100644
--- a/frontend/src/metabase/actions/components/ActionViz/Action.unit.spec.tsx
+++ b/frontend/src/metabase/actions/components/ActionViz/Action.unit.spec.tsx
@@ -25,14 +25,12 @@ const defaultProps = {
       database_id: 2,
       parameters: [
         createMockActionParameter({
-          id: "1",
-          name: "Parameter 1",
+          id: "parameter_1",
           type: "type/Text",
           target: ["variable", ["template-tag", "1"]],
         }),
         createMockActionParameter({
-          id: "2",
-          name: "Parameter 2",
+          id: "parameter_2",
           type: "type/Text",
           target: ["variable", ["template-tag", "2"]],
         }),
@@ -219,8 +217,8 @@ describe("Actions > ActionViz > ActionComponent", () => {
       const expectedBody = {
         modelId: 777,
         parameters: {
-          "1": "foo",
-          "2": "bar",
+          parameter_1: "foo",
+          parameter_2: "bar",
         },
       };
 
@@ -252,8 +250,8 @@ describe("Actions > ActionViz > ActionComponent", () => {
       const expectedBody = {
         modelId: 777,
         parameters: {
-          "1": "foo",
-          "2": "baz",
+          parameter_1: "foo",
+          parameter_2: "baz",
         },
       };
 
diff --git a/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputForm.unit.spec.tsx b/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputForm.unit.spec.tsx
index 193fae26394..2474d3165d4 100644
--- a/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputForm.unit.spec.tsx
+++ b/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputForm.unit.spec.tsx
@@ -19,13 +19,11 @@ import ActionParametersInputModal from "./ActionParametersInputModal";
 const defaultProps = {
   missingParameters: [
     createMockActionParameter({
-      id: "1",
-      name: "Parameter 1",
+      id: "parameter_1",
       type: "type/Text",
     }),
     createMockActionParameter({
-      id: "2",
-      name: "Parameter 2",
+      id: "parameter_2",
       type: "type/Text",
     }),
   ],
@@ -55,8 +53,8 @@ async function setupModal(options?: any) {
 
 function setupPrefetch() {
   fetchMock.get("path:/api/dashboard/123/dashcard/456/execute", {
-    "1": "uno",
-    "2": "dos",
+    parameter_1: "uno",
+    parameter_2: "dos",
   });
 }
 
@@ -95,8 +93,8 @@ describe("Actions > ActionParametersInputForm", () => {
 
     await waitFor(() => {
       expect(submitSpy).toHaveBeenCalledWith({
-        "1": "uno",
-        "2": "dos",
+        parameter_1: "uno",
+        parameter_2: "dos",
       });
     });
   });
@@ -104,13 +102,11 @@ describe("Actions > ActionParametersInputForm", () => {
   it("should generate field types from parameter types", async () => {
     const missingParameters = [
       createMockActionParameter({
-        id: "1",
-        name: "Parameter 1",
+        id: "parameter_1",
         type: "type/Text",
       }),
       createMockActionParameter({
-        id: "2",
-        name: "Parameter 2",
+        id: "parameter_2",
         type: "type/Integer",
       }),
     ];
diff --git a/frontend/src/metabase/actions/utils.ts b/frontend/src/metabase/actions/utils.ts
index 3cb49b8da81..b2b1e4ffffd 100644
--- a/frontend/src/metabase/actions/utils.ts
+++ b/frontend/src/metabase/actions/utils.ts
@@ -17,7 +17,7 @@ import type {
 } from "metabase-types/api";
 
 import { getResponseErrorMessage } from "metabase/core/utils/errors";
-import { slugify } from "metabase/lib/formatting";
+import { slugify, humanize } from "metabase/lib/formatting";
 import { isEmpty } from "metabase/lib/validate";
 
 import { TYPE } from "metabase-lib/types/constants";
@@ -141,7 +141,14 @@ export const generateFieldSettingsFromParameters = (
   params.forEach((param, index) => {
     const field = fieldMetadataMap[param.id]
       ? new Field(fieldMetadataMap[param.id])
-      : undefined;
+      : new Field({
+          id: param.id,
+          name: param.id,
+          slug: param.id,
+          display_name: humanize(param.id),
+          base_type: param.type,
+          semantic_type: param.type,
+        });
 
     const name = param["display-name"] ?? param.name ?? param.id;
     const displayName = field?.displayName?.() ?? name;
@@ -156,7 +163,6 @@ export const generateFieldSettingsFromParameters = (
       description: field?.description ?? "",
       fieldType: getFieldType(param),
       inputType: getInputType(param, field),
-      field: field ?? undefined,
     });
   });
   return fieldSettings;
diff --git a/frontend/src/metabase/actions/utils.unit.spec.ts b/frontend/src/metabase/actions/utils.unit.spec.ts
index 9317e2d1334..cb25969e16e 100644
--- a/frontend/src/metabase/actions/utils.unit.spec.ts
+++ b/frontend/src/metabase/actions/utils.unit.spec.ts
@@ -124,6 +124,42 @@ describe("sortActionParams", () => {
       expect(settings.inputType).toBe("category");
     });
 
+    it("should generate settings for a dateTime field", () => {
+      const fields = [
+        createField({
+          name: "test-field",
+          base_type: "type/DateTime",
+          semantic_type: "type/DateTime",
+        }),
+      ];
+      const params = [
+        createParameter({ id: "test-field", type: "type/DateTime" }),
+      ];
+      const [_id, settings] = getFirstEntry(
+        generateFieldSettingsFromParameters(params, fields),
+      );
+
+      expect(settings.fieldType).toBe("string");
+      expect(settings.inputType).toBe("datetime");
+    });
+
+    it("should generate settings for a date field", () => {
+      const fields = [
+        createField({
+          name: "test-field",
+          base_type: "type/Date",
+          semantic_type: "type/Date",
+        }),
+      ];
+      const params = [createParameter({ id: "test-field", type: "type/Date" })];
+      const [_id, settings] = getFirstEntry(
+        generateFieldSettingsFromParameters(params, fields),
+      );
+
+      expect(settings.fieldType).toBe("string");
+      expect(settings.inputType).toBe("date");
+    });
+
     it("should set the parameter id as the object key", () => {
       const fields = [createField({ name: "test-field" })];
       const params = [createParameter({ id: "test-field" })];
@@ -165,8 +201,8 @@ describe("sortActionParams", () => {
         generateFieldSettingsFromParameters(params, fields),
       );
 
-      expect(settings.placeholder).toBe("test-field");
-      expect(settings.title).toBe("test-field");
+      expect(settings.placeholder).toBe("Test-field");
+      expect(settings.title).toBe("Test-field");
       expect(settings.name).toBe("test-field");
     });
 
@@ -201,6 +237,79 @@ describe("sortActionParams", () => {
 
       expect(settings.description).toBe("foo bar baz");
     });
+
+    describe("without field metadata", () => {
+      it("humanizes parameter id in the field title", () => {
+        const params = [
+          createParameter({ id: "test_field", type: "type/Integer" }),
+        ];
+        const [_id, settings] = getFirstEntry(
+          generateFieldSettingsFromParameters(params),
+        );
+
+        expect(settings.title).toBe("Test field");
+      });
+
+      it("generates field settings for a numeric field", () => {
+        const params = [
+          createParameter({ id: "test-field", type: "type/Integer" }),
+        ];
+        const [_id, settings] = getFirstEntry(
+          generateFieldSettingsFromParameters(params),
+        );
+
+        expect(settings.fieldType).toBe("number");
+        expect(settings.inputType).toBe("number");
+      });
+
+      it("generates field settings for a string field", () => {
+        const params = [
+          createParameter({ id: "test-field", type: "type/String" }),
+        ];
+        const [_id, settings] = getFirstEntry(
+          generateFieldSettingsFromParameters(params),
+        );
+
+        expect(settings.fieldType).toBe("string");
+        expect(settings.inputType).toBe("string");
+      });
+
+      it("generates field settings for a date field", () => {
+        const params = [
+          createParameter({ id: "test-field", type: "type/Date" }),
+        ];
+        const [_id, settings] = getFirstEntry(
+          generateFieldSettingsFromParameters(params),
+        );
+
+        expect(settings.fieldType).toBe("string");
+        expect(settings.inputType).toBe("date");
+      });
+
+      it("generates field settings for a datetime field", () => {
+        const params = [
+          createParameter({ id: "test-field", type: "type/DateTime" }),
+        ];
+        const [_id, settings] = getFirstEntry(
+          generateFieldSettingsFromParameters(params),
+        );
+
+        expect(settings.fieldType).toBe("string");
+        expect(settings.inputType).toBe("datetime");
+      });
+
+      it("generates field settings for a json field", () => {
+        const params = [
+          createParameter({ id: "test-field", type: "type/Structured" }),
+        ];
+        const [_id, settings] = getFirstEntry(
+          generateFieldSettingsFromParameters(params),
+        );
+
+        expect(settings.fieldType).toBe("string");
+        expect(settings.inputType).toBe("text");
+      });
+    });
   });
 
   describe("getInputType", () => {
diff --git a/frontend/test/metabase/scenarios/dashboard/actions-on-dashboards.cy.spec.js b/frontend/test/metabase/scenarios/dashboard/actions-on-dashboards.cy.spec.js
index ec90a209d5b..5896dbc38fb 100644
--- a/frontend/test/metabase/scenarios/dashboard/actions-on-dashboards.cy.spec.js
+++ b/frontend/test/metabase/scenarios/dashboard/actions-on-dashboards.cy.spec.js
@@ -109,8 +109,8 @@ const MODEL_NAME = "Test Action Model";
         cy.button("Create").click();
 
         modal().within(() => {
-          cy.findByPlaceholderText("team_name").type("Zany Zebras");
-          cy.findByPlaceholderText("score").type("44");
+          cy.findByPlaceholderText("Team name").type("Zany Zebras");
+          cy.findByPlaceholderText("Score").type("44");
 
           cy.button("Save").click();
         });
@@ -152,12 +152,12 @@ const MODEL_NAME = "Test Action Model";
         cy.wait("@executePrefetch");
         // let's check that the existing values are pre-filled correctly
         modal().within(() => {
-          cy.findByPlaceholderText("team_name")
+          cy.findByPlaceholderText("Team name")
             .should("have.value", "Energetic Elephants")
             .clear()
             .type("Emotional Elephants");
 
-          cy.findByPlaceholderText("score")
+          cy.findByPlaceholderText("Score")
             .should("have.value", "30")
             .clear()
             .type("88");
@@ -202,7 +202,7 @@ const MODEL_NAME = "Test Action Model";
         cy.button("Delete").click();
 
         modal().within(() => {
-          cy.findByPlaceholderText("id").type("3");
+          cy.findByPlaceholderText("Id").type("3");
           cy.button("Delete").click();
         });
 
diff --git a/frontend/test/metabase/scenarios/models/model-actions.cy.spec.js b/frontend/test/metabase/scenarios/models/model-actions.cy.spec.js
index 6a47754f650..62ff5459f3b 100644
--- a/frontend/test/metabase/scenarios/models/model-actions.cy.spec.js
+++ b/frontend/test/metabase/scenarios/models/model-actions.cy.spec.js
@@ -239,7 +239,7 @@ describe(
         cy.visit(url);
 
         // Order 1 has quantity 2 by default, so we're not actually mutating data
-        cy.findByLabelText("id").type("1");
+        cy.findByLabelText("Id").type("1");
         cy.findByLabelText(/quantity/i).type("2");
 
         cy.findByRole("button", { name: "Submit" }).click();
-- 
GitLab