Skip to content

Commit fce894e

Browse files
authored
Merge pull request #681 from gadget-inc/auto-input-hoc
Auto input hoc
2 parents 90dfc78 + 3047bb6 commit fce894e

38 files changed

+731
-639
lines changed

packages/react/src/auto/AutoInput.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from "react";
2+
3+
export interface AutoInputComponent<P> extends React.FC<P> {
4+
__autoInput: true;
5+
}
6+
7+
export function autoInput<P extends { field: string }>(Component: React.FC<P>): AutoInputComponent<P> {
8+
const WrappedComponent: React.FC<P> = (props) => {
9+
return <Component {...props} />;
10+
};
11+
12+
(WrappedComponent as AutoInputComponent<P>).__autoInput = true;
13+
14+
return WrappedComponent as AutoInputComponent<P>;
15+
}
16+
17+
export function isAutoInput(component: React.ReactElement): component is React.ReactElement<any, AutoInputComponent<any>> {
18+
return typeof component.type === "function" && "__autoInput" in component.type;
19+
}
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React from "react";
2-
import { AutoRichTextInputProps } from "../../../auto/shared/AutoRichTextInputProps.js";
2+
import type { AutoRichTextInputProps } from "../../../auto/shared/AutoRichTextInputProps.js";
3+
import { autoInput } from "../../AutoInput.js";
34

45
// lazy import for smaller bundle size by default
56
const LazyLoadedMUIAutoRichTextInput = React.lazy(() => import("./MUIAutoRichTextInput.js"));
67

7-
export const MUIAutoRichTextInput = (props: AutoRichTextInputProps) => {
8+
export const MUIAutoRichTextInput = autoInput((props: AutoRichTextInputProps) => {
89
return (
910
<>
1011
<LazyLoadedMUIAutoRichTextInput {...props} />
1112
</>
1213
);
13-
};
14+
});

packages/react/src/auto/mui/inputs/MUIAutoBooleanInput.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,27 @@ import type { CheckboxProps } from "@mui/material";
22
import { Checkbox } from "@mui/material";
33
import React from "react";
44
import { useController, type Control } from "../../../useActionForm.js";
5+
import { autoInput } from "../../AutoInput.js";
56
import { useFieldMetadata } from "../../hooks/useFieldMetadata.js";
67
import { MUIAutoFormControl } from "./MUIAutoFormControl.js";
78

8-
export const MUIAutoBooleanInput = (props: { field: string; control?: Control<any>; label?: string } & Partial<CheckboxProps>) => {
9-
const { field: fieldApiIdentifier, label, control, ...rest } = props;
9+
export const MUIAutoBooleanInput = autoInput(
10+
(props: { field: string; control?: Control<any>; label?: string } & Partial<CheckboxProps>) => {
11+
const { field: fieldApiIdentifier, label, control, ...rest } = props;
1012

11-
const { path } = useFieldMetadata(fieldApiIdentifier);
13+
const { path } = useFieldMetadata(fieldApiIdentifier);
1214

13-
const { field: fieldProps } = useController({
14-
control,
15-
name: path,
16-
});
15+
const { field: fieldProps } = useController({
16+
control,
17+
name: path,
18+
});
1719

18-
const { value: _value, ...restFieldProps } = fieldProps;
20+
const { value: _value, ...restFieldProps } = fieldProps;
1921

20-
return (
21-
<MUIAutoFormControl field={props.field} label={label}>
22-
<Checkbox {...restFieldProps} checked={fieldProps.value} {...rest} />
23-
</MUIAutoFormControl>
24-
);
25-
};
22+
return (
23+
<MUIAutoFormControl field={props.field} label={label}>
24+
<Checkbox {...restFieldProps} checked={fieldProps.value} {...rest} />
25+
</MUIAutoFormControl>
26+
);
27+
}
28+
);

packages/react/src/auto/mui/inputs/MUIAutoDateTimePicker.tsx

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,40 @@ import React from "react";
44
import { zonedTimeToUtc } from "../../../dateTimeUtils.js";
55
import type { GadgetDateTimeConfig } from "../../../internal/gql/graphql.js";
66
import { useController } from "../../../useActionForm.js";
7+
import { autoInput } from "../../AutoInput.js";
78
import { useFieldMetadata } from "../../hooks/useFieldMetadata.js";
89

