Skip to content
Snippets Groups Projects
Unverified Commit 8d46b4f5 authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Replace redux-form in Metric and Segment forms (#24682)

parent e9a65ed5
No related branches found
No related tags found
No related merge requests found
Showing
with 250 additions and 234 deletions
......@@ -9,11 +9,13 @@ export * from "./dataset";
export * from "./field";
export * from "./foreign-key";
export * from "./group";
export * from "./metric";
export * from "./models";
export * from "./notifications";
export * from "./permissions";
export * from "./query";
export * from "./revision";
export * from "./segment";
export * from "./settings";
export * from "./slack";
export * from "./table";
......
import { StructuredQuery } from "./query";
import { TableId } from "./table";
export type MetricId = number;
export interface Metric {
id: MetricId;
name: string;
description: string;
table_id: TableId;
archived: boolean;
definition: StructuredQuery;
revision_message?: string;
}
......@@ -5,8 +5,10 @@ export * from "./collection";
export * from "./dashboard";
export * from "./database";
export * from "./dataset";
export * from "./metric";
export * from "./models";
export * from "./query";
export * from "./segment";
export * from "./table";
export * from "./timeline";
export * from "./settings";
......
import { Metric } from "metabase-types/api";
import { createMockStructuredQuery } from "./query";
export const createMockMetric = (opts?: Partial<Metric>): Metric => ({
id: 1,
name: "Metric",
description: "A metric",
table_id: 1,
archived: false,
definition: createMockStructuredQuery(),
...opts,
});
import { Segment } from "metabase-types/api";
import { createMockStructuredQuery } from "./query";
export const createMockSegment = (opts?: Partial<Segment>): Segment => ({
id: 1,
name: "Segment",
description: "A segment",
table_id: 1,
archived: false,
definition: createMockStructuredQuery(),
...opts,
});
import { StructuredQuery } from "./query";
import { TableId } from "./table";
export type SegmentId = number;
export interface Segment {
id: SegmentId;
name: string;
description: string;
table_id: TableId;
archived: boolean;
definition: StructuredQuery;
revision_message?: string;
}
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import cx from "classnames";
import { formDomOnlyProps } from "metabase/lib/redux";
export default class FormInput extends Component {
static propTypes = {};
render() {
const { field, className, placeholder } = this.props;
return (
<input
type="text"
placeholder={placeholder}
className={cx(
"input full",
{ "border-error": !field.active && field.visited && field.invalid },
className,
)}
{...formDomOnlyProps(field)}
/>
);
}
}
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
export interface FormInputRootProps {
touched?: boolean;
error?: string | boolean;
}
export const FormInputRoot = styled.input<FormInputRootProps>`
width: 100%;
&:not(:focus) {
border-color: ${props => props.touched && props.error && color("error")};
}
`;
import React, { forwardRef, InputHTMLAttributes, Ref } from "react";
import cx from "classnames";
import { FormInputRoot } from "./FormInput.styled";
interface FormInputProps extends InputHTMLAttributes<HTMLInputElement> {
touched?: boolean;
error?: string | boolean;
}
const FormInput = forwardRef(function FormInput(
{ className, touched, error, ...props }: FormInputProps,
ref: Ref<HTMLInputElement>,
) {
return (
<FormInputRoot
{...props}
ref={ref}
className={cx("input", className)}
type="text"
touched={touched}
error={error}
/>
);
});
export default FormInput;
export { default } from "./FormInput";
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import PropTypes from "prop-types";
export default class FormLabel extends Component {
static propTypes = {
title: PropTypes.string,
description: PropTypes.string,
};
static defaultProps = {
title: "",
description: "",
};
render() {
const { title, description, children } = this.props;
return (
<div className="mb3">
<div style={{ maxWidth: "575px" }}>
{title && (
<label className="h5 text-bold text-uppercase">{title}</label>
)}
{description && <p className="mt1 mb2">{description}</p>}
</div>
{children}
</div>
);
}
}
import styled from "@emotion/styled";
export const FormLabelRoot = styled.div`
margin-bottom: 2rem;
`;
export const FormLabelContent = styled.div`
max-width: 36rem;
`;
export const FormLabelTitle = styled.label`
font-size: 0.72rem;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.06em;
`;
export const FormLabelDescription = styled.p`
margin-top: 0.5rem;
margin-bottom: 1rem;
`;
import React, {
forwardRef,
HTMLAttributes,
LabelHTMLAttributes,
ReactNode,
Ref,
} from "react";
import {
FormLabelContent,
FormLabelDescription,
FormLabelRoot,
FormLabelTitle,
} from "./FormLabel.styled";
interface FormLabelProps extends HTMLAttributes<HTMLDivElement> {
title?: string;
description?: string;
children?: ReactNode;
}
const FormLabel = forwardRef(function FormLabel(
{ title, description, children, ...props }: FormLabelProps,
ref: Ref<HTMLDivElement>,
) {
return (
<FormLabelRoot {...props} ref={ref}>
<FormLabelContent>
{title && <FormLabelTitle>{title}</FormLabelTitle>}
{description && (
<FormLabelDescription>{description}</FormLabelDescription>
)}
</FormLabelContent>
{children}
</FormLabelRoot>
);
});
export default FormLabel;
export { default } from "./FormLabel";
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import cx from "classnames";
import { formDomOnlyProps } from "metabase/lib/redux";
export default class FormTextArea extends Component {
static propTypes = {};
render() {
const { field, className, placeholder } = this.props;
return (
<textarea
placeholder={placeholder}
className={cx(
"input full",
{ "border-error": !field.active && field.visited && field.invalid },
className,
)}
{...formDomOnlyProps(field)}
/>
);
}
}
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
export interface FormTextAreaRootProps {
touched?: boolean;
error?: string | boolean;
}
export const FormTextAreaRoot = styled.textarea<FormTextAreaRootProps>`
width: 100%;
&:not(:focus) {
border-color: ${props => props.touched && props.error && color("error")};
}
`;
import React, { forwardRef, Ref, TextareaHTMLAttributes } from "react";
import cx from "classnames";
import { FormTextAreaRoot } from "./FormTextArea.styled";
interface FormTextAreaProps
extends TextareaHTMLAttributes<HTMLTextAreaElement> {
touched?: boolean;
error?: string | boolean;
}
const FormTextArea = forwardRef(function FormTextArea(
{ className, touched, error, ...props }: FormTextAreaProps,
ref: Ref<HTMLTextAreaElement>,
) {
return (
<FormTextAreaRoot
{...props}
ref={ref}
className={cx("input", className)}
touched={touched}
error={error}
/>
);
});
export default FormTextArea;
export { default } from "./FormTextArea";
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import { Link } from "react-router";
import { reduxForm } from "redux-form";
import cx from "classnames";
import FormLabel from "../components/FormLabel";
import FormInput from "../components/FormInput";
import FormTextArea from "../components/FormTextArea";
import FieldSet from "metabase/components/FieldSet";
import PartialQueryBuilder from "../components/PartialQueryBuilder";
import { t } from "ttag";
import { formatValue } from "metabase/lib/formatting";
import * as Q from "metabase/lib/query/query";
class MetricForm extends Component {
renderActionButtons() {
const { invalid, handleSubmit } = this.props;
return (
<div>
<button
className={cx("Button", {
"Button--primary": !invalid,
disabled: invalid,
})}
onClick={handleSubmit}
>{t`Save changes`}</button>
<Link
to="/admin/datamodel/metrics"
className="Button ml2"
>{t`Cancel`}</Link>
</div>
);
}
render() {
const {
fields: { id, name, description, definition, revision_message },
handleSubmit,
previewSummary,
updatePreviewSummary,
} = this.props;
const isNewRecord = id.value === "";
return (
<form className="full" onSubmit={handleSubmit}>
<div className="wrapper py4">
<FormLabel
title={isNewRecord ? t`Create Your Metric` : t`Edit Your Metric`}
description={
isNewRecord
? t`You can create saved metrics to add a named metric option. Saved metrics include the aggregation type, the aggregated field, and optionally any filter you add. As an example, you might use this to create something like the official way of calculating "Average Price" for an Orders table.`
: t`Make changes to your metric and leave an explanatory note.`
}
>
<PartialQueryBuilder
features={{
filter: true,
aggregation: true,
}}
previewSummary={
previewSummary == null
? ""
: t`Result: ` + formatValue(previewSummary)
}
updatePreviewSummary={updatePreviewSummary}
canChangeTable={isNewRecord}
{...definition}
/>
</FormLabel>
<div style={{ maxWidth: "575px" }}>
<FormLabel
title={t`Name Your Metric`}
description={t`Give your metric a name to help others find it.`}
>
<FormInput
field={name}
placeholder={t`Something descriptive but not too long`}
/>
</FormLabel>
<FormLabel
title={t`Describe Your Metric`}
description={t`Give your metric a description to help others understand what it's about.`}
>
<FormTextArea
field={description}
placeholder={t`This is a good place to be more specific about less obvious metric rules`}
/>
</FormLabel>
{!isNewRecord && (
<FieldSet legend={t`Reason For Changes`}>
<FormLabel
description={t`Leave a note to explain what changes you made and why they were required.`}
>
<FormTextArea
field={revision_message}
placeholder={t`This will show up in the revision history for this metric to help everyone remember why things changed`}
/>
</FormLabel>
<div className="flex align-center">
{this.renderActionButtons()}
</div>
</FieldSet>
)}
</div>
</div>
{isNewRecord && (
<div className="border-top py4">
<div className="wrapper">{this.renderActionButtons()}</div>
</div>
)}
</form>
);
}
}
export default reduxForm(
{
form: "metric",
fields: [
"id",
"name",
"description",
"table_id",
"definition",
"revision_message",
"show_in_getting_started",
],
validate: values => {
const errors = {};
if (!values.name) {
errors.name = t`Name is required`;
}
if (!values.description) {
errors.description = t`Description is required`;
}
if (values.id != null) {
if (!values.revision_message) {
errors.revision_message = t`Revision message is required`;
}
}
const aggregations =
values.definition && Q.getAggregations(values.definition);
if (!aggregations || aggregations.length === 0) {
errors.definition = t`Aggregation is required`;
}
return errors;
},
},
(state, { metric }) => ({ initialValues: metric }),
)(MetricForm);
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import Button from "metabase/core/components/Button";
import {
breakpointMinMedium,
breakpointMinSmall,
} from "metabase/styled-components/theme";
export const FormRoot = styled.form`
width: 100%;
`;
export const FormSection = styled.div`
margin: 0 auto;
padding: 0 1em;
${breakpointMinSmall} {
padding-left: 1.75rem;
padding-right: 1.75rem;
}
${breakpointMinMedium} {
padding-left: 2.625rem;
padding-right: 2.625rem;
}
`;
export const FormBody = styled(FormSection)`
padding-top: 2rem;
padding-bottom: 2rem;
`;
export const FormBodyContent = styled.div`
max-width: 36rem;
`;
export const FormFooter = styled.div`
padding-top: 2rem;
padding-bottom: 2rem;
border-top: 1px solid ${color("border")};
`;
export const FormFooterContent = styled.div`
display: flex;
align-items: center;
`;
export const FormSubmitButton = styled(Button)`
margin-right: 1rem;
`;
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