diff --git a/frontend/src/metabase/actions/components/ActionForm/ActionForm.tsx b/frontend/src/metabase/actions/components/ActionForm/ActionForm.tsx index 323860325ccf6b95c50097250a2911ed0f6f8a67..c9875ab1e3347117488a3617cd06272ccecc098b 100644 --- a/frontend/src/metabase/actions/components/ActionForm/ActionForm.tsx +++ b/frontend/src/metabase/actions/components/ActionForm/ActionForm.tsx @@ -24,7 +24,6 @@ import type { ParametersForActionExecution, } from "metabase-types/api"; -import { reorderFields } from "metabase/actions/containers/ActionCreator/FormCreator"; import { FormFieldWidget } from "./ActionFormFieldWidget"; import FormFieldEditor from "./FormFieldEditor"; import { @@ -32,7 +31,7 @@ import { FormFieldEditorDragContainer, } from "./ActionForm.styled"; -import { getForm, getFormValidationSchema } from "./utils"; +import { getForm, getFormValidationSchema, reorderFields } from "./utils"; export interface ActionFormComponentProps { parameters: WritebackParameter[] | Parameter[]; diff --git a/frontend/src/metabase/actions/components/ActionForm/FormFieldEditor.tsx b/frontend/src/metabase/actions/components/ActionForm/FormFieldEditor.tsx index fac992170ce4e55765670e7bdb44983602857e5a..4d9a47cbcfcadc8de9cfe10f0249192ed62046aa 100644 --- a/frontend/src/metabase/actions/components/ActionForm/FormFieldEditor.tsx +++ b/frontend/src/metabase/actions/components/ActionForm/FormFieldEditor.tsx @@ -4,18 +4,16 @@ import { t } from "ttag"; import Radio from "metabase/core/components/Radio"; import { isNotNull } from "metabase/core/utils/types"; -import type { ActionFormFieldProps } from "metabase/actions/types"; +import { getFieldTypes, getInputTypes } from "metabase/actions/constants"; + import type { FieldSettings, FieldType, FieldValueOptions, } from "metabase-types/api"; +import type { ActionFormFieldProps } from "metabase/actions/types"; import { FieldSettingsButtons } from "../../containers/ActionCreator/FormCreator/FieldSettingsButtons"; -import { - getFieldTypes, - getInputTypes, -} from "../../containers/ActionCreator/FormCreator/constants"; import { inputTypeHasOptions } from "./utils"; import { FormFieldWidget } from "./ActionFormFieldWidget"; diff --git a/frontend/src/metabase/actions/components/ActionForm/utils.ts b/frontend/src/metabase/actions/components/ActionForm/utils.ts index a98f1c6cef39f6dd7240a95c92737e71ed5ad23d..d8c36b659a4240f79789c80237a15f9aad2406ed 100644 --- a/frontend/src/metabase/actions/components/ActionForm/utils.ts +++ b/frontend/src/metabase/actions/components/ActionForm/utils.ts @@ -1,29 +1,29 @@ +import _ from "underscore"; import { t } from "ttag"; import * as Yup from "yup"; +import { moveElement } from "metabase/core/utils/arrays"; import * as Errors from "metabase/core/utils/errors"; +import { sortActionParams } from "metabase/actions/utils"; import type { ActionFormSettings, ActionFormOption, + FieldType, FieldSettingsMap, InputSettingType, InputComponentType, Parameter, WritebackParameter, - FieldType, } from "metabase-types/api"; import type { ActionFormProps, ActionFormFieldProps, FieldSettings, } from "metabase/actions/types"; +import type Field from "metabase-lib/metadata/Field"; -import { sortActionParams, isEditableField } from "metabase/actions/utils"; - -const getOptionsFromArray = ( - options: (number | string)[], -): ActionFormOption[] => options.map(o => ({ name: o, value: o })); +import { TYPE } from "metabase-lib/types/constants"; export const inputTypeHasOptions = (inputType: InputSettingType) => ["select", "radio"].includes(inputType); @@ -42,16 +42,59 @@ const fieldPropsTypeMap: FieldPropTypeMap = { radio: "radio", }; +const getOptionsFromArray = ( + options: (number | string)[], +): ActionFormOption[] => options.map(o => ({ name: o, value: o })); + function getSampleOptions(fieldType: FieldType) { return fieldType === "number" ? getOptionsFromArray([1, 2, 3]) : getOptionsFromArray([t`Option One`, t`Option Two`, t`Option Three`]); } -export const getFormField = ( - parameter: Parameter, - fieldSettings: FieldSettings, -) => { +const AUTOMATIC_DATE_TIME_FIELDS = [ + TYPE.CreationDate, + TYPE.CreationTemporal, + TYPE.CreationTime, + TYPE.CreationTimestamp, + + TYPE.DeletionDate, + TYPE.DeletionTemporal, + TYPE.DeletionTime, + TYPE.DeletionTimestamp, + + TYPE.UpdatedDate, + TYPE.UpdatedTemporal, + TYPE.UpdatedTime, + TYPE.UpdatedTimestamp, +]; + +const isAutomaticDateTimeField = (field: Field) => { + return AUTOMATIC_DATE_TIME_FIELDS.includes(field.semantic_type); +}; + +const isEditableField = (field: Field, parameter: Parameter) => { + const isRealField = typeof field.id === "number"; + if (!isRealField) { + // Filters out custom, aggregated columns, etc. + return false; + } + + if (field.isPK()) { + // Most of the time PKs are auto-generated, + // but there are rare cases when they're not + // In this case they're marked as `required` + return parameter.required; + } + + if (isAutomaticDateTimeField(field)) { + return parameter.required; + } + + return true; +}; + +const getFormField = (parameter: Parameter, fieldSettings: FieldSettings) => { if ( fieldSettings.field && !isEditableField(fieldSettings.field, parameter as Parameter) @@ -143,3 +186,27 @@ export const getFormValidationSchema = ( }); return Yup.object(Object.fromEntries(schema)); }; + +export const reorderFields = ( + fields: FieldSettingsMap, + oldIndex: number, + newIndex: number, +) => { + // we have to jump through some hoops here because fields settings are an unordered map + // with order properties + const fieldsWithIds = _.mapObject(fields, (field, key) => ({ + ...field, + id: key, + })); + const orderedFields = _.sortBy(Object.values(fieldsWithIds), "order"); + const reorderedFields = moveElement(orderedFields, oldIndex, newIndex); + + const fieldsWithUpdatedOrderProperty = reorderedFields.map( + (field, index) => ({ + ...field, + order: index, + }), + ); + + return _.indexBy(fieldsWithUpdatedOrderProperty, "id"); +}; diff --git a/frontend/src/metabase/actions/components/ActionForm/utils.unit.spec.ts b/frontend/src/metabase/actions/components/ActionForm/utils.unit.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f8056195315ed09f81e106e01eaf093bb8dedaa0 --- /dev/null +++ b/frontend/src/metabase/actions/components/ActionForm/utils.unit.spec.ts @@ -0,0 +1,18 @@ +import { getDefaultFieldSettings } from "metabase/actions/utils"; +import type { FieldSettingsMap } from "metabase-types/api"; +import { reorderFields } from "./utils"; + +describe("reorderFields", () => { + it("should reorder fields", () => { + const fields = { + a: getDefaultFieldSettings({ order: 0 }), + b: getDefaultFieldSettings({ order: 1 }), + c: getDefaultFieldSettings({ order: 2 }), + } as FieldSettingsMap; + // move b to index 0 + const reorderedFields = reorderFields(fields, 1, 0); + expect(reorderedFields.a.order).toEqual(1); + expect(reorderedFields.b.order).toEqual(0); + expect(reorderedFields.c.order).toEqual(2); + }); +}); diff --git a/frontend/src/metabase/actions/components/ActionViz/Action.tsx b/frontend/src/metabase/actions/components/ActionViz/Action.tsx index b56099cf7d08ff28ff7803ba5fcb70108da149d5..adf1cf0bfa592a0331caddb3c1c40c24764efe04 100644 --- a/frontend/src/metabase/actions/components/ActionViz/Action.tsx +++ b/frontend/src/metabase/actions/components/ActionViz/Action.tsx @@ -3,30 +3,27 @@ import _ from "underscore"; import { t } from "ttag"; import { connect } from "react-redux"; -import { executeRowAction } from "metabase/dashboard/actions"; - import Tooltip from "metabase/core/components/Tooltip"; - import { getResponseErrorMessage } from "metabase/core/utils/errors"; +import Databases from "metabase/entities/databases"; + +import { + generateFieldSettingsFromParameters, + setNumericValues, +} from "metabase/actions/utils"; +import { executeRowAction } from "metabase/dashboard/actions"; +import { getEditingDashcardId } from "metabase/dashboard/selectors"; + import type { ActionDashboardCard, ParametersForActionExecution, WritebackQueryAction, Dashboard, } from "metabase-types/api"; - import type { VisualizationProps } from "metabase-types/types/Visualization"; -import type { Dispatch, State } from "metabase-types/store"; import type { ParameterValueOrArray } from "metabase-types/types/Parameter"; - -import { - generateFieldSettingsFromParameters, - setNumericValues, -} from "metabase/actions/utils"; - -import { getEditingDashcardId } from "metabase/dashboard/selectors"; -import Databases from "metabase/entities/databases"; +import type { Dispatch, State } from "metabase-types/store"; import type Database from "metabase-lib/metadata/Database"; @@ -35,6 +32,7 @@ import { getNotProvidedActionParameters, shouldShowConfirmation, } from "./utils"; + import ActionVizForm from "./ActionVizForm"; import ActionButtonView from "./ActionButtonView"; import { FullContainer } from "./ActionButton.styled"; diff --git a/frontend/src/metabase/actions/components/ActionViz/ActionButtonView.tsx b/frontend/src/metabase/actions/components/ActionViz/ActionButtonView.tsx index 8b965bd94d97b8072d18dbb7418c5c3bdc5c1176..917b032e76bdcc494130d1d18e5f4e612d24d017 100644 --- a/frontend/src/metabase/actions/components/ActionViz/ActionButtonView.tsx +++ b/frontend/src/metabase/actions/components/ActionViz/ActionButtonView.tsx @@ -1,10 +1,11 @@ import React from "react"; import { t } from "ttag"; +import Ellipsified from "metabase/core/components/Ellipsified"; +import Icon from "metabase/components/Icon"; + import type { VisualizationProps } from "metabase-types/types/Visualization"; -import Icon from "metabase/components/Icon"; -import Ellipsified from "metabase/core/components/Ellipsified"; import { StyledButton, StyledButtonContent } from "./ActionButton.styled"; interface ActionButtonViewProps extends Pick<VisualizationProps, "settings"> { diff --git a/frontend/src/metabase/actions/components/ActionViz/ActionParameterMapper.tsx b/frontend/src/metabase/actions/components/ActionViz/ActionParameterMapper.tsx index 8051feb0174890b6c55943cf843cbeea7cfb003e..f7fe6ef731733134ca3cae2e6c34ecba3d2a852e 100644 --- a/frontend/src/metabase/actions/components/ActionViz/ActionParameterMapper.tsx +++ b/frontend/src/metabase/actions/components/ActionViz/ActionParameterMapper.tsx @@ -2,10 +2,10 @@ import React, { useCallback } from "react"; import { t } from "ttag"; import { connect } from "react-redux"; -import Select from "metabase/core/components/Select"; +import Select, { SelectChangeEvent } from "metabase/core/components/Select"; +import EmptyState from "metabase/components/EmptyState"; import { setParameterMapping } from "metabase/dashboard/actions"; -import type { SelectChangeEvent } from "metabase/core/components/Select"; import type { ActionDashboardCard, @@ -14,12 +14,10 @@ import type { WritebackAction, Dashboard, } from "metabase-types/api"; - import type { ParameterTarget, ParameterId, } from "metabase-types/types/Parameter"; -import EmptyState from "metabase/components/EmptyState"; import type Question from "metabase-lib/Question"; import { diff --git a/frontend/src/metabase/actions/components/ActionViz/ActionVizForm.tsx b/frontend/src/metabase/actions/components/ActionViz/ActionVizForm.tsx index 41bcfc14db279ac01ebeeb488b216baea676c540..986b9517fa040913e2b616b2164856c54e8f4816 100644 --- a/frontend/src/metabase/actions/components/ActionViz/ActionVizForm.tsx +++ b/frontend/src/metabase/actions/components/ActionViz/ActionVizForm.tsx @@ -1,5 +1,7 @@ import React, { useState } from "react"; +import { getFormTitle } from "metabase/actions/utils"; + import type { WritebackQueryAction, WritebackParameter, @@ -9,7 +11,6 @@ import type { VisualizationSettings, Dashboard, } from "metabase-types/api"; -import { getFormTitle } from "metabase/actions/utils"; import ActionParametersInputForm, { ActionParametersInputModal, diff --git a/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/constants.ts b/frontend/src/metabase/actions/constants.ts similarity index 87% rename from frontend/src/metabase/actions/containers/ActionCreator/FormCreator/constants.ts rename to frontend/src/metabase/actions/constants.ts index d58598b119532a77d05195fbd0395cdd773110ed..e5f971314f78e9c8c6b638539394e95101463db5 100644 --- a/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/constants.ts +++ b/frontend/src/metabase/actions/constants.ts @@ -6,6 +6,13 @@ interface FieldOptionType { name: string; } +interface InputOptionType { + value: InputSettingType; + name: string; +} + +type InputOptionsMap = Record<FieldType, InputOptionType[]>; + export const getFieldTypes = (): FieldOptionType[] => [ { value: "string", @@ -21,13 +28,6 @@ export const getFieldTypes = (): FieldOptionType[] => [ }, ]; -interface InputOptionType { - value: InputSettingType; - name: string; -} - -type InputOptionsMap = Record<FieldType, InputOptionType[]>; - const getTextInputs = (): InputOptionType[] => [ { value: "string", @@ -68,13 +68,5 @@ export const getInputTypes = (): InputOptionsMap => ({ value: "datetime", name: t`Date + time`, }, - // { - // value: "monthyear", - // name: t`month + year`, - // }, - // { - // value: "quarteryear", - // name: t`quarter + year`, - // }, ], }); diff --git a/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider.tsx b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx similarity index 60% rename from frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider.tsx rename to frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx index 0128814acfcdc8071ccd1766d1411e9cb74e6ca0..46589cc9acf7216b9b57213635458e2ec65df83d 100644 --- a/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider.tsx +++ b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx @@ -1,23 +1,32 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { CreateQueryActionParams } from "metabase/entities/actions"; -import QueryActionEditor from "metabase/actions/containers/ActionCreator/QueryActionEditor"; -import type { DatabaseId, WritebackQueryAction } from "metabase-types/api"; +import type { + ActionFormSettings, + DatabaseId, + NativeDatasetQuery, + VisualizationSettings, + WritebackParameter, + WritebackQueryAction, +} from "metabase-types/api"; +import type { Card as LegacyCard } from "metabase-types/types/Card"; import type Metadata from "metabase-lib/metadata/Metadata"; import type NativeQuery from "metabase-lib/queries/NativeQuery"; +import Question from "metabase-lib/Question"; import { getTemplateTagParametersFromCard } from "metabase-lib/parameters/utils/template-tags"; -import { getDefaultFormSettings } from "../../../utils"; -import { - newQuestion, - convertActionToQuestion, - convertQuestionToAction, -} from "../utils"; +import { getDefaultFormSettings } from "../../../../utils"; + +import { ActionContext } from "../ActionContext"; +import type { ActionContextProviderProps, EditorBodyProps } from "../types"; -import { ActionContext } from "./ActionContext"; -import type { ActionContextProviderProps, EditorBodyProps } from "./types"; +import { + setParameterTypesFromFieldSettings, + setTemplateTagTypesFromFieldSettings, +} from "./utils"; +import QueryActionEditor from "./QueryActionEditor"; export interface QueryActionContextProviderProps extends ActionContextProviderProps<WritebackQueryAction> { @@ -25,6 +34,70 @@ export interface QueryActionContextProviderProps databaseId?: DatabaseId; } +// ActionCreator uses the NativeQueryEditor, which expects a Question object +// This utilities help us to work with the WritebackQueryAction as with a Question + +function newQuestion(metadata: Metadata, databaseId?: DatabaseId) { + return new Question( + { + dataset_query: { + type: "native", + database: databaseId ?? null, + native: { + query: "", + }, + }, + }, + metadata, + ); +} + +function convertActionToQuestionCard( + action: WritebackQueryAction, +): LegacyCard<NativeDatasetQuery> { + return { + id: action.id, + name: action.name, + description: action.description, + dataset_query: action.dataset_query as NativeDatasetQuery, + display: "action", + visualization_settings: + action.visualization_settings as VisualizationSettings, + }; +} + +function convertActionToQuestion( + action: WritebackQueryAction, + metadata: Metadata, +) { + const question = new Question(convertActionToQuestionCard(action), metadata); + return question.setParameters(action.parameters); +} + +function convertQuestionToAction( + question: Question, + formSettings: ActionFormSettings, +) { + const cleanQuestion = setTemplateTagTypesFromFieldSettings( + question, + formSettings, + ); + const parameters = setParameterTypesFromFieldSettings( + formSettings, + cleanQuestion.parameters(), + ); + + return { + id: question.id(), + name: question.displayName() as string, + description: question.description(), + dataset_query: question.datasetQuery() as NativeDatasetQuery, + database_id: question.databaseId() as DatabaseId, + parameters: parameters as WritebackParameter[], + visualization_settings: formSettings, + }; +} + function resolveQuestion( action: WritebackQueryAction | undefined, { metadata, databaseId }: { metadata: Metadata; databaseId?: DatabaseId }, diff --git a/frontend/src/metabase/actions/containers/ActionCreator/QueryActionEditor.tsx b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionEditor.tsx similarity index 100% rename from frontend/src/metabase/actions/containers/ActionCreator/QueryActionEditor.tsx rename to frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionEditor.tsx diff --git a/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/index.ts b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4456dbd9eaa570b48fec1d63f66647260c57208b --- /dev/null +++ b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/index.ts @@ -0,0 +1,3 @@ +export { default } from "./QueryActionContextProvider"; +export * from "./QueryActionContextProvider"; +export { ACE_ELEMENT_ID } from "./QueryActionEditor"; diff --git a/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/utils.ts b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ebc38c38e2003d98324a71c2e7f4db93135bdbb --- /dev/null +++ b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/utils.ts @@ -0,0 +1,86 @@ +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 Question from "metabase-lib/Question"; + +type FieldTypeMap = Record<string, ParameterType>; +type TagTypeMap = Record<string, TemplateTagType>; + +const fieldTypeToParameterTypeMap: FieldTypeMap = { + string: "string/=", + number: "number/=", +}; + +const dateTypeToParameterTypeMap: FieldTypeMap = { + date: "date/single", + datetime: "date/single", + monthyear: "date/month-year", + quarteryear: "date/quarter-year", +}; + +const fieldTypeToTagTypeMap: TagTypeMap = { + string: "text", + number: "number", + date: "date", +}; + +const getTagTypeFromFieldSettings = (fieldType: FieldType): TemplateTagType => { + return fieldTypeToTagTypeMap[fieldType] ?? "text"; +}; + +const getParameterTypeFromFieldSettings = ( + fieldType: FieldType, + inputType: InputSettingType, +): ParameterType => { + if (fieldType === "date") { + return dateTypeToParameterTypeMap[inputType] ?? "date/single"; + } + + return fieldTypeToParameterTypeMap[fieldType] ?? "string/="; +}; + +export const setTemplateTagTypesFromFieldSettings = ( + question: Question, + settings: ActionFormSettings, +): Question => { + const fields = settings.fields || {}; + const query = question.query() as NativeQuery; + let tempQuestion = question.clone(); + + query.variableTemplateTags().forEach((tag: TemplateTag) => { + const currentQuery = tempQuestion.query() as NativeQuery; + const fieldType = fields[tag.id]?.fieldType ?? "string"; + const nextTag = { + ...tag, + type: getTagTypeFromFieldSettings(fieldType), + }; + tempQuestion = tempQuestion.setQuery( + currentQuery.setTemplateTag(tag.name, nextTag), + ); + }); + + return tempQuestion; +}; + +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/=", + }; + }); +}; diff --git a/frontend/src/metabase/actions/containers/ActionCreator/tests/utils.unit.spec.ts b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/utils.unit.spec.ts similarity index 97% rename from frontend/src/metabase/actions/containers/ActionCreator/tests/utils.unit.spec.ts rename to frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/utils.unit.spec.ts index e5c5e51a424bb232ac0e09c76a72b4e3dd99b381..e8195de2f9650f98a9431542d2038e357a9c90d4 100644 --- a/frontend/src/metabase/actions/containers/ActionCreator/tests/utils.unit.spec.ts +++ b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/utils.unit.spec.ts @@ -11,7 +11,7 @@ import { getUnsavedNativeQuestion } from "metabase-lib/mocks"; import { setParameterTypesFromFieldSettings, setTemplateTagTypesFromFieldSettings, -} from "../utils"; +} from "./utils"; const createQuestionWithTemplateTags = (tagType: TemplateTagType) => getUnsavedNativeQuestion({ @@ -39,7 +39,7 @@ const createQuestionWithTemplateTags = (tagType: TemplateTagType) => }, }); -describe("entities > actions > utils", () => { +describe("actions > containers > ActionCreator > QueryActionContextProvider > utils", () => { describe("setParameterTypesFromFieldSettings", () => { it("should set string parameter types", () => { const formSettings = getDefaultFormSettings({ diff --git a/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx b/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx index 0b9f5aaffba39300227db05ebc7ba8690b5e9072..b9bb2464420b59496a36b6759ea7c87a5d07ecef 100644 --- a/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx +++ b/frontend/src/metabase/actions/containers/ActionCreator/ActionCreator.tsx @@ -28,8 +28,8 @@ import type Metadata from "metabase-lib/metadata/Metadata"; import { isSavedAction } from "../../utils"; import ActionContext, { useActionContext } from "./ActionContext"; +import { ACE_ELEMENT_ID } from "./ActionContext/QueryActionContextProvider"; import ActionCreatorView from "./ActionCreatorView"; -import { ACE_ELEMENT_ID } from "./QueryActionEditor"; import CreateActionForm, { FormValues as CreateActionFormValues, } from "./CreateActionForm"; diff --git a/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/FieldSettingsPopover.tsx b/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/FieldSettingsPopover.tsx index 188ac206a5fcb2f5d8fe26e61805a668def632a1..3005e590a221023a44f84b5f7c848f11779cc731 100644 --- a/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/FieldSettingsPopover.tsx +++ b/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/FieldSettingsPopover.tsx @@ -1,19 +1,20 @@ import React, { ChangeEvent, useMemo } from "react"; import { t } from "ttag"; -import type { - FieldSettings, - FieldType, - InputSettingType, -} from "metabase-types/api"; - import Input from "metabase/core/components/Input"; import Radio from "metabase/core/components/Radio"; import Toggle from "metabase/core/components/Toggle"; import TippyPopoverWithTrigger from "metabase/components/PopoverWithTrigger/TippyPopoverWithTrigger"; + import { useUniqueId } from "metabase/hooks/use-unique-id"; +import { getInputTypes } from "metabase/actions/constants"; + +import type { + FieldSettings, + FieldType, + InputSettingType, +} from "metabase-types/api"; -import { getInputTypes } from "./constants"; import { getDefaultValueInputType } from "./utils"; import { SettingsTriggerIcon, diff --git a/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/FormCreator.tsx b/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/FormCreator.tsx index 7c9422d1b7e9b07186cd14dc89760c6720726cc3..198e4ae2454f66da123cd0b7fc1898ea86429be3 100644 --- a/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/FormCreator.tsx +++ b/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/FormCreator.tsx @@ -3,11 +3,12 @@ import { jt, t } from "ttag"; import _ from "underscore"; import ExternalLink from "metabase/core/components/ExternalLink"; -import { ActionForm } from "metabase/actions/components/ActionForm"; -import SidebarContent from "metabase/query_builder/components/SidebarContent"; import MetabaseSettings from "metabase/lib/settings"; +import { ActionForm } from "metabase/actions/components/ActionForm"; +import SidebarContent from "metabase/query_builder/components/SidebarContent"; + import type { ActionFormSettings, Parameter } from "metabase-types/api"; import { getDefaultFormSettings, sortActionParams } from "../../../utils"; diff --git a/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/utils.ts b/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/utils.ts index 6b296eee3ad2f03acec6642eb8b7775eec31953e..a169f93165ba7a4c6429b34d975ed1e47a0e7950 100644 --- a/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/utils.ts +++ b/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/utils.ts @@ -1,64 +1,4 @@ -import { t } from "ttag"; -import _ from "underscore"; -import { moveElement } from "metabase/core/utils/arrays"; -import { - FieldSettingsMap, - InputSettingType, - WritebackAction, -} from "metabase-types/api"; - -export const getSubmitButtonColor = (action: WritebackAction): string => { - if (action.type === "implicit" && action.kind === "row/delete") { - return "danger"; - } - return action.visualization_settings?.submitButtonColor ?? "primary"; -}; - -export const getSubmitButtonLabel = (action: WritebackAction): string => { - if (action.visualization_settings?.submitButtonLabel) { - return action.visualization_settings.submitButtonLabel; - } - - if (action.type === "implicit") { - if (action.kind === "row/delete") { - return t`Delete`; - } - - if (action.kind === "row/update") { - return t`Update`; - } - - if (action.kind === "row/create") { - return t`Save`; - } - } - - return t`Run`; -}; - -export const reorderFields = ( - fields: FieldSettingsMap, - oldIndex: number, - newIndex: number, -) => { - // we have to jump through some hoops here because fields settings are an unordered map - // with order properties - const fieldsWithIds = _.mapObject(fields, (field, key) => ({ - ...field, - id: key, - })); - const orderedFields = _.sortBy(Object.values(fieldsWithIds), "order"); - const reorderedFields = moveElement(orderedFields, oldIndex, newIndex); - - const fieldsWithUpdatedOrderProperty = reorderedFields.map( - (field, index) => ({ - ...field, - order: index, - }), - ); - - return _.indexBy(fieldsWithUpdatedOrderProperty, "id"); -}; +import { InputSettingType } from "metabase-types/api"; const inputTypeMap: Record<InputSettingType, string> = { string: "text", diff --git a/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/utils.unit.spec.ts b/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/utils.unit.spec.ts deleted file mode 100644 index a76dfeb9fa3642e8ffbd97269bc9b44ddad9ff62..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/actions/containers/ActionCreator/FormCreator/utils.unit.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { FieldSettingsMap } from "metabase-types/api"; - -import { getDefaultFieldSettings } from "../../../utils"; -import { reorderFields } from "./utils"; - -describe("actions > ActionCreator > FormCreator > utils", () => { - describe("reorderFields", () => { - it("should reorder fields", () => { - const fields = { - a: getDefaultFieldSettings({ order: 0 }), - b: getDefaultFieldSettings({ order: 1 }), - c: getDefaultFieldSettings({ order: 2 }), - } as FieldSettingsMap; - // move b to index 0 - const reorderedFields = reorderFields(fields, 1, 0); - expect(reorderedFields.a.order).toEqual(1); - expect(reorderedFields.b.order).toEqual(0); - expect(reorderedFields.c.order).toEqual(2); - }); - }); -}); diff --git a/frontend/src/metabase/actions/containers/ActionCreator/utils.ts b/frontend/src/metabase/actions/containers/ActionCreator/utils.ts index 443b71c160d0514ab5eb6c0505d526e750d8b579..453fe4a753c43b7c21efffc1dc7633c4448237a0 100644 --- a/frontend/src/metabase/actions/containers/ActionCreator/utils.ts +++ b/frontend/src/metabase/actions/containers/ActionCreator/utils.ts @@ -2,118 +2,7 @@ import _ from "underscore"; import { getDefaultFieldSettings } from "metabase/actions/utils"; -import type { - ActionFormSettings, - DatabaseId, - FieldType, - InputSettingType, - NativeDatasetQuery, - Parameter, - ParameterType, - VisualizationSettings, - WritebackParameter, - WritebackQueryAction, -} from "metabase-types/api"; -import type { Card as LegacyCard } from "metabase-types/types/Card"; -import type { TemplateTag, TemplateTagType } from "metabase-types/types/Query"; - -import type Metadata from "metabase-lib/metadata/Metadata"; -import type NativeQuery from "metabase-lib/queries/NativeQuery"; -import Question from "metabase-lib/Question"; - -type FieldTypeMap = Record<string, ParameterType>; - -type TagTypeMap = Record<string, TemplateTagType>; - -const fieldTypeToParameterTypeMap: FieldTypeMap = { - string: "string/=", - number: "number/=", -}; - -const dateTypeToParameterTypeMap: FieldTypeMap = { - date: "date/single", - datetime: "date/single", - monthyear: "date/month-year", - quarteryear: "date/quarter-year", -}; - -const fieldTypeToTagTypeMap: TagTypeMap = { - string: "text", - number: "number", - date: "date", -}; - -// ActionCreator uses the NativeQueryEditor, which expects a Question object -// This utilities help us to work with the WritebackQueryAction as with a Question - -export const newQuestion = (metadata: Metadata, databaseId?: number) => { - return new Question( - { - dataset_query: { - type: "native", - database: databaseId ?? null, - native: { - query: "", - }, - }, - }, - metadata, - ); -}; - -const getTagTypeFromFieldSettings = (fieldType: FieldType): TemplateTagType => { - return fieldTypeToTagTypeMap[fieldType] ?? "text"; -}; - -export const setTemplateTagTypesFromFieldSettings = ( - question: Question, - settings: ActionFormSettings, -): Question => { - const fields = settings.fields || {}; - const query = question.query() as NativeQuery; - let tempQuestion = question.clone(); - - query.variableTemplateTags().forEach((tag: TemplateTag) => { - const currentQuery = tempQuestion.query() as NativeQuery; - const fieldType = fields[tag.id]?.fieldType ?? "string"; - const nextTag = { - ...tag, - type: getTagTypeFromFieldSettings(fieldType), - }; - tempQuestion = tempQuestion.setQuery( - currentQuery.setTemplateTag(tag.name, nextTag), - ); - }); - - return tempQuestion; -}; - -const getParameterTypeFromFieldSettings = ( - fieldType: FieldType, - inputType: InputSettingType, -): ParameterType => { - if (fieldType === "date") { - return dateTypeToParameterTypeMap[inputType] ?? "date/single"; - } - - return fieldTypeToParameterTypeMap[fieldType] ?? "string/="; -}; - -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/=", - }; - }); -}; +import type { ActionFormSettings, Parameter } from "metabase-types/api"; export const syncFieldsWithParameters = ( settings: ActionFormSettings, @@ -138,49 +27,3 @@ export const syncFieldsWithParameters = ( }, }; }; - -export const convertQuestionToAction = ( - question: Question, - formSettings: ActionFormSettings, -) => { - const cleanQuestion = setTemplateTagTypesFromFieldSettings( - question, - formSettings, - ); - const parameters = setParameterTypesFromFieldSettings( - formSettings, - cleanQuestion.parameters(), - ); - - return { - id: question.id(), - name: question.displayName() as string, - description: question.description(), - dataset_query: question.datasetQuery() as NativeDatasetQuery, - database_id: question.databaseId() as DatabaseId, - parameters: parameters as WritebackParameter[], - visualization_settings: formSettings, - }; -}; - -const convertActionToQuestionCard = ( - action: WritebackQueryAction, -): LegacyCard<NativeDatasetQuery> => { - return { - id: action.id, - name: action.name, - description: action.description, - dataset_query: action.dataset_query as NativeDatasetQuery, - display: "action", - visualization_settings: - action.visualization_settings as VisualizationSettings, - }; -}; - -export const convertActionToQuestion = ( - action: WritebackQueryAction, - metadata: Metadata, -) => { - const question = new Question(convertActionToQuestionCard(action), metadata); - return question.setParameters(action.parameters); -}; diff --git a/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputForm.tsx b/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputForm.tsx index b1105fa643550380fbb9bb8054c65d1720db72d1..a7f37f89a5fb3c63c4cf9d20955d8d98ba1f3ace 100644 --- a/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputForm.tsx +++ b/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputForm.tsx @@ -1,12 +1,17 @@ import React, { useCallback, useMemo, useState, useEffect } from "react"; import { t } from "ttag"; -import { ActionForm } from "metabase/actions/components/ActionForm"; +import EmptyState from "metabase/components/EmptyState"; + +import { ActionsApi, PublicApi } from "metabase/services"; + +import { ActionForm } from "metabase/actions/components/ActionForm"; import { + generateFieldSettingsFromParameters, getSubmitButtonColor, getSubmitButtonLabel, -} from "metabase/actions/containers/ActionCreator/FormCreator"; -import EmptyState from "metabase/components/EmptyState"; +} from "metabase/actions/utils"; +import { getDashboardType } from "metabase/dashboard/utils"; import type { WritebackParameter, @@ -17,19 +22,11 @@ import type { ActionFormSettings, WritebackAction, } from "metabase-types/api"; - -import { ActionsApi, PublicApi } from "metabase/services"; -import { - shouldPrefetchValues, - generateFieldSettingsFromParameters, -} from "metabase/actions/utils"; -import { getDashboardType } from "metabase/dashboard/utils"; - import type Field from "metabase-lib/metadata/Field"; import { getChangedValues, getInitialValues } from "./utils"; -export interface ActionParamatersInputFormProps { +export interface ActionParametersInputFormProps { action: WritebackAction; missingParameters?: WritebackParameter[]; dashcardParamValues?: ParametersForActionExecution; @@ -41,6 +38,9 @@ export interface ActionParamatersInputFormProps { onSubmitSuccess?: () => void; } +const shouldPrefetchValues = (action: WritebackAction) => + action.type === "implicit" && action.kind === "row/update"; + function ActionParametersInputForm({ action, missingParameters = action.parameters, @@ -50,7 +50,7 @@ function ActionParametersInputForm({ onCancel, onSubmit, onSubmitSuccess, -}: ActionParamatersInputFormProps) { +}: ActionParametersInputFormProps) { const [prefetchValues, setPrefetchValues] = useState<ParametersForActionExecution>({}); diff --git a/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputModal.tsx b/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputModal.tsx index e8c5f8064229aa2c8e5d9cc59e8dc308dc13361a..59328aba35fc4eb02ff075649627afef794ac895 100644 --- a/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputModal.tsx +++ b/frontend/src/metabase/actions/containers/ActionParametersInputForm/ActionParametersInputModal.tsx @@ -4,7 +4,7 @@ import Modal from "metabase/components/Modal"; import ModalContent from "metabase/components/ModalContent"; import ActionParametersInputForm, { - ActionParamatersInputFormProps, + ActionParametersInputFormProps, } from "./ActionParametersInputForm"; interface ModalProps { @@ -20,7 +20,7 @@ export default function ActionParametersInputModal({ showConfirmMessage, confirmMessage, ...formProps -}: ModalProps & ActionParamatersInputFormProps) { +}: ModalProps & ActionParametersInputFormProps) { return ( <Modal onClose={onClose}> <ModalContent title={title} onClose={onClose}> diff --git a/frontend/src/metabase/actions/containers/ActionPicker/ActionPicker.tsx b/frontend/src/metabase/actions/containers/ActionPicker/ActionPicker.tsx index d0bcd14afd3415a88577628734c718df1d1c571b..0a1f8dd38e7eeca9d3b8a8e2232dc2f093f5e2a4 100644 --- a/frontend/src/metabase/actions/containers/ActionPicker/ActionPicker.tsx +++ b/frontend/src/metabase/actions/containers/ActionPicker/ActionPicker.tsx @@ -8,7 +8,6 @@ import { useToggle } from "metabase/hooks/use-toggle"; import Actions from "metabase/entities/actions"; import Search from "metabase/entities/search"; -import { isImplicitAction } from "metabase/actions/utils"; import ActionCreator from "metabase/actions/containers/ActionCreator"; import type { Card, WritebackAction } from "metabase-types/api"; @@ -110,7 +109,7 @@ function ModelActionPicker({ onClick={() => onClick(action)} > <span>{action.name}</span> - {!isImplicitAction(action) && ( + {action.type !== "implicit" && ( <EditButton icon="pencil" onlyIcon diff --git a/frontend/src/metabase/actions/utils.ts b/frontend/src/metabase/actions/utils.ts index 0f1cc3547532241d45fab36d46356f5c9c829479..43637934e2d392c8b6c05cf5cde6b6009441fc20 100644 --- a/frontend/src/metabase/actions/utils.ts +++ b/frontend/src/metabase/actions/utils.ts @@ -1,83 +1,29 @@ import { t } from "ttag"; +import { getResponseErrorMessage } from "metabase/core/utils/errors"; +import { slugify, humanize } from "metabase/lib/formatting"; +import { isEmpty } from "metabase/lib/validate"; + import type { - ActionFormSettings, - Parameter, - WritebackAction, - WritebackActionBase, ActionDashboardCard, + ActionFormSettings, BaseDashboardOrderedCard, Card, FieldSettings, FieldSettingsMap, + Parameter, ParameterId, ParametersForActionExecution, - ImplicitQueryAction, + WritebackAction, + WritebackActionBase, + WritebackImplicitQueryAction, } from "metabase-types/api"; -import { getResponseErrorMessage } from "metabase/core/utils/errors"; -import { slugify, humanize } from "metabase/lib/formatting"; -import { isEmpty } from "metabase/lib/validate"; - import { TYPE } from "metabase-lib/types/constants"; import Field from "metabase-lib/metadata/Field"; import type { FieldSettings as LocalFieldSettings } from "./types"; -const AUTOMATIC_DATE_TIME_FIELDS = [ - TYPE.CreationDate, - TYPE.CreationTemporal, - TYPE.CreationTime, - TYPE.CreationTimestamp, - - TYPE.DeletionDate, - TYPE.DeletionTemporal, - TYPE.DeletionTime, - TYPE.DeletionTimestamp, - - TYPE.UpdatedDate, - TYPE.UpdatedTemporal, - TYPE.UpdatedTime, - TYPE.UpdatedTimestamp, -]; - -const isAutomaticDateTimeField = (field: Field) => { - return AUTOMATIC_DATE_TIME_FIELDS.includes(field.semantic_type); -}; - -export const isEditableField = (field: Field, parameter: Parameter) => { - const isRealField = typeof field.id === "number"; - if (!isRealField) { - // Filters out custom, aggregated columns, etc. - return false; - } - - if (field.isPK()) { - // Most of the time PKs are auto-generated, - // but there are rare cases when they're not - // In this case they're marked as `required` - return parameter.required; - } - - if (isAutomaticDateTimeField(field)) { - return parameter.required; - } - - return true; -}; - -export const hasImplicitActions = (actions: WritebackAction[]): boolean => - actions.some(isImplicitAction); - -export const isImplicitAction = (action: WritebackAction): boolean => - action.type === "implicit"; - -export const shouldPrefetchValues = (action: WritebackAction) => { - // in the future there should be a setting to configure this - // for custom actions - return action.type === "implicit" && action.kind === "row/update"; -}; - export const sortActionParams = (formSettings: ActionFormSettings) => (a: Parameter, b: Parameter) => { const fields = formSettings.fields || {}; @@ -250,7 +196,9 @@ function hasDataFromExplicitAction(result: any) { return !isInsert && !isUpdate && !isDelete; } -function getImplicitActionExecutionMessage(action: ImplicitQueryAction) { +function getImplicitActionExecutionMessage( + action: WritebackImplicitQueryAction, +) { if (action.kind === "row/create") { return t`Successfully saved`; } @@ -282,3 +230,32 @@ export function getActionErrorMessage(error: unknown) { t`Something went wrong while executing the action` ); } + +export const getSubmitButtonColor = (action: WritebackAction): string => { + if (action.type === "implicit" && action.kind === "row/delete") { + return "danger"; + } + return action.visualization_settings?.submitButtonColor ?? "primary"; +}; + +export const getSubmitButtonLabel = (action: WritebackAction): string => { + if (action.visualization_settings?.submitButtonLabel) { + return action.visualization_settings.submitButtonLabel; + } + + if (action.type === "implicit") { + if (action.kind === "row/delete") { + return t`Delete`; + } + + if (action.kind === "row/update") { + return t`Update`; + } + + if (action.kind === "row/create") { + return t`Save`; + } + } + + return t`Run`; +}; diff --git a/frontend/src/metabase/entities/actions/constants.ts b/frontend/src/metabase/entities/actions/constants.ts deleted file mode 100644 index 814cd308103fc9fdae974b5c73ba1ee1b77c4354..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/entities/actions/constants.ts +++ /dev/null @@ -1,28 +0,0 @@ -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/=", - 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", - number: "number", - date: "date", -};