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

Remove redux-form and legacy form framework (#26967)

parent f173c80d
No related branches found
No related tags found
No related merge requests found
Showing
with 0 additions and 1028 deletions
/**
* @deprecated
*/
export type FieldName = string;
/**
* @deprecated
*/
export type DefaultFieldValue = unknown;
/**
* @deprecated
*/
export type FieldValues = Record<FieldName, DefaultFieldValue>;
type FieldValidateResultOK = undefined;
type FieldValidateResultError = string;
// Extending Record type here as field definition's props
// will be just spread to the final field widget
// (e.g. autoFocus, placeholder)
/**
* @deprecated
*/
export type BaseFieldDefinition = Record<string, unknown> & {
name: string;
type?: string;
title?: string;
description?: string;
placeholder?: string;
hidden?: boolean;
info?: string;
infoLabel?: string;
infoLabelTooltip?: string;
align?: "left" | "right";
horizontal?: boolean;
descriptionPosition?: "top" | "bottom";
visibleIf?: Record<FieldName, unknown>;
initial?: (value: unknown) => DefaultFieldValue;
validate?: (
value: DefaultFieldValue,
) => FieldValidateResultOK | FieldValidateResultError;
normalize?: (value: unknown) => DefaultFieldValue;
};
/**
* @deprecated
*/
export type StandardFormFieldDefinition = BaseFieldDefinition & {
// If not is not provided, we're going to use default text input
type?: string | (() => JSX.Element);
};
/**
* @deprecated
*/
export type CustomFormFieldDefinition = BaseFieldDefinition & {
widget: () => JSX.Element;
};
/**
* @deprecated
*/
export type FormFieldDefinition =
| StandardFormFieldDefinition
| CustomFormFieldDefinition;
/**
* @deprecated
*/
export type FormField<Value = DefaultFieldValue> = {
name: FieldName;
value: Value;
error?: string;
initialValue: Value;
active: boolean;
dirty: boolean;
invalid: boolean;
pristine: boolean;
touched: boolean;
valid: boolean;
visited: boolean;
onBlur: () => void;
onFocus: () => void;
};
/**
* @deprecated
*/
export type FormObject = {
fields:
| FormFieldDefinition[]
| ((values?: FieldValues) => FormFieldDefinition[]);
};
/**
* @deprecated
*/
export type PopulatedFormObject = {
fields: (values?: FieldValues) => FormFieldDefinition[];
fieldNames: (values: FieldValues) => FieldName[];
hidden: (obj: unknown) => void;
initial: (obj: unknown) => void;
normalize: (obj: unknown) => void;
validate: (obj: unknown) => void;
disablePristineSubmit?: boolean;
};
import type {
StandardFormFieldDefinition,
CustomFormFieldDefinition,
FormFieldDefinition,
} from "metabase-types/forms/legacy";
export function isCustomWidget(
formField: FormFieldDefinition,
): formField is CustomFormFieldDefinition {
return (
!(formField as StandardFormFieldDefinition).type &&
typeof (formField as CustomFormFieldDefinition).widget === "function"
);
}
import React from "react";
import _ from "underscore";
import {
FormFieldDefinition,
PopulatedFormObject,
} from "metabase-types/forms/legacy";
import {
BaseFormProps,
OptionalFormViewProps,
CustomFormLegacyContext,
LegacyContextTypes,
} from "./types";
import CustomFormField, { CustomFormFieldProps } from "./CustomFormField";
import CustomFormFooter, { CustomFormFooterProps } from "./CustomFormFooter";
import CustomFormMessage, { CustomFormMessageProps } from "./CustomFormMessage";
import CustomFormSubmit from "./CustomFormSubmit";
import Form from "./Form";
interface FormRenderProps extends BaseFormProps {
form: PopulatedFormObject;
formFields: FormFieldDefinition[];
Form: React.ComponentType<{ children: React.ReactNode }>;
FormField: React.ComponentType<CustomFormFieldProps>;
FormSubmit: React.ComponentType<{ children: React.ReactNode }>;
FormMessage: React.ComponentType<CustomFormMessageProps>;
FormFooter: React.ComponentType<CustomFormFooterProps>;
}
interface CustomFormProps extends BaseFormProps, OptionalFormViewProps {
children: React.ReactNode | ((props: FormRenderProps) => JSX.Element);
}
function CustomForm(props: CustomFormProps) {
const { formObject: form, values, children } = props;
if (typeof children === "function") {
return children({
...props,
form,
formFields: form.fields(values),
Form: Form,
FormField: CustomFormField,
FormSubmit: CustomFormSubmit,
FormMessage: CustomFormMessage,
FormFooter: CustomFormFooter,
});
}
return <Form {...props} />;
}
/**
* @deprecated
*/
class CustomFormWithLegacyContext extends React.Component<CustomFormProps> {
static childContextTypes = LegacyContextTypes;
getChildContext(): CustomFormLegacyContext {
const {
fields,
values,
formObject: form,
submitting,
invalid,
pristine,
error,
handleSubmit,
submitTitle,
renderSubmit,
className,
style,
onChangeField,
} = this.props;
const { disablePristineSubmit } = form;
const formFields = form.fields(values);
const formFieldsByName = _.indexBy(formFields, "name");
return {
handleSubmit,
submitTitle,
renderSubmit,
className,
style,
fields,
formFields,
formFieldsByName,
values,
submitting,
invalid,
pristine,
error,
onChangeField,
disablePristineSubmit,
};
}
render() {
return <CustomForm {...this.props} />;
}
}
export default CustomFormWithLegacyContext;
import React, { useCallback, useMemo } from "react";
import PropTypes from "prop-types";
import { getIn } from "icepick";
import _ from "underscore";
import FormField from "metabase/components/form/FormField";
import FormWidget from "metabase/components/form/FormWidget";
import { useOnMount } from "metabase/hooks/use-on-mount";
import { useOnUnmount } from "metabase/hooks/use-on-unmount";
import { BaseFieldDefinition } from "metabase-types/forms/legacy";
import { isCustomWidget } from "metabase-types/guards/forms-legacy";
import {
CustomFormLegacyContext,
FormContainerLegacyContext,
LegacyContextTypes,
} from "./types";
export interface CustomFormFieldProps extends BaseFieldDefinition {
onChange?: (e: unknown) => void;
}
interface LegacyContextProps
extends CustomFormLegacyContext,
FormContainerLegacyContext {}
function getFieldDefinition(props: CustomFormFieldProps): BaseFieldDefinition {
return _.pick(
props,
"name",
"type",
"title",
"description",
"initial",
"validate",
"normalize",
);
}
function RawCustomFormField({
fields,
formFieldsByName,
values,
onChangeField,
registerFormField,
unregisterFormField,
...props
}: CustomFormFieldProps & LegacyContextProps & { forwardedRef?: any }) {
const { name, onChange, forwardedRef } = props;
const field = getIn(fields, name.split("."));
const formField = formFieldsByName[name];
useOnMount(() => {
registerFormField?.(getFieldDefinition(props));
});
useOnUnmount(() => {
unregisterFormField?.(getFieldDefinition(props));
});
const handleChange = useCallback(
e => {
field.onChange(e);
onChange?.(e);
},
[field, onChange],
);
const fieldProps = useMemo(
() => ({
...props,
values,
onChangeField,
formField,
field:
typeof onChange === "function"
? {
...field,
onChange: handleChange,
}
: field,
}),
[props, values, formField, field, onChange, onChangeField, handleChange],
);
if (!field || !formField) {
return null;
}
const hasCustomWidget = isCustomWidget(formField);
const Widget = hasCustomWidget ? formField.widget : FormWidget;
return (
<FormField {...fieldProps}>
<Widget {...fieldProps} ref={forwardedRef} />
</FormField>
);
}
const CustomFormFieldLegacyContext = (
props: CustomFormFieldProps & { forwardedRef?: any },
context: LegacyContextProps,
) => <RawCustomFormField {...props} {...context} />;
CustomFormFieldLegacyContext.contextTypes = {
..._.pick(
LegacyContextTypes,
"fields",
"formFieldsByName",
"values",
"onChangeField",
),
registerFormField: PropTypes.func,
unregisterFormField: PropTypes.func,
};
/**
* @deprecated
*/
const CustomFormField = React.forwardRef<
HTMLInputElement,
CustomFormFieldProps
>(function CustomFormField(props, ref) {
return <CustomFormFieldLegacyContext {...props} forwardedRef={ref} />;
});
export default CustomFormField;
import styled from "@emotion/styled";
interface CustomFormFooterStyledProps {
shouldReverse?: boolean;
}
export const CustomFormFooterStyled = styled.div<CustomFormFooterStyledProps>`
display: flex;
align-items: flex-start;
flex-direction: ${({ shouldReverse }) =>
shouldReverse ? "row-reverse" : "column"};
`;
import React from "react";
import PropTypes from "prop-types";
import { t } from "ttag";
import Button from "metabase/core/components/Button";
import CustomFormMessage from "../CustomFormMessage";
import CustomFormSubmit from "../CustomFormSubmit";
import { CustomFormFooterStyled } from "./CustomFormFooter.styled";
import { CustomFormFooterProps } from "./CustomFormFooterTypes";
interface LegacyContextProps {
isModal?: boolean;
}
function CustomFormFooter({
submitTitle,
cancelTitle = t`Cancel`,
onCancel,
footerExtraButtons,
fullWidth,
isModal,
isContextModal,
}: CustomFormFooterProps & { isContextModal?: boolean }) {
return (
<CustomFormFooterStyled shouldReverse={isModal || isContextModal}>
<CustomFormSubmit fullWidth={fullWidth}>{submitTitle}</CustomFormSubmit>
{onCancel && (
<Button className="mx1" type="button" onClick={onCancel}>
{cancelTitle}
</Button>
)}
<CustomFormMessage className="mt1" />
{footerExtraButtons}
</CustomFormFooterStyled>
);
}
/**
* @deprecated
*/
const CustomFormFooterLegacyContext = (
props: CustomFormFooterProps,
{ isModal: isContextModal }: LegacyContextProps,
) => <CustomFormFooter {...props} isContextModal={isContextModal} />;
CustomFormFooterLegacyContext.contextTypes = {
isModal: PropTypes.bool,
};
export default CustomFormFooterLegacyContext;
export interface CustomFormFooterProps {
submitTitle: string;
cancelTitle?: string;
fullWidth?: boolean;
isModal?: boolean;
footerExtraButtons: React.ReactElement[];
onCancel?: () => void;
}
export { default } from "./CustomFormFooter";
export * from "./CustomFormFooterTypes";
import React from "react";
import _ from "underscore";
import FormMessage from "metabase/components/form/FormMessage";
import { CustomFormLegacyContext, LegacyContextTypes } from "./types";
export interface CustomFormMessageProps {
className?: string;
noPadding?: boolean;
}
function CustomFormMessage({
error,
...props
}: CustomFormMessageProps & CustomFormLegacyContext) {
if (error) {
return <FormMessage {...props} message={error} />;
}
return null;
}
/**
* @deprecated
*/
const CustomFormMessageLegacyContext = (
props: CustomFormMessageProps,
context: CustomFormLegacyContext,
) => <CustomFormMessage {...props} {...context} />;
CustomFormMessageLegacyContext.contextTypes = _.pick(
LegacyContextTypes,
"error",
);
export default CustomFormMessageLegacyContext;
import React from "react";
import DisclosureTriangle from "metabase/components/DisclosureTriangle";
import { useToggle } from "metabase/hooks/use-toggle";
interface SectionProps {
title?: string;
children: React.ReactNode;
}
function StandardSection({ title, children }: SectionProps) {
return (
<section className="mb4">
{title && <h2 className="mb2">{title}</h2>}
{children}
</section>
);
}
function CollapsibleSection({ title, children }: SectionProps) {
const [isExpanded, { toggle: handleToggle }] = useToggle(false);
return (
<section className="mb4">
<div
className="mb2 flex align-center cursor-pointer text-brand-hover"
onClick={handleToggle}
>
<DisclosureTriangle className="mr1" open={isExpanded} />
<h3>{title}</h3>
</div>
<div className={isExpanded ? undefined : "hide"}>{children}</div>
</section>
);
}
interface CustomFormSectionProps extends SectionProps {
collapsible?: boolean;
}
/**
* @deprecated
*/
function CustomFormSection({ collapsible, ...props }: CustomFormSectionProps) {
const Section = collapsible ? CollapsibleSection : StandardSection;
return <Section {...props} />;
}
export default CustomFormSection;
import React from "react";
import { t } from "ttag";
import _ from "underscore";
import ActionButton from "metabase/components/ActionButton";
import { CustomFormLegacyContext, LegacyContextTypes } from "./types";
export interface CustomFormSubmitProps {
children: React.ReactNode;
// ActionButton props
fullWidth?: boolean;
}
function CustomFormSubmit({
submitting,
invalid,
pristine,
handleSubmit,
submitTitle,
renderSubmit,
disablePristineSubmit,
children,
...props
}: CustomFormSubmitProps & CustomFormLegacyContext) {
const title = children || submitTitle || t`Submit`;
const canSubmit = !(
submitting ||
invalid ||
(pristine && disablePristineSubmit)
);
if (renderSubmit) {
return renderSubmit({ title, canSubmit, handleSubmit });
}
return (
<ActionButton
normalText={title}
activeText={title}
failedText={t`Failed`}
successText={t`Success`}
primary={canSubmit}
disabled={!canSubmit}
{...props}
type="submit"
actionFn={handleSubmit}
/>
);
}
/**
* @deprecated
*/
const CustomFormSubmitLegacyContext = (
props: CustomFormSubmitProps,
context: CustomFormLegacyContext,
) => <CustomFormSubmit {...props} {...context} />;
CustomFormSubmitLegacyContext.contextTypes = _.pick(
LegacyContextTypes,
"values",
"submitting",
"invalid",
"pristine",
"handleSubmit",
"submitTitle",
"renderSubmit",
"disablePristineSubmit",
);
export default CustomFormSubmitLegacyContext;
import React from "react";
import _ from "underscore";
import { CustomFormLegacyContext, LegacyContextTypes } from "./types";
type Props = {
children: React.ReactNode;
};
function Form({
children,
handleSubmit,
className,
style,
}: Props & CustomFormLegacyContext) {
return (
<form onSubmit={handleSubmit} className={className} style={style}>
{children}
</form>
);
}
/**
* @deprecated
*/
const FormUsingLegacyContext = (
props: Props,
context: CustomFormLegacyContext,
) => <Form {...props} {...context} />;
FormUsingLegacyContext.contextTypes = _.pick(
LegacyContextTypes,
"handleSubmit",
"className",
"style",
);
export default FormUsingLegacyContext;
export { default as CustomFormField } from "./CustomFormField";
export { default as CustomFormFooter } from "./CustomFormFooter";
export { default as CustomFormMessage } from "./CustomFormMessage";
export { default as CustomFormSection } from "./CustomFormSection";
export { default as CustomFormSubmit } from "./CustomFormSubmit";
export { default } from "./CustomForm";
import PropTypes from "prop-types";
import {
BaseFieldDefinition,
FieldName,
DefaultFieldValue,
FieldValues,
FormFieldDefinition,
FormField,
PopulatedFormObject,
} from "metabase-types/forms/legacy";
export interface BaseFormProps {
formKey?: string;
formObject: PopulatedFormObject;
formFields: FormFieldDefinition[];
formFieldsByName: Record<FieldName, FormFieldDefinition>;
disablePristineSubmit?: boolean;
fields: Record<string, FormField>;
values: FieldValues;
errors: Record<FieldName, string>;
active?: boolean;
asyncValidating?: boolean;
dirty: boolean;
error?: string;
invalid: boolean;
overwriteOnInitialValuesChange?: boolean;
pristine: boolean;
readonly?: boolean;
submitFailed: boolean;
submitting: boolean;
valid: boolean;
asyncValidate: () => void;
destroyForm: () => void;
handleSubmit: () => void;
initializeForm: () => void;
onChangeField: (fieldName: FieldName, value: DefaultFieldValue) => void;
onSubmitSuccess: () => void;
resetForm: () => void;
}
type RenderSubmitProps = {
title: React.ReactNode;
canSubmit: boolean;
handleSubmit: () => void;
};
export interface OptionalFormViewProps {
submitTitle?: string;
renderSubmit?: (props: RenderSubmitProps) => JSX.Element;
className?: string;
style?: React.CSSProperties;
}
export interface CustomFormLegacyContext
extends OptionalFormViewProps,
Pick<
BaseFormProps,
| "formFields"
| "formFieldsByName"
| "disablePristineSubmit"
| "handleSubmit"
| "fields"
| "values"
| "submitting"
| "invalid"
| "pristine"
| "error"
| "onChangeField"
> {}
export interface FormContainerLegacyContext {
registerFormField: (fieldDef: BaseFieldDefinition) => void;
unregisterFormField: (fieldDef: BaseFieldDefinition) => void;
}
export const LegacyContextTypes = {
handleSubmit: PropTypes.func,
submitTitle: PropTypes.string,
renderSubmit: PropTypes.func,
className: PropTypes.string,
style: PropTypes.object,
fields: PropTypes.object,
formFields: PropTypes.array,
formFieldsByName: PropTypes.object,
values: PropTypes.object,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
pristine: PropTypes.bool,
error: PropTypes.string,
onChangeField: PropTypes.func,
disablePristineSubmit: PropTypes.bool,
};
import styled from "@emotion/styled";
import { css } from "@emotion/react";
import { color } from "metabase/lib/colors";
import Icon from "metabase/components/Icon";
export const FieldRow = styled.div`
display: flex;
align-items: center;
margin-bottom: 0.5em;
`;
export const Label = styled.label<{
horizontal?: boolean;
standAlone?: boolean;
}>`
margin-bottom: 0;
${props =>
props.horizontal &&
css`
margin-right: auto;
`}
${props =>
props.standAlone &&
css`
margin-top: 0.8em;
`}
`;
Label.defaultProps = { className: "Form-label" };
export const InfoIcon = styled(Icon)`
margin-left: 8px;
color: ${color("bg-dark")};
&:hover {
color: ${() => color("brand")};
}
`;
export const InfoLabel = styled.span`
color: ${color("text-medium")};
font-size: 0.88em;
margin-left: auto;
cursor: default;
`;
export const FieldContainer = styled.div<{
horizontal?: boolean;
align?: "left" | "right";
}>`
width: 100%;
margin-right: ${props => (props.horizontal ? "1rem" : "")};
margin-left: ${props => (props.align === "left" ? "0.5rem" : "")};
`;
export const InputContainer = styled.div`
flex-shrink: 0;
`;
import React from "react";
import {
FieldName,
FieldValues,
FormField as FormFieldType,
BaseFieldDefinition,
FormFieldDefinition,
} from "metabase-types/forms/legacy";
import FormFieldView from "./FormFieldView";
type ReduxFormProps = Pick<FormFieldType, "name"> &
Partial<Pick<FormFieldType, "error" | "visited" | "active">>;
interface FormFieldProps extends BaseFieldDefinition, ReduxFormProps {
field: FormFieldType;
formField: FormFieldDefinition;
values: FieldValues;
className?: string;
children: React.ReactNode;
onChangeField: (fieldName: FieldName, value: unknown) => void;
}
const ALL_DOT_CHARS = /\./g;
function getFieldId(formFieldName: FieldName) {
return `formField-${formFieldName.replace(ALL_DOT_CHARS, "-")}`;
}
function getDescriptionPositionPropValue(
descriptionPosition?: "top" | "bottom",
formField?: FormFieldDefinition,
) {
return descriptionPosition ?? formField?.descriptionPosition ?? "top";
}
function getHiddenPropValue(hidden?: boolean, formField?: FormFieldDefinition) {
if (typeof hidden === "boolean") {
return hidden;
}
if (formField) {
return formField.hidden || formField.type === "hidden";
}
return false;
}
function getHorizontalPropValue(
horizontal?: boolean,
formField?: FormFieldDefinition,
) {
if (typeof horizontal === "boolean") {
return horizontal;
}
if (formField) {
return formField.horizontal || formField.type === "boolean";
}
return false;
}
/**
* @deprecated
*/
function FormField({
className,
formField,
children,
...props
}: FormFieldProps) {
const title = props.title ?? formField?.title;
const type = props.type ?? formField.type;
const description = props.description ?? formField?.description;
const descriptionPosition = getDescriptionPositionPropValue(
props.descriptionPosition,
formField,
);
const info = props.info ?? formField?.info;
const infoLabel = props.infoLabel ?? formField?.infoLabel;
const infoLabelTooltip =
props.infoLabelTooltip ?? formField?.infoLabelTooltip;
const align = props.align ?? formField?.align ?? "right";
const hidden = getHiddenPropValue(props.hidden, formField);
const horizontal = getHorizontalPropValue(props.horizontal, formField);
const isToggle = type === "boolean";
const standAloneLabel = isToggle && align === "right" && !description;
if (hidden) {
return null;
}
const {
name,
error: errorProp,
visited,
active,
} = {
...(props.field || {}),
...props,
};
const shouldShowError = visited && !active;
const error = !shouldShowError ? undefined : errorProp;
return (
<FormFieldView
fieldId={getFieldId(name)}
className={className}
name={name}
error={error}
title={title}
description={description}
descriptionPosition={descriptionPosition}
info={info}
infoLabel={infoLabel}
infoLabelTooltip={infoLabelTooltip}
align={align}
standAloneLabel={standAloneLabel}
horizontal={horizontal}
>
{children}
</FormFieldView>
);
}
export default FormField;
import React from "react";
interface FormFieldDescriptionProps {
className: string;
description: string;
}
export const FormFieldDescription = ({
className,
description,
}: FormFieldDescriptionProps) => {
if (typeof description === "string") {
return (
<div
className={className}
dangerouslySetInnerHTML={{
__html: description,
}}
/>
);
}
return <div className={className}>{description}</div>;
};
import React from "react";
import cx from "classnames";
import Tooltip from "metabase/components/Tooltip";
import { BaseFieldDefinition } from "metabase-types/forms/legacy";
import { FormFieldDescription } from "./FormFieldDescription";
import {
FieldRow,
Label,
InfoIcon,
InputContainer,
FieldContainer,
InfoLabel,
} from "./FormField.styled";
interface FormFieldViewProps extends BaseFieldDefinition {
fieldId: string;
error?: string;
className?: string;
standAloneLabel?: boolean;
children: React.ReactNode;
}
function FormFieldView({
fieldId,
className,
name,
error,
title,
description,
descriptionPosition,
info,
infoLabel,
infoLabelTooltip,
align,
horizontal,
standAloneLabel,
children,
}: FormFieldViewProps) {
const rootClassNames = cx("Form-field", className, {
"Form--fieldError": !!error,
flex: horizontal,
});
return (
<div id={fieldId} className={rootClassNames}>
{align === "left" && <InputContainer>{children}</InputContainer>}
{(title || description) && (
<FieldContainer horizontal={horizontal} align={align}>
<FieldRow>
{title && (
<Label
id={`${name}-label`}
htmlFor={name}
horizontal={horizontal}
standAlone={standAloneLabel}
>
{title}
{error && <span className="text-error">: {error}</span>}
</Label>
)}
{info && (
<Tooltip tooltip={info}>
<InfoIcon name="info" size={12} />
</Tooltip>
)}
{infoLabel && (
<Tooltip tooltip={infoLabelTooltip} maxWidth="100%">
<InfoLabel>{infoLabel}</InfoLabel>
</Tooltip>
)}
</FieldRow>
{description && descriptionPosition === "top" && (
<FormFieldDescription className="mb1" description={description} />
)}
</FieldContainer>
)}
{align !== "left" && <InputContainer>{children}</InputContainer>}
{description && descriptionPosition === "bottom" && (
<FormFieldDescription className="mt1" description={description} />
)}
</div>
);
}
export default FormFieldView;
export { default } from "./FormField";
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useMemo } from "react";
import { getIn } from "icepick";
import _ from "underscore"; import _ from "underscore";
import { isCustomWidget } from "metabase-types/guards"; import { isCustomWidget } from "metabase-types/guards";
......
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