9-
export const MUIAutoDateTimePicker = (props: {
10-
field: string;
11-
value?: Date;
12-
onChange?: (value: Date) => void;
13-
error?: string;
14-
label?: string;
15-
}) => {
16-
const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
17-
const { path, metadata } = useFieldMetadata(props.field);
18-
const config = metadata.configuration;
19-
const isRequired = metadata.requiredArgumentForInput;
20-
const label = (props.label ?? metadata.name) + (isRequired ? " *" : "");
10+
export const MUIAutoDateTimePicker = autoInput(
11+
(props: { field: string; value?: Date; onChange?: (value: Date) => void; error?: string; label?: string }) => {
12+
const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
13+
const { path, metadata } = useFieldMetadata(props.field);
14+
const config = metadata.configuration;
15+
const isRequired = metadata.requiredArgumentForInput;
16+
const label = (props.label ?? metadata.name) + (isRequired ? " *" : "");
2117

22-
const { field: fieldProps, fieldState } = useController({ name: path });
18+
const { field: fieldProps, fieldState } = useController({ name: path });
2319

24-
return (
25-
<Box sx={{ display: "flex" }}>
26-
<DatePicker
27-
label={label}
28-
slotProps={{ textField: { error: !!fieldState.error, helperText: fieldState.error?.message } }}
29-
onChange={(newValue: string | number | Date | null) => {
30-
props.onChange?.(zonedTimeToUtc(new Date(newValue ?? ""), localTz));
31-
fieldProps.onChange(zonedTimeToUtc(new Date(newValue ?? ""), localTz));
32-
}}
33-
/>
34-
{(config as GadgetDateTimeConfig).includeTime && (
35-
<TimePicker
20+
return (
21+
<Box sx={{ display: "flex" }}>
22+
<DatePicker
23+
label={label}
24+
slotProps={{ textField: { error: !!fieldState.error, helperText: fieldState.error?.message } }}
3625
onChange={(newValue: string | number | Date | null) => {
3726
props.onChange?.(zonedTimeToUtc(new Date(newValue ?? ""), localTz));
3827
fieldProps.onChange(zonedTimeToUtc(new Date(newValue ?? ""), localTz));
3928
}}
4029
/>
41-
)}
42-
</Box>
43-
);
44-
};
30+
{(config as GadgetDateTimeConfig).includeTime && (
31+
<TimePicker
32+
onChange={(newValue: string | number | Date | null) => {
33+
props.onChange?.(zonedTimeToUtc(new Date(newValue ?? ""), localTz));
34+
fieldProps.onChange(zonedTimeToUtc(new Date(newValue ?? ""), localTz));
35+
}}
36+
/>
37+
)}
38+
</Box>
39+
);
40+
}
41+
);
4542

4643
export default MUIAutoDateTimePicker;

packages/react/src/auto/mui/inputs/MUIAutoEncryptedStringInput.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ import type { TextFieldProps } from "@mui/material";
22
import { IconButton } from "@mui/material";
33
import React, { useState } from "react";
44
import type { Control } from "../../../useActionForm.js";
5+
import { autoInput } from "../../AutoInput.js";
56
import { MUIAutoTextInput } from "./MUIAutoTextInput.js";
67

