From b0c7a36aa375a78cc28dfc41070db3bcce871343 Mon Sep 17 00:00:00 2001 From: Anton Kulyk <kuliks.anton@gmail.com> Date: Fri, 5 Aug 2022 19:51:54 +0100 Subject: [PATCH] Add confirmation modal and input modal description for custom actions (#24598) * Fix action buttons size * Allow requesting confirmation to action buttons * Add `ChartSettingsTextArea` viz settings widget * Remove not used variables * Add `user_input_modal.description` action viz setting * Render user input modal description * Move `ActionParametersInputForm` to its own dir --- .../components/drill/DashboardClickDrill.jsx | 1 + .../ChartSettingsTextArea.styled.tsx | 20 ++++ .../ChartSettingsTextArea.tsx | 20 ++++ .../settings/ChartSettingsTextArea/index.ts | 1 + .../metabase/visualizations/lib/settings.js | 2 + .../writeback/components/ActionButtonViz.tsx | 113 ++++++++++++++++-- .../ActionParametersInputForm.styled.tsx | 7 ++ .../ActionParametersInputForm.tsx | 28 ++++- .../ActionParametersInputForm/index.ts | 1 + 9 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.styled.tsx create mode 100644 frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.tsx create mode 100644 frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/index.ts create mode 100644 frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.styled.tsx rename frontend/src/metabase/writeback/containers/{ => ActionParametersInputForm}/ActionParametersInputForm.tsx (81%) create mode 100644 frontend/src/metabase/writeback/containers/ActionParametersInputForm/index.ts diff --git a/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx b/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx index 9495a4b6b82..fbf43f06500 100644 --- a/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx +++ b/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx @@ -66,6 +66,7 @@ export default ({ question, clicked }) => { openActionParametersModal({ emitterId: emitterId, props: { + description: settings["user_input_modal.description"], missingParameters, onSubmit: filledMissingParameters => executeRowAction({ diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.styled.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.styled.tsx new file mode 100644 index 00000000000..33b0b21297b --- /dev/null +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.styled.tsx @@ -0,0 +1,20 @@ +import styled from "@emotion/styled"; +import { color } from "metabase/lib/colors"; + +export const TextArea = styled.textarea` + min-width: 100%; + max-width: 100%; + min-height: 9rem; + padding: 8px; + + border: 1px solid ${color("border")}; + border-radius: 6px; + + transition: border 0.3s; + outline: none; + + &:hover, + &:focus { + border-color: ${color("brand")}; + } +`; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.tsx new file mode 100644 index 00000000000..a0e136ebe74 --- /dev/null +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.tsx @@ -0,0 +1,20 @@ +import React, { useCallback } from "react"; +import { TextArea } from "./ChartSettingsTextArea.styled"; + +interface Props { + value: string; + onChange: (value: string) => void; +} + +function ChartSettingsTextArea({ value, onChange }: Props) { + const handleChange = useCallback( + (e: React.ChangeEvent<HTMLTextAreaElement>) => { + onChange(e.target.value); + }, + [onChange], + ); + + return <TextArea value={value} onChange={handleChange} />; +} + +export default ChartSettingsTextArea; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/index.ts b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/index.ts new file mode 100644 index 00000000000..45138a1668d --- /dev/null +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/index.ts @@ -0,0 +1 @@ +export { default } from "./ChartSettingsTextArea"; diff --git a/frontend/src/metabase/visualizations/lib/settings.js b/frontend/src/metabase/visualizations/lib/settings.js index 405cfc00d63..65b0fef1089 100644 --- a/frontend/src/metabase/visualizations/lib/settings.js +++ b/frontend/src/metabase/visualizations/lib/settings.js @@ -13,12 +13,14 @@ import ChartSettingFieldsPicker from "metabase/visualizations/components/setting import ChartSettingFieldsPartition from "metabase/visualizations/components/settings/ChartSettingFieldsPartition"; import ChartSettingColorPicker from "metabase/visualizations/components/settings/ChartSettingColorPicker"; import ChartSettingColorsPicker from "metabase/visualizations/components/settings/ChartSettingColorsPicker"; +import ChartSettingsTextArea from "metabase/visualizations/components/settings/ChartSettingsTextArea"; import * as MetabaseAnalytics from "metabase/lib/analytics"; const WIDGETS = { input: ChartSettingInput, inputGroup: ChartSettingInputGroup, + text: ChartSettingsTextArea, number: ChartSettingInputNumeric, radio: ChartSettingRadio, select: ChartSettingSelect, diff --git a/frontend/src/metabase/writeback/components/ActionButtonViz.tsx b/frontend/src/metabase/writeback/components/ActionButtonViz.tsx index 52eccb9ec7e..a5c80eb780c 100644 --- a/frontend/src/metabase/writeback/components/ActionButtonViz.tsx +++ b/frontend/src/metabase/writeback/components/ActionButtonViz.tsx @@ -1,9 +1,11 @@ import React, { useCallback, useMemo } from "react"; import { t } from "ttag"; -import cx from "classnames"; +import _ from "underscore"; import Button from "metabase/core/components/Button"; +import { useConfirmation } from "metabase/hooks/use-confirmation"; + import { DashboardWithCards } from "metabase-types/types/Dashboard"; import { VisualizationProps } from "metabase-types/types/Visualization"; @@ -50,6 +52,49 @@ const ACTIONS_VIZ_DEFINITION = { ], }, }, + "confirmation_modal.is_required": { + section: t`Confirmation`, + title: t`Require confirmation`, + widget: "toggle", + default: false, + }, + "confirmation_modal.title": { + section: t`Confirmation`, + title: t`Confirmation modal title`, + widget: "input", + default: t`Are you sure?`, + getHidden: (_: any, settings: any) => + !settings["confirmation_modal.is_required"], + }, + "confirmation_modal.description": { + section: t`Confirmation`, + title: t`Confirmation modal description`, + widget: "input", + default: t`This cannot be undone`, + getHidden: (_: any, settings: any) => + !settings["confirmation_modal.is_required"], + }, + "confirmation_modal.submit.title": { + section: t`Confirmation`, + title: t`Submit button title`, + widget: "input", + default: t`Confirm`, + getHidden: (_: any, settings: any) => + !settings["confirmation_modal.is_required"], + }, + "confirmation_modal.cancel.title": { + section: t`Confirmation`, + title: t`Cancel button title`, + widget: "input", + default: t`Cancel`, + getHidden: (_: any, settings: any) => + !settings["confirmation_modal.is_required"], + }, + "user_input_modal.description": { + section: t`User input modal`, + title: t`Description`, + widget: "text", + }, }, }; @@ -58,14 +103,29 @@ interface ActionButtonVizProps extends VisualizationProps { } function ActionButtonViz({ - isSettings, settings, getExtraDataForClick, onVisualizationClick, }: ActionButtonVizProps) { + const { modalContent: confirmationModal, show: requestConfirmation } = + useConfirmation(); + const label = settings["button.label"]; const variant = settings["button.variant"]; + const confirmationModalSettings = useMemo(() => { + const result: Record<string, any> = {}; + + Object.keys(settings).forEach(key => { + if (key.startsWith("confirmation_modal.")) { + const shortKey = key.replace("confirmation_modal.", ""); + result[shortKey] = settings[key]; + } + }); + + return result; + }, [settings]); + const variantProps: any = {}; if (variant !== "default") { variantProps[variant] = true; @@ -83,9 +143,9 @@ function ActionButtonViz({ [clicked, getExtraDataForClick], ); - const onClick = useCallback( + const handleTriggerAction = useCallback( (e: React.MouseEvent) => { - onVisualizationClick({ + return onVisualizationClick({ ...clicked, extraData, element: e.currentTarget as HTMLElement, @@ -94,16 +154,43 @@ function ActionButtonViz({ [clicked, extraData, onVisualizationClick], ); + const handleActionRequiringConfirmation = useCallback( + (e: React.MouseEvent) => { + requestConfirmation({ + title: confirmationModalSettings.title, + message: confirmationModalSettings.description, + confirmButtonText: confirmationModalSettings["submit.title"], + cancelButtonText: confirmationModalSettings["cancel.title"], + onConfirm: async () => handleTriggerAction(e), + }); + }, + [confirmationModalSettings, requestConfirmation, handleTriggerAction], + ); + + const onClick = useCallback( + (e: React.MouseEvent) => { + if (confirmationModalSettings.is_required) { + handleActionRequiringConfirmation(e); + } else { + handleTriggerAction(e); + } + }, + [ + confirmationModalSettings, + handleTriggerAction, + handleActionRequiringConfirmation, + ], + ); + return ( - <Button - className={cx({ - "full-height": !isSettings, - })} - onClick={onClick} - {...variantProps} - > - {label} - </Button> + <> + <div className="flex full-height full-width layout-centered px1"> + <Button onClick={onClick} {...variantProps} fullWidth> + {label} + </Button> + </div> + {confirmationModal} + </> ); } diff --git a/frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.styled.tsx b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.styled.tsx new file mode 100644 index 00000000000..fd5e545bc2b --- /dev/null +++ b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.styled.tsx @@ -0,0 +1,7 @@ +import styled from "@emotion/styled"; + +import Markdown from "metabase/core/components/Markdown"; + +export const FormDescription = styled(Markdown)` + margin-bottom: 1rem; +`; diff --git a/frontend/src/metabase/writeback/containers/ActionParametersInputForm.tsx b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.tsx similarity index 81% rename from frontend/src/metabase/writeback/containers/ActionParametersInputForm.tsx rename to frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.tsx index 1f9a2eaab54..7bb58745ba9 100644 --- a/frontend/src/metabase/writeback/containers/ActionParametersInputForm.tsx +++ b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.tsx @@ -2,15 +2,18 @@ import React, { useCallback, useMemo } from "react"; import { connect } from "react-redux"; import { t } from "ttag"; +import { useDataAppContext } from "metabase/writeback/containers/DataAppContext"; import { getActionTemplateTagType, getActionParameterType, } from "metabase/writeback/utils"; -import Form from "metabase/containers/Form"; +import RootForm from "metabase/containers/Form"; import { TemplateTag } from "metabase-types/types/Query"; import { Parameter, ParameterId } from "metabase-types/types/Parameter"; +import { FormDescription } from "./ActionParametersInputForm.styled"; + type MappedParameters = Record< string, { type: string; value: string | number } @@ -18,10 +21,12 @@ type MappedParameters = Record< interface Props { missingParameters: TemplateTag[] | Parameter[]; + description?: string; onSubmit: (parameters: MappedParameters) => { type: string; payload: any }; onSubmitSuccess: () => void; dispatch: (action: any) => void; } + function isTemplateTag( tagOrParameter: TemplateTag | Parameter, ): tagOrParameter is TemplateTag { @@ -103,11 +108,14 @@ function formatParametersBeforeSubmit( } function ActionParametersInputForm({ + description, missingParameters, dispatch, onSubmit, onSubmitSuccess, }: Props) { + const dataAppContext = useDataAppContext(); + const form = useMemo(() => { return { fields: missingParameters.map(tagOrParameter => { @@ -137,7 +145,23 @@ function ActionParametersInputForm({ [missingParameters, onSubmit, onSubmitSuccess, dispatch], ); - return <Form form={form} onSubmit={handleSubmit} submitTitle={t`Execute`} />; + return ( + <RootForm form={form} onSubmit={handleSubmit} submitTitle={t`Execute`}> + {({ Form, FormField, FormFooter, formFields }: any) => ( + <Form> + {description && ( + <FormDescription> + {dataAppContext.format(description)} + </FormDescription> + )} + {formFields.map((field: any) => ( + <FormField key={field.name} name={field.name} /> + ))} + <FormFooter /> + </Form> + )} + </RootForm> + ); } export default connect()(ActionParametersInputForm); diff --git a/frontend/src/metabase/writeback/containers/ActionParametersInputForm/index.ts b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/index.ts new file mode 100644 index 00000000000..943e8c4a1cb --- /dev/null +++ b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/index.ts @@ -0,0 +1 @@ +export { default } from "./ActionParametersInputForm"; -- GitLab