Skip to content
Snippets Groups Projects
Unverified Commit cb51e574 authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Action Creator 4: Derive parameter types from form settings on save (#25297)

* derive parameter types from form settings on save

* update template tag types with parameter types

* improve tests
parent cf806195
No related branches found
No related tags found
No related merge requests found
Showing with 358 additions and 53 deletions
......@@ -25,3 +25,4 @@ export * from "./timeline";
export * from "./user";
export * from "./writeback";
export * from "./writeback-form-settings";
export * from "./parameters";
export type StringParameterType =
| "string/="
| "string/!="
| "string/contains"
| "string/does-not-contain"
| "string/starts-with"
| "string/ends-with";
export type NumberParameterType =
| "number/="
| "number/!="
| "number/between"
| "number/>="
| "number/<=";
export type DateParameterType =
| "date/single"
| "date/range"
| "date/relative"
| "date/month-year"
| "date/quarter-year";
("date/all-options");
export type ParameterType =
| StringParameterType
| NumberParameterType
| DateParameterType;
export type FormType = "inline" | "modal";
export type FieldType = "text" | "number" | "date" | "category";
export type FieldType = "string" | "number" | "date" | "category";
export type DateInputType = "date" | "datetime" | "monthyear" | "quarteryear";
export type InputType =
| DateInputType
| "string"
| "text"
| "number"
| "date"
| "datetime"
| "monthyear"
| "quarteryear"
| "dropdown"
| "inline-select";
export type Size = "small" | "medium" | "large";
export type DateRange = [string, string];
......
......@@ -6,7 +6,11 @@ import type { ActionFormSettings } from "metabase-types/api";
import { CardApi } from "metabase/services";
import { saveForm } from "./forms";
import { removeOrphanSettings } from "metabase/entities/actions/utils";
import {
removeOrphanSettings,
setParameterTypesFromFieldSettings,
setTemplateTagTypesFromFieldSettings,
} from "metabase/entities/actions/utils";
type ActionParams = {
name: string;
......@@ -24,12 +28,17 @@ const getAPIFn =
question,
collection_id,
formSettings,
}: ActionParams) =>
apifn({
}: ActionParams) => {
question = setTemplateTagTypesFromFieldSettings(formSettings, question);
return apifn({
...question.card(),
name,
description,
parameters: question.parameters(),
parameters: setParameterTypesFromFieldSettings(
formSettings,
question.parameters(),
),
is_write: true,
display: "table",
visualization_settings: removeOrphanSettings(
......@@ -38,6 +47,7 @@ const getAPIFn =
),
collection_id,
});
};
const createAction = getAPIFn(CardApi.create);
const updateAction = getAPIFn(CardApi.update);
......
import type { ParameterType } from "metabase-types/api";
import type { TemplateTagType } from "metabase-types/types/Query";
interface FieldTypeMap {
[key: string]: ParameterType;
}
interface TagTypeMap {
[key: string]: TemplateTagType;
}
export const fieldTypeToParameterTypeMap: FieldTypeMap = {
string: "string/=",
category: "string/=",
number: "number/=",
};
export const dateTypetoParameterTypeMap: FieldTypeMap = {
date: "date/single",
datetime: "date/single",
monthyear: "date/month-year",
quarteryear: "date/quarter-year",
};
export const fieldTypeToTagTypeMap: TagTypeMap = {
string: "text",
category: "text",
number: "number",
date: "date",
};
import _ from "underscore";
import type {
ActionFormSettings,
FieldType,
InputType,
ParameterType,
} from "metabase-types/api";
import type { ActionFormSettings } from "metabase-types/api";
import type { Parameter as ParameterObject } from "metabase-types/types/Parameter";
import type { TemplateTag, TemplateTagType } from "metabase-types/types/Query";
import type NativeQuery from "metabase-lib/lib/queries/NativeQuery";
import type Question from "metabase-lib/lib/Question";
import {
fieldTypeToParameterTypeMap,
dateTypetoParameterTypeMap,
fieldTypeToTagTypeMap,
} from "./constants";
export const removeOrphanSettings = (
settings: ActionFormSettings,
......@@ -16,3 +30,54 @@ export const removeOrphanSettings = (
fields: _.omit(settings.fields, orphanIds),
};
};
const getParameterTypeFromFieldSettings = (
fieldType: FieldType,
inputType: InputType,
): ParameterType => {
if (fieldType === "date") {
return dateTypetoParameterTypeMap[inputType] ?? "date/single";
}
return fieldTypeToParameterTypeMap[fieldType] ?? "string/=";
};
const getTagTypeFromFieldSettings = (fieldType: FieldType): TemplateTagType => {
return fieldTypeToTagTypeMap[fieldType] ?? "text";
};
export const setParameterTypesFromFieldSettings = (
settings: ActionFormSettings,
parameters: ParameterObject[],
): ParameterObject[] => {
const fields = settings.fields;
return parameters.map(parameter => {
const field = fields[parameter.id];
return {
...parameter,
type: field
? getParameterTypeFromFieldSettings(field.fieldType, field.inputType)
: "string/=",
};
});
};
export const setTemplateTagTypesFromFieldSettings = (
settings: ActionFormSettings,
question: Question,
): Question => {
const fields = settings.fields;
(question.query() as NativeQuery)
.templateTagsWithoutSnippets()
.forEach((tag: TemplateTag) => {
question = question.setQuery(
(question.query() as NativeQuery).setTemplateTag(tag.name, {
...tag,
type: getTagTypeFromFieldSettings(fields[tag.id].fieldType),
}),
);
});
return question;
};
import { removeOrphanSettings } from "./utils";
import {
getDefaultFormSettings,
getDefaultFieldSettings,
} from "metabase/writeback/components/ActionCreator/FormCreator/utils";
import Question from "metabase-lib/lib/Question";
import { metadata } from "__support__/sample_database_fixture";
import type { Parameter as ParameterObject } from "metabase-types/types/Parameter";
import {
removeOrphanSettings,
setParameterTypesFromFieldSettings,
setTemplateTagTypesFromFieldSettings,
} from "./utils";
import { NativeDatasetQuery } from "metabase-types/types/Card";
const creatQuestionWithTemplateTags = (tagType: string) =>
new Question(
{
dataset_query: {
type: "native",
database: null,
native: {
query:
"INSERT INTO products (name, price) VALUES ({{name}}, {{price}});",
"template-tags": {
name: {
id: "aaa",
name: "name",
type: tagType,
},
price: {
id: "bbb",
name: "price",
type: tagType,
},
},
},
},
},
metadata,
);
describe("entities > actions > utils", () => {
it("should remove orphan settings", () => {
const formSettings = getDefaultFormSettings();
formSettings.name = "test form";
formSettings.fields.aaa = getDefaultFieldSettings();
formSettings.fields.bbb = getDefaultFieldSettings();
formSettings.fields.ccc = getDefaultFieldSettings();
const parameters = [
{ id: "aaa", name: "foo" },
{ id: "ccc", name: "bar" },
] as ParameterObject[];
const result = removeOrphanSettings(formSettings, parameters);
expect(result.name).toEqual("test form");
expect(result.fields).toHaveProperty("aaa");
expect(result.fields).toHaveProperty("ccc");
expect(result.fields).not.toHaveProperty("bbb");
describe("removeOrphanSettings", () => {
it("should remove orphan settings", () => {
const formSettings = getDefaultFormSettings({
name: "test form",
fields: {
aaa: getDefaultFieldSettings(),
bbb: getDefaultFieldSettings(),
ccc: getDefaultFieldSettings(),
},
});
const parameters = [
{ id: "aaa", name: "foo" },
{ id: "ccc", name: "bar" },
] as ParameterObject[];
const result = removeOrphanSettings(formSettings, parameters);
expect(result.name).toEqual("test form");
expect(result.fields).toHaveProperty("aaa");
expect(result.fields).toHaveProperty("ccc");
expect(result.fields).not.toHaveProperty("bbb");
});
it("should leave non-orphan settings intact", () => {
const formSettings = getDefaultFormSettings({
name: "test form",
fields: {
aaa: getDefaultFieldSettings(),
bbb: getDefaultFieldSettings(),
ccc: getDefaultFieldSettings(),
},
});
const parameters = [
{ id: "aaa", name: "foo" },
{ id: "bbb", name: "foo" },
{ id: "ccc", name: "bar" },
] as ParameterObject[];
const result = removeOrphanSettings(formSettings, parameters);
expect(result.name).toEqual("test form");
expect(result.fields).toHaveProperty("aaa");
expect(result.fields).toHaveProperty("bbb");
expect(result.fields).toHaveProperty("ccc");
});
});
it("should leave non-orphan settings intact", () => {
const formSettings = getDefaultFormSettings();
formSettings.name = "test form";
describe("setParameterTypesFromFieldSettings", () => {
it("should set string parameter types", () => {
const formSettings = getDefaultFormSettings({
name: "test form",
fields: {
aaa: getDefaultFieldSettings({ fieldType: "string" }),
bbb: getDefaultFieldSettings({ fieldType: "string" }),
ccc: getDefaultFieldSettings({ fieldType: "string" }),
},
});
const parameters = [
{ id: "aaa", name: "foo", type: "number/=" },
{ id: "bbb", name: "foo", type: "number/=" },
{ id: "ccc", name: "bar", type: "number/=" },
] as ParameterObject[];
const newParams = setParameterTypesFromFieldSettings(
formSettings,
parameters,
);
newParams.forEach(param => expect(param.type).toEqual("string/="));
});
it("should set number parameter types", () => {
const formSettings = getDefaultFormSettings({
name: "test form",
fields: {
aaa: getDefaultFieldSettings({ fieldType: "number" }),
bbb: getDefaultFieldSettings({ fieldType: "number" }),
ccc: getDefaultFieldSettings({ fieldType: "number" }),
},
});
const parameters = [
{ id: "aaa", name: "foo", type: "string/=" },
{ id: "bbb", name: "foo", type: "string/=" },
{ id: "ccc", name: "bar", type: "string/=" },
] as ParameterObject[];
const newParams = setParameterTypesFromFieldSettings(
formSettings,
parameters,
);
newParams.forEach(param => expect(param.type).toEqual("number/="));
});
it("should set date parameter types", () => {
const formSettings = getDefaultFormSettings({
name: "test form",
fields: {
aaa: getDefaultFieldSettings({ fieldType: "date" }),
bbb: getDefaultFieldSettings({ fieldType: "date" }),
ccc: getDefaultFieldSettings({ fieldType: "date" }),
},
});
const parameters = [
{ id: "aaa", name: "foo", type: "string/=" },
{ id: "bbb", name: "foo", type: "string/=" },
{ id: "ccc", name: "bar", type: "string/=" },
] as ParameterObject[];
const newParams = setParameterTypesFromFieldSettings(
formSettings,
parameters,
);
newParams.forEach(param => expect(param.type).toEqual("date/single"));
});
});
describe("setTemplateTagTypesFromFieldSettings", () => {
it("should set text and number template tag types", () => {
const question = creatQuestionWithTemplateTags("date");
const formSettings = getDefaultFormSettings({
name: "test form",
fields: {
aaa: getDefaultFieldSettings({ fieldType: "string" }),
bbb: getDefaultFieldSettings({ fieldType: "number" }),
},
});
const newQuestion = setTemplateTagTypesFromFieldSettings(
formSettings,
question,
);
const tags = (newQuestion.card().dataset_query as NativeDatasetQuery)
.native["template-tags"];
expect(tags.name.type).toEqual("text");
expect(tags.price.type).toEqual("number");
});
it("should set date template tag types", () => {
const question = creatQuestionWithTemplateTags("number");
formSettings.fields.aaa = getDefaultFieldSettings();
formSettings.fields.bbb = getDefaultFieldSettings();
formSettings.fields.ccc = getDefaultFieldSettings();
const formSettings = getDefaultFormSettings({
name: "test form",
fields: {
aaa: getDefaultFieldSettings({ fieldType: "date" }),
bbb: getDefaultFieldSettings({ fieldType: "date" }),
},
});
const parameters = [
{ id: "aaa", name: "foo" },
{ id: "bbb", name: "foo" },
{ id: "ccc", name: "bar" },
] as ParameterObject[];
const newQuestion = setTemplateTagTypesFromFieldSettings(
formSettings,
question,
);
const result = removeOrphanSettings(formSettings, parameters);
const tags = (newQuestion.card().dataset_query as NativeDatasetQuery)
.native["template-tags"];
expect(result.name).toEqual("test form");
expect(result.fields).toHaveProperty("aaa");
expect(result.fields).toHaveProperty("bbb");
expect(result.fields).toHaveProperty("ccc");
expect(tags.name.type).toEqual("date");
expect(tags.price.type).toEqual("date");
});
});
});
......@@ -107,7 +107,7 @@ function InputTypeSelect({
<Radio
vertical
value={value}
options={inputTypes[fieldType ?? "text"]}
options={inputTypes[fieldType ?? "string"]}
onChange={onChange}
/>
);
......
......@@ -8,7 +8,7 @@ interface FieldOptionType {
export const getFieldTypes = (): FieldOptionType[] => [
{
value: "text",
value: "string",
name: t`text`,
},
{
......@@ -31,7 +31,7 @@ interface InputOptionType {
}
interface InputOptionsMap {
text: InputOptionType[];
string: InputOptionType[];
number: InputOptionType[];
date: InputOptionType[];
category: InputOptionType[];
......@@ -60,7 +60,7 @@ const getSelectInputs = (): InputOptionType[] => [
];
export const getInputTypes = (): InputOptionsMap => ({
text: [...getTextInputs(), ...getSelectInputs()],
string: [...getTextInputs(), ...getSelectInputs()],
number: [
{
value: "number",
......
import type { ActionFormSettings, FieldSettings } from "metabase-types/api";
export const getDefaultFormSettings = (): ActionFormSettings => ({
export const getDefaultFormSettings = (
overrides: Partial<ActionFormSettings> = {},
): ActionFormSettings => ({
name: "",
type: "inline",
description: "",
fields: {},
confirmMessage: "",
...overrides,
});
export const getDefaultFieldSettings = (): FieldSettings => ({
export const getDefaultFieldSettings = (
overrides: Partial<FieldSettings> = {},
): FieldSettings => ({
name: "",
order: 0,
description: "",
placeholder: "",
fieldType: "text",
fieldType: "string",
inputType: "string",
required: false,
hidden: false,
width: "medium",
...overrides,
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment