diff --git a/frontend/src/metabase-types/api/actions.ts b/frontend/src/metabase-types/api/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..bdf3fc13f56c2e936407326fb9f30137889e0897 --- /dev/null +++ b/frontend/src/metabase-types/api/actions.ts @@ -0,0 +1,163 @@ +import type { ParameterTarget } from "metabase-types/types/Parameter"; +import type { Parameter, ParameterId } from "./parameters"; +import type { NativeDatasetQuery } from "./query"; + +export interface WritebackParameter extends Parameter { + target: ParameterTarget; +} + +export type WritebackActionType = "http" | "query" | "implicit"; + +export type WritebackActionId = number; + +export interface WritebackActionBase { + id: WritebackActionId; + model_id: number; + name: string; + description: string | null; + parameters: WritebackParameter[]; + visualization_settings?: ActionFormSettings; + updated_at: string; + created_at: string; +} + +export interface QueryAction { + type: "query"; + dataset_query: NativeDatasetQuery; +} + +export interface ImplicitQueryAction { + type: "implicit"; + kind: "row/create" | "row/update" | "row/delete"; +} + +export interface HttpAction { + type: "http"; + template: HttpActionTemplate; + response_handle: string | null; + error_handle: string | null; +} + +export type HttpActionResponseHandle = any; +export type HttpActionErrorHandle = any; + +export interface HttpActionTemplate { + method: string; + url: string; + body: string; + headers: string; + parameters: Record<ParameterId, Parameter>; + parameter_mappings: Record<ParameterId, ParameterTarget>; +} + +export type WritebackQueryAction = WritebackActionBase & QueryAction; +export type WritebackImplicitQueryAction = WritebackActionBase & + ImplicitQueryAction; +export type WritebackHttpAction = WritebackActionBase & HttpAction; +export type WritebackAction = WritebackActionBase & + (QueryAction | ImplicitQueryAction | HttpAction); + +export type ParameterMappings = Record<ParameterId, ParameterTarget>; + +export type ParametersForActionExecution = { + [id: ParameterId]: string | number | null; +}; + +export type ActionFormInitialValues = ParametersForActionExecution; + +export interface ActionFormSubmitResult { + success: boolean; + message?: string; + error?: string; +} + +export type OnSubmitActionForm = ( + parameters: ParametersForActionExecution, +) => Promise<ActionFormSubmitResult>; + +// Action Forms + +export type ActionDisplayType = "form" | "button"; +export type FieldType = "string" | "number" | "date" | "category"; + +export type DateInputType = "date" | "time" | "datetime"; + +// these types are saved in visualization_settings +export type InputSettingType = + | DateInputType + | "string" + | "text" + | "number" + | "select" + | "radio" + | "boolean" + | "category"; + +// these types get passed to the input components +export type InputComponentType = + | "text" + | "textarea" + | "number" + | "boolean" + | "select" + | "radio" + | "date" + | "time" + | "datetime-local" + | "category"; + +export type Size = "small" | "medium" | "large"; + +export type DateRange = [string, string]; +export type NumberRange = [number, number]; + +export interface FieldSettings { + id: string; + name: string; + title: string; + order: number; + description?: string | null; + placeholder?: string; + fieldType: FieldType; + inputType: InputSettingType; + required: boolean; + defaultValue?: string | number; + hidden: boolean; + range?: DateRange | NumberRange; + valueOptions?: (string | number)[]; + width?: Size; + height?: number; + hasSearch?: boolean; +} + +export type FieldSettingsMap = Record<ParameterId, FieldSettings>; +export interface ActionFormSettings { + name?: string; + type: ActionDisplayType; + description?: string; + fields: FieldSettingsMap; + submitButtonLabel?: string; + submitButtonColor?: string; + confirmMessage?: string; + successMessage?: string; + errorMessage?: string; +} + +export type ActionFormOption = { + name: string | number; + value: string | number; +}; + +export type ActionFormFieldProps = { + name: string; + title: string; + description?: string; + placeholder?: string; + type: InputComponentType; + optional?: boolean; + options?: ActionFormOption[]; +}; + +export type ActionFormProps = { + fields: ActionFormFieldProps[]; +}; diff --git a/frontend/src/metabase-types/api/card.ts b/frontend/src/metabase-types/api/card.ts index 65c87ac2a47953d7cb22066f61f14599206545c4..5fede8011f09517cbbf5b97be86633d98ec10eec 100644 --- a/frontend/src/metabase-types/api/card.ts +++ b/frontend/src/metabase-types/api/card.ts @@ -1,3 +1,4 @@ +import type { DatabaseId } from "./database"; import type { Field } from "./field"; import type { DatasetQuery } from "./query"; @@ -7,6 +8,7 @@ export interface Card extends UnsavedCard { name: string; description: string | null; dataset: boolean; + database_id?: DatabaseId; can_write: boolean; cache_ttl: number | null; query_average_duration?: number | null; diff --git a/frontend/src/metabase-types/api/index.ts b/frontend/src/metabase-types/api/index.ts index ca5b38d3b8af693b24989f4410abaf050bcfc79b..db49f2a142f63bcd5cfdf447dd1288795b45d6a8 100644 --- a/frontend/src/metabase-types/api/index.ts +++ b/frontend/src/metabase-types/api/index.ts @@ -1,3 +1,4 @@ +export * from "./actions"; export * from "./activity"; export * from "./automagic-dashboards"; export * from "./bookmark"; diff --git a/frontend/src/metabase-types/api/mocks/actions.ts b/frontend/src/metabase-types/api/mocks/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca1def78ae9332d1f8da101b61eb5e94d790ca89 --- /dev/null +++ b/frontend/src/metabase-types/api/mocks/actions.ts @@ -0,0 +1,54 @@ +import { + WritebackParameter, + WritebackQueryAction, + WritebackImplicitQueryAction, +} from "metabase-types/api"; +import { createMockNativeDatasetQuery } from "./query"; +import { createMockParameter } from "./parameters"; + +export const createMockActionParameter = ( + opts?: Partial<WritebackParameter>, +): WritebackParameter => ({ + target: opts?.target || ["variable", ["template-tag", "id"]], + ...createMockParameter({ + id: "id", + name: "ID", + type: "type/Integer", + slug: "id", + ...opts, + }), +}); + +export const createMockQueryAction = ({ + dataset_query = createMockNativeDatasetQuery(), + ...opts +}: Partial<WritebackQueryAction> = {}): WritebackQueryAction => { + return { + id: 1, + dataset_query, + name: "Query Action Mock", + description: null, + model_id: 1, + parameters: [], + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + ...opts, + type: "query", + }; +}; + +export const createMockImplicitQueryAction = ( + options: Partial<WritebackImplicitQueryAction>, +): WritebackImplicitQueryAction => ({ + id: 1, + kind: "row/create", + name: "", + description: "", + model_id: 1, + parameters: [], + visualization_settings: undefined, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + ...options, + type: "implicit", +}); diff --git a/frontend/src/metabase-types/api/mocks/index.ts b/frontend/src/metabase-types/api/mocks/index.ts index 65286226c84a54c8156161a8fa05f4481c585e50..8384fb7853aac9a1485881fdc37f5737fd7c9fda 100644 --- a/frontend/src/metabase-types/api/mocks/index.ts +++ b/frontend/src/metabase-types/api/mocks/index.ts @@ -1,3 +1,4 @@ +export * from "./actions"; export * from "./activity"; export * from "./automagic-dashboards"; export * from "./card"; diff --git a/frontend/src/metabase-types/api/parameters.ts b/frontend/src/metabase-types/api/parameters.ts index 618b9e4783474efa0f249b62c23c0fc72726e757..9c457b35f48c83125b61e2abed4c5a91f8dd1db8 100644 --- a/frontend/src/metabase-types/api/parameters.ts +++ b/frontend/src/metabase-types/api/parameters.ts @@ -35,10 +35,12 @@ export type ActionParameterValue = string | number; export interface Parameter { id: ParameterId; name: string; + "display-name"?: string; type: string; slug: string; sectionId?: string; default?: any; + required?: boolean; filteringParameters?: ParameterId[]; isMultiSelect?: boolean; value?: any; diff --git a/frontend/src/metabase-types/store/entities.ts b/frontend/src/metabase-types/store/entities.ts index 5726a5f5b8f81543bcaab581262f059ec6cb4cc4..3c0c4b7b6f096201c782dbf14dec826e7841665c 100644 --- a/frontend/src/metabase-types/store/entities.ts +++ b/frontend/src/metabase-types/store/entities.ts @@ -9,9 +9,12 @@ import { Table, User, UserId, + WritebackAction, + WritebackActionId, } from "metabase-types/api"; export interface EntitiesState { + actions?: Record<WritebackActionId, WritebackAction>; collections?: Record<CollectionId, Collection>; databases?: Record<number, Database>; fields?: Record<FieldId, Field>; diff --git a/frontend/src/metabase/actions/utils.ts b/frontend/src/metabase/actions/utils.ts index b052e29f9fc6225a34e60c4858f3afbda9b1ed02..1ddf786c0f08631bc52f21929f27f4097900c2b1 100644 --- a/frontend/src/metabase/actions/utils.ts +++ b/frontend/src/metabase/actions/utils.ts @@ -1,7 +1,39 @@ -import type { Database } from "metabase-types/api"; +import type { + ActionFormSettings, + Database, + FieldSettings, +} from "metabase-types/api"; export const checkDatabaseSupportsActions = (database: Database) => database.features.includes("actions"); export const checkDatabaseActionsEnabled = (database: Database) => !!database.settings?.["database-enable-actions"]; + +export const getDefaultFormSettings = ( + overrides: Partial<ActionFormSettings> = {}, +): ActionFormSettings => ({ + name: "", + type: "button", + description: "", + fields: {}, + confirmMessage: "", + ...overrides, +}); + +export const getDefaultFieldSettings = ( + overrides: Partial<FieldSettings> = {}, +): FieldSettings => ({ + id: "", + name: "", + title: "", + description: "", + placeholder: "", + order: 999, + fieldType: "string", + inputType: "string", + required: true, + hidden: false, + width: "medium", + ...overrides, +}); diff --git a/frontend/src/metabase/entities/actions/actions.ts b/frontend/src/metabase/entities/actions/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..041bbfa548d7d03df2066a0082c962fee8e50aa9 --- /dev/null +++ b/frontend/src/metabase/entities/actions/actions.ts @@ -0,0 +1,211 @@ +import { t } from "ttag"; +import { createEntity } from "metabase/lib/entities"; + +import type { + ActionFormSettings, + ImplicitQueryAction, + WritebackActionBase, + WritebackAction, +} from "metabase-types/api"; +import type { Dispatch } from "metabase-types/store"; + +import { ActionsApi } from "metabase/services"; + +import { + removeOrphanSettings, + addMissingSettings, + setParameterTypesFromFieldSettings, + setTemplateTagTypesFromFieldSettings, +} from "metabase/entities/actions/utils"; +import type Question from "metabase-lib/Question"; +import { saveForm, updateForm } from "./forms"; + +export type ActionParams = { + id?: WritebackAction["id"]; + name: WritebackAction["name"]; + type?: WritebackAction["type"]; + kind?: ImplicitQueryAction["kind"]; + description?: WritebackAction["description"]; + model_id: WritebackAction["model_id"]; + question?: Question; + formSettings?: ActionFormSettings; +}; + +interface BaseCreateActionParams { + model_id: WritebackActionBase["model_id"]; + name: WritebackActionBase["name"]; + description: WritebackActionBase["description"]; + parameters: WritebackActionBase["parameters"]; +} + +interface UpdateActionParams { + id: WritebackActionBase["id"]; +} + +export interface CreateQueryActionOptions extends BaseCreateActionParams { + question: Question; + formSettings: ActionFormSettings; +} + +export type UpdateQueryActionOptions = CreateQueryActionOptions & + UpdateActionParams; + +export interface CreateImplicitActionOptions extends BaseCreateActionParams { + kind: ImplicitQueryAction["kind"]; +} + +export type UpdateImplicitActionOptions = CreateImplicitActionOptions & + UpdateActionParams; + +function cleanUpQueryAction( + question: Question, + formSettings: ActionFormSettings, +) { + question = setTemplateTagTypesFromFieldSettings(formSettings, question); + + const parameters = setParameterTypesFromFieldSettings( + formSettings, + question.parameters(), + ); + + const visualization_settings = removeOrphanSettings( + addMissingSettings(formSettings, parameters), + parameters, + ); + + return { + dataset_query: question.datasetQuery(), + parameters, + visualization_settings, + }; +} + +function createQueryAction({ + question, + formSettings, + ...action +}: CreateQueryActionOptions) { + const { dataset_query, parameters, visualization_settings } = + cleanUpQueryAction(question, formSettings); + + return ActionsApi.create({ + ...action, + type: "query", + dataset_query, + database_id: dataset_query.database, + parameters, + visualization_settings, + }); +} + +function updateQueryAction({ + question, + formSettings, + ...action +}: UpdateQueryActionOptions) { + const { dataset_query, parameters, visualization_settings } = + cleanUpQueryAction(question, formSettings); + + return ActionsApi.update({ + ...action, + dataset_query, + parameters, + visualization_settings, + }); +} + +function createImplicitAction(action: CreateImplicitActionOptions) { + return Actions.actions.create({ + ...action, + type: "implicit", + }); +} + +function updateImplicitAction(action: UpdateImplicitActionOptions) { + return Actions.actions.update({ + ...action, + type: "implicit", + }); +} + +const defaultImplicitActionCreateOptions = { + insert: true, + update: true, + delete: true, +}; + +const enableImplicitActionsForModel = + async (modelId: number, options = defaultImplicitActionCreateOptions) => + async (dispatch: Dispatch) => { + const requests = []; + + if (options.insert) { + requests.push( + ActionsApi.create({ + name: t`Create`, + type: "implicit", + kind: "row/create", + model_id: modelId, + }), + ); + } + + if (options.update) { + requests.push( + ActionsApi.create({ + name: t`Update`, + type: "implicit", + kind: "row/update", + model_id: modelId, + }), + ); + } + + if (options.delete) { + requests.push( + ActionsApi.create({ + name: t`Delete`, + type: "implicit", + kind: "row/delete", + model_id: modelId, + }), + ); + } + + await Promise.all(requests); + + dispatch(Actions.actions.invalidateLists()); + }; + +const Actions = createEntity({ + name: "actions", + nameOne: "action", + path: "/api/action", + api: { + create: ( + params: CreateQueryActionOptions | CreateImplicitActionOptions, + ) => { + if ("question" in params) { + return createQueryAction(params); + } + return createImplicitAction(params); + }, + update: ( + params: UpdateQueryActionOptions | UpdateImplicitActionOptions, + ) => { + if ("question" in params) { + return updateQueryAction(params); + } + return updateImplicitAction(params); + }, + }, + actions: { + enableImplicitActionsForModel, + }, + forms: { + saveForm, + updateForm, + }, +}); + +export default Actions; diff --git a/frontend/src/metabase/entities/actions/constants.ts b/frontend/src/metabase/entities/actions/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..5eea11262dfa6a8df8c44546ee0bf1194b6047a9 --- /dev/null +++ b/frontend/src/metabase/entities/actions/constants.ts @@ -0,0 +1,30 @@ +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", +}; diff --git a/frontend/src/metabase/entities/actions/forms.ts b/frontend/src/metabase/entities/actions/forms.ts new file mode 100644 index 0000000000000000000000000000000000000000..bab7fceb86db9deb7a73544c1695b13f624e6885 --- /dev/null +++ b/frontend/src/metabase/entities/actions/forms.ts @@ -0,0 +1,41 @@ +import { t } from "ttag"; + +const getFormFields = (formAction: "create" | "update") => [ + { + name: "name", + title: t`Name`, + placeholder: t`My new fantastic action`, + autoFocus: true, + validate: (name: string) => + (!name && t`Name is required`) || + (name && name.length > 100 && t`Name must be 100 characters or less`), + }, + { + name: "description", + title: t`Description`, + type: "text", + placeholder: t`It's optional but oh, so helpful`, + normalize: (description: string) => description || null, // expected to be nil or non-empty string + }, + { + name: "model_id", + title: t`Model it's saved in`, + type: formAction === "create" ? "model" : "hidden", + }, + { + name: "question", + type: "hidden", + }, + { + name: "formSettings", + type: "hidden", + }, +]; + +export const saveForm = { + fields: getFormFields("create"), +}; + +export const updateForm = { + fields: getFormFields("update"), +}; diff --git a/frontend/src/metabase/entities/actions/index.ts b/frontend/src/metabase/entities/actions/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6d262f57b87efc344fab769a520242d6ec197cc --- /dev/null +++ b/frontend/src/metabase/entities/actions/index.ts @@ -0,0 +1,2 @@ +export { default } from "./actions"; +export * from "./actions"; diff --git a/frontend/src/metabase/entities/actions/utils.ts b/frontend/src/metabase/entities/actions/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..e705dfcddaecfb17f4d28076c46dd91be1ceaadb --- /dev/null +++ b/frontend/src/metabase/entities/actions/utils.ts @@ -0,0 +1,107 @@ +import _ from "underscore"; + +import { getDefaultFieldSettings } from "metabase/actions/utils"; + +import type { + ActionFormSettings, + FieldType, + InputSettingType, + Parameter, + ParameterType, +} from "metabase-types/api"; +import type { TemplateTag, TemplateTagType } from "metabase-types/types/Query"; +import type NativeQuery from "metabase-lib/queries/NativeQuery"; +import type Question from "metabase-lib/Question"; + +import { + fieldTypeToParameterTypeMap, + dateTypeToParameterTypeMap, + fieldTypeToTagTypeMap, +} from "./constants"; + +export const removeOrphanSettings = ( + settings: ActionFormSettings, + parameters: Parameter[], +): ActionFormSettings => { + const parameterIds = parameters.map(p => p.id); + return { + ...settings, + fields: _.pick(settings.fields, parameterIds), + }; +}; + +export const addMissingSettings = ( + settings: ActionFormSettings, + parameters: Parameter[], +): ActionFormSettings => { + const parameterIds = parameters.map(p => p.id); + const fieldIds = Object.keys(settings.fields); + const missingIds = _.difference(parameterIds, fieldIds); + + if (!missingIds.length) { + return settings; + } + + return { + ...settings, + fields: { + ...settings.fields, + ...Object.fromEntries( + missingIds.map(id => [id, getDefaultFieldSettings({ id })]), + ), + }, + }; +}; + +const getParameterTypeFromFieldSettings = ( + fieldType: FieldType, + inputType: InputSettingType, +): 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: Parameter[], +): Parameter[] => { + 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 ?? "string", + ), + }), + ); + }); + + return question; +}; diff --git a/frontend/src/metabase/entities/actions/utils.unit.spec.ts b/frontend/src/metabase/entities/actions/utils.unit.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..eda385c1f80e827fce9535f7de23a75cfb77a711 --- /dev/null +++ b/frontend/src/metabase/entities/actions/utils.unit.spec.ts @@ -0,0 +1,214 @@ +import { + getDefaultFormSettings, + getDefaultFieldSettings, +} from "metabase/actions/utils"; + +import type { Parameter } from "metabase-types/api"; +import type { NativeDatasetQuery } from "metabase-types/types/Card"; +import type { TemplateTagType } from "metabase-types/types/Query"; +import { getUnsavedNativeQuestion } from "metabase-lib/mocks"; + +import { + removeOrphanSettings, + setParameterTypesFromFieldSettings, + setTemplateTagTypesFromFieldSettings, +} from "./utils"; + +const createQuestionWithTemplateTags = (tagType: TemplateTagType) => + getUnsavedNativeQuestion({ + dataset_query: { + type: "native", + database: 1, + native: { + query: + "INSERT INTO products (name, price) VALUES ({{name}}, {{price}});", + "template-tags": { + name: { + id: "aaa", + name: "name", + "display-name": "Name", + type: tagType, + }, + price: { + id: "bbb", + name: "price", + "display-name": "Price", + type: tagType, + }, + }, + }, + }, + }); + +describe("entities > actions > utils", () => { + 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 Parameter[]; + + 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 Parameter[]; + + 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"); + }); + }); + + 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 Parameter[]; + + 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 Parameter[]; + + 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 Parameter[]; + + 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 = createQuestionWithTemplateTags("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 = createQuestionWithTemplateTags("number"); + + const formSettings = getDefaultFormSettings({ + name: "test form", + fields: { + aaa: getDefaultFieldSettings({ fieldType: "date" }), + bbb: getDefaultFieldSettings({ fieldType: "date" }), + }, + }); + + const newQuestion = setTemplateTagTypesFromFieldSettings( + formSettings, + question, + ); + + const tags = (newQuestion.card().dataset_query as NativeDatasetQuery) + .native["template-tags"]; + + expect(tags.name.type).toEqual("date"); + expect(tags.price.type).toEqual("date"); + }); + }); +}); diff --git a/frontend/src/metabase/entities/index.js b/frontend/src/metabase/entities/index.js index d536e5ce7b65df2e6ac4e3babce3c82780337146..710b3b14cbb0df26cb842ed4e0c2d6cfab396a82 100644 --- a/frontend/src/metabase/entities/index.js +++ b/frontend/src/metabase/entities/index.js @@ -1,3 +1,4 @@ +export { default as actions } from "./actions"; export { default as alerts } from "./alerts"; export { default as collections } from "./collections"; export { default as snippetCollections } from "./snippet-collections"; diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js index cb25bf004e91c44f1da63e7a58cc4673d6d28b32..7d20d3788f4452bdd09c53c3cceba300543e4493 100644 --- a/frontend/src/metabase/services.js +++ b/frontend/src/metabase/services.js @@ -465,3 +465,14 @@ function setParamsEndpoints(prefix) { prefix + "/dashboard/:dashId/params/:paramId/search/:query", ); } + +export const ActionsApi = { + list: GET("/api/action"), + get: GET("/api/action/:id"), + create: POST("/api/action"), + update: PUT("/api/action/:id"), + prefetchValues: GET( + "/api/dashboard/:dashboardId/dashcard/:dashcardId/execute", + ), + execute: POST("/api/dashboard/:dashboardId/dashcard/:dashcardId/execute"), +};