Skip to content
Snippets Groups Projects
Unverified Commit b0c7a36a authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

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
parent 6cfc3fe4
No related branches found
No related tags found
No related merge requests found
Showing
with 178 additions and 15 deletions
......@@ -66,6 +66,7 @@ export default ({ question, clicked }) => {
openActionParametersModal({
emitterId: emitterId,
props: {
description: settings["user_input_modal.description"],
missingParameters,
onSubmit: filledMissingParameters =>
executeRowAction({
......
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")};
}
`;
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;
export { default } from "./ChartSettingsTextArea";
......@@ -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,
......
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}
</>
);
}
......
import styled from "@emotion/styled";
import Markdown from "metabase/core/components/Markdown";
export const FormDescription = styled(Markdown)`
margin-bottom: 1rem;
`;
......@@ -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);
export { default } from "./ActionParametersInputForm";
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