7-
export const MUIAutoEncryptedStringInput = (
8-
props: {
9-
field: string; // The field API identifier
10-
control?: Control<any>;
11-
} & Partial<TextFieldProps>
12-
) => {
13-
const [isShown, setIsShown] = useState(false);
14-
const showHideToggleButton = <IconButton onClick={() => setIsShown(!isShown)}>{isShown ? `🔒` : `👁️`}</IconButton>;
8+
export const MUIAutoEncryptedStringInput = autoInput(
9+
(
10+
props: {
11+
field: string; // The field API identifier
12+
control?: Control<any>;
13+
} & Partial<TextFieldProps>
14+
) => {
15+
const [isShown, setIsShown] = useState(false);
16+
const showHideToggleButton = <IconButton onClick={() => setIsShown(!isShown)}>{isShown ? `🔒` : `👁️`}</IconButton>;
1517

16-
return <MUIAutoTextInput InputProps={{ endAdornment: showHideToggleButton }} type={isShown ? "text" : "password"} {...props} />;
17-
};
18+
return <MUIAutoTextInput InputProps={{ endAdornment: showHideToggleButton }} type={isShown ? "text" : "password"} {...props} />;
19+
}
20+
);
Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,43 @@
11
import type { AutocompleteProps, ChipTypeMap } from "@mui/material";
22
import { Autocomplete, TextField } from "@mui/material";
33
import React from "react";
4+
import { autoInput } from "../../AutoInput.js";
45
import { useEnumInputController } from "../../hooks/useEnumInputController.js";
56

6-
export const MUIAutoEnumInput = <
7-
Value,
8-
Multiple extends boolean | undefined = false,
9-
DisableClearable extends boolean | undefined = false,
10-
FreeSolo extends boolean | undefined = false,
11-
ChipComponent extends React.ElementType = ChipTypeMap["defaultComponent"]
12-
>(
13-
props: { field: string; label?: string } & Partial<AutocompleteProps<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>>
14-
) => {
15-
const { allowMultiple, selectedOptions, onSelectionChange, allOptions, label } = useEnumInputController(props);
7+
export const MUIAutoEnumInput = autoInput(
8+
<
9+
Value,
10+
Multiple extends boolean | undefined = false,
11+
DisableClearable extends boolean | undefined = false,
12+
FreeSolo extends boolean | undefined = false,
13+
ChipComponent extends React.ElementType = ChipTypeMap["defaultComponent"]
14+
>(
15+
props: { field: string; label?: string } & Partial<AutocompleteProps<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>>
16+
) => {
17+
const { allowMultiple, selectedOptions, onSelectionChange, allOptions, label } = useEnumInputController(props);
1618

17-
return (
18-
<Autocomplete
19-
disablePortal
20-
multiple={allowMultiple}
21-
options={allOptions}
22-
renderInput={(params) => <TextField {...params} label={props.label ?? label} />}
23-
value={allowMultiple ? selectedOptions : selectedOptions[0]}
24-
onChange={(event, value) => {
25-
if (value === null || (Array.isArray(value) && value.length === 0)) {
26-
onSelectionChange(null);
27-
return;
28-
}
19+
return (
20+
<Autocomplete
21+
disablePortal
22+
multiple={allowMultiple}
23+
options={allOptions}
24+
renderInput={(params) => <TextField {...params} label={props.label ?? label} />}
25+
value={allowMultiple ? selectedOptions : selectedOptions[0]}
26+
onChange={(event, value) => {
27+
if (value === null || (Array.isArray(value) && value.length === 0)) {
28+
onSelectionChange(null);
29+
return;
30+
}
2931

30-
if (typeof value === "string") {
31-
onSelectionChange(value);
32-
} else {
33-
for (const option of value) {
34-
onSelectionChange(option);
32+
if (typeof value === "string") {
33+
onSelectionChange(value);
34+
} else {
35+
for (const option of value) {
36+
onSelectionChange(option);
37+
}
3538
}
36-
}
37-
}}
38-
/>
39-
);
40-
};
39+
}}
40+
/>
41+
);
42+
}
43+
);

packages/react/src/auto/mui/inputs/MUIAutoFileInput.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Button, styled } from "@mui/material";
22
import React from "react";
33
import type { Control } from "../../../useActionForm.js";
4+
import { autoInput } from "../../AutoInput.js";
45
import { useFileInputController } from "../../hooks/useFileInputController.js";
56
import { MUIAutoFormControl } from "./MUIAutoFormControl.js";
67

@@ -21,7 +22,7 @@ const VisuallyHiddenInput = styled("input")({
2122
width: 1,
2223
});
2324

24-
export const MUIAutoFileInput = (props: { field: string; control?: Control<any>; label?: string }) => {
25+
export const MUIAutoFileInput = autoInput((props: { field: string; control?: Control<any>; label?: string }) => {
2526
const { field: fieldApiIdentifier, control, label } = props;
2627
const { onFileUpload, metadata } = useFileInputController({
2728
field: fieldApiIdentifier,
@@ -43,4 +44,4 @@ export const MUIAutoFileInput = (props: { field: string; control?: Control<any>;
4344
</Button>
4445
</MUIAutoFormControl>
4546
);
46-
};
47+
});

packages/react/src/auto/mui/inputs/MUIAutoFormControl.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { FormControl, FormControlLabel, FormGroup, FormHelperText } from "@mui/m
22
import type { ReactElement } from "react";
33
import React from "react";
44
import { useController } from "../../../useActionForm.js";
5+
import { autoInput } from "../../AutoInput.js";
56
import { useFieldMetadata } from "../../hooks/useFieldMetadata.js";
67

7-
export const MUIAutoFormControl = (props: { field: string; children: ReactElement; label?: string }) => {
8+
export const MUIAutoFormControl = autoInput((props: { field: string; children: ReactElement; label?: string }) => {
89
const { path, metadata } = useFieldMetadata(props.field);
910
const {
1011
fieldState: { error },
@@ -20,4 +21,4 @@ export const MUIAutoFormControl = (props: { field: string; children: ReactElemen
2021
{error && <FormHelperText>{error?.message}</FormHelperText>}
2122
</FormControl>
2223
);
23-
};
24+
});
Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import React from "react";
2+
import { autoInput } from "../../AutoInput.js";
23
import { useHiddenInput } from "../../hooks/useHiddenInput.js";
34

4-
export const MUIAutoHiddenInput = (props: {
5-
field: string; // The field API identifier
6-
value: any;
7-
}) => {
8-
const fieldProps = useHiddenInput(props);
5+
export const MUIAutoHiddenInput = autoInput(
6+
(props: {
7+
field: string; // The field API identifier
8+
value: any;
9+
}) => {
10+
const fieldProps = useHiddenInput(props);
911

10-
return <input type="hidden" {...fieldProps} />;
11-
};
12+
return <input type="hidden" {...fieldProps} />;
13+
}
14+
);
Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
1-
import { FieldType } from "../../../metadata.js";
2-
31
import type { TextFieldProps } from "@mui/material";
42
import React from "react";
3+
import { FieldType } from "../../../metadata.js";
4+
import { autoInput } from "../../AutoInput.js";
55
import { useStringInputController } from "../../hooks/useStringInputController.js";
66
import { MUIAutoTextInput } from "./MUIAutoTextInput.js";
77

8-
export const MUIAutoIdInput = (
9-
props: {
10-
field: string;
11-
} & Partial<TextFieldProps>
12-
) => {
13-
const { field } = props;
14-
const { name, metadata } = useStringInputController({ field });
8+
export const MUIAutoIdInput = autoInput(
9+
(
10+
props: {
11+
field: string;
12+
} & Partial<TextFieldProps>
13+
) => {
14+
const { field } = props;
15+
const { name, metadata } = useStringInputController({ field });
1516

16-
if (metadata.fieldType !== FieldType.Id || field !== "id") {
17-
throw new Error(`PolarisAutoIdInput: field ${field} is not of type Id`);
18-
}
17+
if (metadata.fieldType !== FieldType.Id || field !== "id") {
18+
throw new Error(`PolarisAutoIdInput: field ${field} is not of type Id`);
19+
}
1920

20-
return (
21-
<MUIAutoTextInput
22-
field={field}
23-
label="ID"
24-
name={name}
25-
InputProps={{
26-
inputProps: { min: 1, step: 1 },
27-
}}
28-
type="number"
29-
/>
30-
);
31-
};
21+
return (
22+
<MUIAutoTextInput
23+
field={field}
24+
label="ID"
25+
name={name}
26+
InputProps={{
27+
inputProps: { min: 1, step: 1 },
28+
}}
29+
type="number"
30+
/>
31+
);
32+
}
33+
);

0 commit comments

Comments
 (0)