migrate remaining fields to react-hook-form

This commit is contained in:
Sidney Alcantara
2020-08-21 21:52:18 +10:00
parent 90cdd14cb7
commit d7bb345f9f
19 changed files with 792 additions and 822 deletions

View File

@@ -1,5 +1,5 @@
import React, { useContext, useState } from "react";
import { FieldProps } from "formik";
import { Control, Controller, useWatch } from "react-hook-form";
import {
createStyles,
@@ -40,87 +40,109 @@ const useStyles = makeStyles(theme =>
})
);
function Action({
field,
form,
editable,
config,
}: FieldProps<any> & { config: { callableName: string }; editable?: boolean }) {
export interface IActionProps {
control: Control;
name: string;
docRef: firebase.firestore.DocumentReference;
config: { callableName: string };
editable?: boolean;
}
function Action({ control, name, docRef, editable, config }: IActionProps) {
const classes = useStyles();
const { ref, ...docData } = form.values;
const docData = useWatch({ control });
const [isRunning, setIsRunning] = useState(false);
const snack = useContext(SnackContext);
const handleRun = () => {
setIsRunning(true);
console.log("RUN");
cloudFunction(
config.callableName,
{
ref: { path: ref.path, id: ref.id },
row: sanitiseRowData(Object.assign({}, docData)),
},
response => {
const { message, cellValue } = response.data;
setIsRunning(false);
snack.open({ message, severity: "success" });
if (cellValue) form.setFieldValue(field.name, cellValue);
},
error => {
console.error("ERROR", config.callableName, error);
setIsRunning(false);
snack.open({ message: JSON.stringify(error), severity: "error" });
}
);
};
const hasRan = field.value && field.value.status;
const disabled = editable === false;
return (
<Grid
container
alignItems="center"
wrap="nowrap"
className={classes.root}
spacing={2}
>
<Grid item xs className={classes.labelGridItem}>
<Grid container alignItems="center" className={classes.labelContainer}>
<Typography variant="body1" className={classes.label}>
{hasRan && isUrl(field.value.status) ? (
<a
href={field.value.status}
target="_blank"
rel="noopener noreferrer"
>
{field.value.status}
</a>
) : hasRan ? (
field.value.status
) : (
sanitiseCallableName(config.callableName)
)}
</Typography>
</Grid>
</Grid>
<Grid item>
<Fab
onClick={handleRun}
disabled={isRunning || !!(hasRan && !field.value.redo) || disabled}
>
{isRunning ? (
<CircularProgress color="secondary" size={24} thickness={4.6} />
) : hasRan ? (
<RefreshIcon />
) : (
<PlayIcon />
)}
</Fab>
</Grid>
</Grid>
return (
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
const handleRun = () => {
setIsRunning(true);
console.log("RUN");
cloudFunction(
config.callableName,
{
ref: { path: docRef.path, id: docRef.id },
row: sanitiseRowData(Object.assign({}, docData)),
},
response => {
const { message, cellValue } = response.data;
setIsRunning(false);
snack.open({ message, severity: "success" });
if (cellValue) onChange(cellValue);
},
error => {
console.error("ERROR", config.callableName, error);
setIsRunning(false);
snack.open({ message: JSON.stringify(error), severity: "error" });
}
);
};
const hasRan = value && value.status;
return (
<Grid
container
alignItems="center"
wrap="nowrap"
className={classes.root}
spacing={2}
>
<Grid item xs className={classes.labelGridItem}>
<Grid
container
alignItems="center"
className={classes.labelContainer}
>
<Typography variant="body1" className={classes.label}>
{hasRan && isUrl(value.status) ? (
<a
href={value.status}
target="_blank"
rel="noopener noreferrer"
>
{value.status}
</a>
) : hasRan ? (
value.status
) : (
sanitiseCallableName(config.callableName)
)}
</Typography>
</Grid>
</Grid>
<Grid item>
<Fab
onClick={handleRun}
disabled={isRunning || !!(hasRan && !value.redo) || disabled}
>
{isRunning ? (
<CircularProgress
color="secondary"
size={24}
thickness={4.6}
/>
) : hasRan ? (
<RefreshIcon />
) : (
<PlayIcon />
)}
</Fab>
</Grid>
</Grid>
);
}}
/>
);
}

View File

@@ -55,38 +55,36 @@ export default function Checkbox({
const classes = useStyles();
return (
<>
<ButtonBase className={classes.buttonBase}>
<FormControlLabel
control={
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
const handleChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
onChange(event.target.checked);
};
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange(event.target.checked);
};
return (
<Switch
color="secondary"
{...props}
checked={value}
onChange={handleChange}
onBlur={onBlur}
disabled={editable === false}
/>
);
}}
const handleClick = () => onChange(!value);
return (
<ButtonBase className={classes.buttonBase} onClick={handleClick}>
<FormControlLabel
control={
<Switch
color="secondary"
{...props}
checked={value}
onChange={handleChange}
onBlur={onBlur}
disabled={editable === false}
/>
}
label={label}
labelPlacement="start"
classes={{ root: classes.formControlLabel, label: classes.label }}
/>
}
label={label}
labelPlacement="start"
classes={{ root: classes.formControlLabel, label: classes.label }}
/>
</ButtonBase>
</>
</ButtonBase>
);
}}
/>
);
}

View File

@@ -1,12 +1,11 @@
import React, { useState, useEffect, useRef } from "react";
import { FieldProps } from "formik";
import { Controller, Control } from "react-hook-form";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/theme-monokai";
import "ace-builds/src-noconflict/theme-github";
import { makeStyles, createStyles, Button } from "@material-ui/core";
import ErrorMessage from "../ErrorMessage";
import CornerResizeIcon from "assets/icons/CornerResize";
const useStyles = makeStyles(theme =>
@@ -35,17 +34,23 @@ const useStyles = makeStyles(theme =>
})
);
export default function Code({ form, field }: FieldProps) {
export interface IControlledCodeProps {
onChange: (...event: any[]) => void;
onBlur: () => void;
value: any;
}
function ControlledCode({ onChange, onBlur, value }: IControlledCodeProps) {
const classes = useStyles();
const [localValue, setLocalValue] = useState(field.value);
const [localValue, setLocalValue] = useState(value);
useEffect(() => {
if (field.value !== localValue) setLocalValue(field.value);
}, [field.value]);
if (value !== localValue) setLocalValue(value);
}, [value]);
const autoSave = false;
const handleChange = autoSave
? value => form.setFieldValue(field.name, value)
? value => onChange(value)
: value => setLocalValue(value);
const editor = useRef<AceEditor>(null);
@@ -58,11 +63,9 @@ export default function Code({ form, field }: FieldProps) {
<>
<div className={classes.editorWrapper} onMouseUp={handleResize}>
<AceEditor
key={`${form.initialValues.id}-${field.name}`}
placeholder="Type code here…"
mode="javascript"
theme="github"
name={field.name}
//onLoad={this.onLoad}
onChange={handleChange}
fontSize={13}
@@ -71,7 +74,7 @@ export default function Code({ form, field }: FieldProps) {
showGutter
highlightActiveLine
showPrintMargin
value={autoSave ? field.value : localValue}
value={autoSave ? value : localValue}
setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
@@ -85,19 +88,30 @@ export default function Code({ form, field }: FieldProps) {
<CornerResizeIcon className={classes.resizeIcon} />
</div>
{!autoSave && field.value !== localValue && (
{!autoSave && value !== localValue && (
<Button
onClick={() => {
form.setFieldValue(field.name, localValue);
}}
onClick={() => onChange(localValue)}
className={classes.saveButton}
variant="contained"
>
Save Changes
</Button>
)}
<ErrorMessage name={field.name} />
</>
);
}
export interface ICodeProps {
control: Control;
name: string;
}
export default function Code({ control, name, ...props }: ICodeProps) {
return (
<Controller
control={control}
name={name}
render={renderProps => <ControlledCode {...props} {...renderProps} />}
/>
);
}

View File

@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { FieldProps } from "formik";
import { Controller, Control } from "react-hook-form";
import { ChromePicker } from "react-color";
import {
@@ -18,7 +18,16 @@ const useStyles = makeStyles(theme =>
cursor: "pointer",
textAlign: "left",
borderRadius: theme.shape.borderRadius,
backgroundColor:
theme.palette.type === "light"
? "rgba(0, 0, 0, 0.09)"
: "rgba(255, 255, 255, 0.09)",
margin: 0,
width: "100%",
padding: theme.spacing(0, 0.75),
},
colorIndicator: {
width: 20,
height: 20,
@@ -30,51 +39,57 @@ const useStyles = makeStyles(theme =>
})
);
export interface IColorProps extends FieldProps {}
export interface IColorProps {
control: Control;
name: string;
}
export default function Color({ field, form }: IColorProps) {
export default function Color({ control, name }: IColorProps) {
const classes = useStyles();
const [showPicker, setShowPicker] = useState(false);
const toggleOpen = () => setShowPicker(s => !s);
const handleChangeComplete = color => {
form.setFieldValue(field.name, color);
};
return (
<>
<Grid
container
alignItems="center"
spacing={1}
className={classes.root}
onClick={toggleOpen}
component={ButtonBase}
>
<Grid item>
<div
className={classes.colorIndicator}
style={{ backgroundColor: field.value.hex }}
/>
</Grid>
<Grid item xs>
<Typography
variant="body1"
color={field.value.hex ? "textPrimary" : "textSecondary"}
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => (
<>
<Grid
container
alignItems="center"
spacing={1}
className={classes.root}
onClick={() => {
toggleOpen();
onBlur();
}}
component={ButtonBase}
focusRipple
>
{field.value.hex ?? "Choose a color…"}
</Typography>
</Grid>
</Grid>
<Grid item>
<div
className={classes.colorIndicator}
style={{ backgroundColor: value?.hex }}
/>
</Grid>
<Collapse in={showPicker}>
<ChromePicker
color={field.value.rgb}
onChangeComplete={handleChangeComplete}
/>
</Collapse>
</>
<Grid item xs>
<Typography
variant="body1"
color={value?.hex ? "textPrimary" : "textSecondary"}
>
{value?.hex ?? "Choose a color…"}
</Typography>
</Grid>
</Grid>
<Collapse in={showPicker}>
<ChromePicker color={value?.rgb} onChangeComplete={onChange} />
</Collapse>
</>
)}
/>
);
}

View File

@@ -1,63 +1,75 @@
import React from "react";
import { FieldProps } from "formik";
import { Control, Controller } from "react-hook-form";
import { useTheme, Grid, Chip } from "@material-ui/core";
import ConnectTableSelect, {
ConnectTableValue,
IConnectTableSelectProps,
} from "components/ConnectTableSelect";
export interface IConnectTableProps extends Partial<IConnectTableSelectProps> {
control: Control;
name: string;
}
export default function ConnectTable({
field,
form,
control,
name,
editable,
...props
}: FieldProps<ConnectTableValue[]> & IConnectTableSelectProps) {
}: IConnectTableProps) {
const theme = useTheme();
const handleDelete = (hit: any) => () => {
// if (multiple)
form.setFieldValue(
field.name,
field.value.filter(v => v.snapshot.objectID !== hit.objectID)
);
// else form.setFieldValue(field.name, []);
};
const disabled = editable === false;
return (
<>
{!disabled && (
<ConnectTableSelect
{...props}
value={field.value}
onChange={value => form.setFieldValue(field.name, value)}
TextFieldProps={{
fullWidth: true,
error: !!(form.touched[field.name] && form.errors[field.name]),
helperText:
(form.touched[field.name] && form.errors[field.name]) || "",
onBlur: () => form.setFieldTouched(field.name),
}}
/>
)}
{Array.isArray(field.value) && (
<Grid container spacing={1} style={{ marginTop: theme.spacing(1) }}>
{field.value.map(({ snapshot }) => (
<Grid item key={snapshot.objectID}>
<Chip
component="li"
size="medium"
label={props.config?.primaryKeys
?.map((key: string) => snapshot[key])
.join(" ")}
onDelete={disabled ? undefined : handleDelete(snapshot)}
return (
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
const handleDelete = (hit: any) => () => {
// if (multiple)
onChange(value.filter(v => v.snapshot.objectID !== hit.objectID));
// else form.setFieldValue(field.name, []);
};
return (
<>
{!disabled && (
<ConnectTableSelect
{...(props as any)}
value={value}
onChange={onChange}
TextFieldProps={{
fullWidth: true,
onBlur,
}}
/>
</Grid>
))}
</Grid>
)}
</>
)}
{Array.isArray(value) && (
<Grid
container
spacing={1}
style={{ marginTop: theme.spacing(1) }}
>
{value.map(({ snapshot }) => (
<Grid item key={snapshot.objectID}>
<Chip
component="li"
size="medium"
label={props.config?.primaryKeys
?.map((key: string) => snapshot[key])
.join(" ")}
onDelete={disabled ? undefined : handleDelete(snapshot)}
/>
</Grid>
))}
</Grid>
)}
</>
);
}}
/>
);
}

View File

@@ -8,6 +8,9 @@ import {
} from "@material-ui/pickers";
import { DATE_FORMAT } from "constants/dates";
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
import DateFnsUtils from "@date-io/date-fns";
export interface IDatePickerProps
extends Omit<KeyboardDatePickerProps, "onChange" | "value"> {
control: Control;
@@ -22,38 +25,42 @@ export default function DatePicker({
const theme = useTheme();
return (
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
let transformedValue = null;
if (value && "toDate" in value) transformedValue = value.toDate();
else if (value !== undefined) transformedValue = value;
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
let transformedValue = null;
if (value && "toDate" in value) transformedValue = value.toDate();
else if (value !== undefined) transformedValue = value;
const handleChange = (date: Date | null) => {
if (isNaN(date?.valueOf() ?? 0)) return;
onChange(date);
};
const handleChange = (date: Date | null) => {
if (isNaN(date?.valueOf() ?? 0)) return;
onChange(date);
};
return (
<KeyboardDatePicker
variant="inline"
inputVariant="filled"
fullWidth
margin="none"
format={DATE_FORMAT}
placeholder={DATE_FORMAT}
InputAdornmentProps={{ style: { marginRight: theme.spacing(-1) } }}
{...props}
value={transformedValue}
onChange={handleChange}
onBlur={onBlur}
label=""
hiddenLabel
id={`sidedrawer-field-${name}`}
/>
);
}}
/>
return (
<KeyboardDatePicker
variant="inline"
inputVariant="filled"
fullWidth
margin="none"
format={DATE_FORMAT}
placeholder={DATE_FORMAT}
InputAdornmentProps={{
style: { marginRight: theme.spacing(-1) },
}}
{...props}
value={transformedValue}
onChange={handleChange}
onBlur={onBlur}
label=""
hiddenLabel
id={`sidedrawer-field-${name}`}
/>
);
}}
/>
</MuiPickersUtilsProvider>
);
}

View File

@@ -10,6 +10,9 @@ import { DATE_TIME_FORMAT } from "constants/dates";
import AccessTimeIcon from "@material-ui/icons/AccessTime";
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
import DateFnsUtils from "@date-io/date-fns";
export interface IDateTimePickerProps
extends Omit<KeyboardDateTimePickerProps, "onChange" | "value"> {
control: Control;
@@ -24,39 +27,43 @@ export default function DateTimePicker({
const theme = useTheme();
return (
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
let transformedValue = null;
if (value && "toDate" in value) transformedValue = value.toDate();
else if (value !== undefined) transformedValue = value;
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
let transformedValue = null;
if (value && "toDate" in value) transformedValue = value.toDate();
else if (value !== undefined) transformedValue = value;
const handleChange = (date: Date | null) => {
if (isNaN(date?.valueOf() ?? 0)) return;
onChange(date);
};
const handleChange = (date: Date | null) => {
if (isNaN(date?.valueOf() ?? 0)) return;
onChange(date);
};
return (
<KeyboardDateTimePicker
variant="inline"
inputVariant="filled"
fullWidth
margin="none"
format={DATE_TIME_FORMAT}
placeholder={DATE_TIME_FORMAT}
InputAdornmentProps={{ style: { marginRight: theme.spacing(-1) } }}
keyboardIcon={<AccessTimeIcon />}
{...props}
value={transformedValue}
onChange={handleChange}
onBlur={onBlur}
label=""
hiddenLabel
id={`sidedrawer-field-${name}`}
/>
);
}}
/>
return (
<KeyboardDateTimePicker
variant="inline"
inputVariant="filled"
fullWidth
margin="none"
format={DATE_TIME_FORMAT}
placeholder={DATE_TIME_FORMAT}
InputAdornmentProps={{
style: { marginRight: theme.spacing(-1) },
}}
keyboardIcon={<AccessTimeIcon />}
{...props}
value={transformedValue}
onChange={handleChange}
onBlur={onBlur}
label=""
hiddenLabel
id={`sidedrawer-field-${name}`}
/>
);
}}
/>
</MuiPickersUtilsProvider>
);
}

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from "react";
import clsx from "clsx";
import { FieldProps } from "formik";
import { Controller, Control } from "react-hook-form";
import { useDropzone } from "react-dropzone";
import useUploader, { FileValue } from "hooks/useFiretable/useUploader";
@@ -19,7 +19,6 @@ import {
import UploadIcon from "assets/icons/Upload";
import { FileIcon } from "constants/fields";
import ErrorMessage from "../ErrorMessage";
import Confirmation from "components/Confirmation";
const useStyles = makeStyles(theme =>
@@ -55,17 +54,21 @@ const useStyles = makeStyles(theme =>
})
);
export interface IFileUploaderProps extends FieldProps {
editable?: boolean;
docRef?: firebase.firestore.DocumentReference;
export interface IControlledFileUploaderProps
extends Pick<IFileUploaderProps, "editable" | "docRef" | "name"> {
onChange: (...event: any[]) => void;
onBlur: () => void;
value: any;
}
export default function FileUploader({
form,
field,
export function ControlledFileUploader({
onChange,
onBlur,
value,
name,
docRef,
editable,
}: IFileUploaderProps) {
}: IControlledFileUploaderProps) {
const classes = useStyles();
const { uploaderState, upload, deleteUpload } = useUploader();
@@ -81,25 +84,25 @@ export default function FileUploader({
if (docRef && file) {
upload({
docRef,
fieldName: field.name,
fieldName: name,
files: [file],
previousValue: field.value ?? [],
previousValue: value ?? [],
onComplete: newValue => {
form.setFieldValue(field.name, newValue);
onChange(newValue);
setLocalFile("");
},
});
setLocalFile(file.name);
}
},
[docRef, field.value]
[docRef, value]
);
const handleDelete = (index: number) => {
const newValue = [...field.value];
const newValue = [...value];
const toBeDeleted = newValue.splice(index, 1);
toBeDeleted.length && deleteUpload(toBeDeleted[0]);
form.setFieldValue(field.name, newValue);
onChange(newValue);
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
@@ -117,7 +120,7 @@ export default function FileUploader({
)}
{...getRootProps()}
>
<input id={`sidedrawer-field-${field.name}`} {...getInputProps()} />
<input id={`sidedrawer-field-${name}`} {...getInputProps()} />
<UploadIcon />
<Typography variant="body1" color="textSecondary">
Upload file
@@ -126,8 +129,8 @@ export default function FileUploader({
)}
<Grid container spacing={1} className={classes.chipList}>
{Array.isArray(field.value) &&
field.value.map((file: FileValue, i) => (
{Array.isArray(value) &&
value.map((file: FileValue, i) => (
<Grid item key={file.name} className={classes.chipGridItem}>
<Confirmation
message={{
@@ -166,8 +169,30 @@ export default function FileUploader({
</Grid>
)}
</Grid>
<ErrorMessage name={field.name} />
</>
);
}
export interface IFileUploaderProps {
control: Control;
name: string;
editable?: boolean;
docRef?: firebase.firestore.DocumentReference;
}
export default function FileUploader({
control,
name,
...props
}: IFileUploaderProps) {
return (
<Controller
control={control}
name={name}
render={renderProps => (
<ControlledFileUploader {...props} name={name} {...renderProps} />
)}
/>
);
}

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from "react";
import clsx from "clsx";
import { FieldProps } from "formik";
import { Controller, Control } from "react-hook-form";
import { useDropzone } from "react-dropzone";
import useUploader from "hooks/useFiretable/useUploader";
@@ -91,17 +91,21 @@ const useStyles = makeStyles(theme =>
})
);
export interface IImageUploaderProps extends FieldProps {
docRef?: firebase.firestore.DocumentReference;
editable?: boolean;
export interface IControlledImageUploaderProps
extends Pick<IImageUploaderProps, "docRef" | "editable" | "name"> {
onChange: (...event: any[]) => void;
onBlur: () => void;
value: any;
}
export default function ImageUploader({
form,
field,
export function ControlledImageUploader({
onChange,
onBlur,
value,
name,
editable,
docRef,
}: IImageUploaderProps) {
}: IControlledImageUploaderProps) {
const disabled = editable === false;
const classes = useStyles();
@@ -118,25 +122,25 @@ export default function ImageUploader({
if (docRef && imageFile) {
upload({
docRef,
fieldName: field.name,
fieldName: name,
files: [imageFile],
previousValue: field.value ?? [],
previousValue: value ?? [],
onComplete: newValue => {
form.setFieldValue(field.name, newValue);
onChange(newValue);
setLocalImage("");
},
});
setLocalImage(URL.createObjectURL(imageFile));
}
},
[docRef, field.value]
[docRef, value]
);
const handleDelete = (index: number) => {
const newValue = [...field.value];
const newValue = [...value];
const toBeDeleted = newValue.splice(index, 1);
toBeDeleted.length && deleteUpload(toBeDeleted[0]);
form.setFieldValue(field.name, newValue);
onChange(newValue);
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
@@ -155,7 +159,7 @@ export default function ImageUploader({
)}
{...getRootProps()}
>
<input id={`sidedrawer-field-${field.name}`} {...getInputProps()} />
<input id={`sidedrawer-field-${name}`} {...getInputProps()} />
<AddIcon />
<Typography variant="body1" color="inherit">
{isDragActive ? "Drop your image here" : "Upload image"}
@@ -164,8 +168,8 @@ export default function ImageUploader({
)}
<Grid container spacing={1} className={classes.imagesContainer}>
{Array.isArray(field.value) &&
field.value.map((image, i) => (
{Array.isArray(value) &&
value.map((image, i) => (
<Grid item key={image.downloadURL}>
{disabled ? (
<Tooltip title="Click to open">
@@ -246,8 +250,29 @@ export default function ImageUploader({
</Grid>
)}
</Grid>
<ErrorMessage name={field.name} />
</>
);
}
export interface IImageUploaderProps {
control: Control;
name: string;
docRef?: firebase.firestore.DocumentReference;
editable?: boolean;
}
export default function ImageUploader({
control,
name,
...props
}: IImageUploaderProps) {
return (
<Controller
control={control}
name={name}
render={renderProps => (
<ControlledImageUploader {...props} name={name} {...renderProps} />
)}
/>
);
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { FieldProps } from "formik";
import { Control, Controller } from "react-hook-form";
import ReactJson from "react-json-view";
import { makeStyles, createStyles, useTheme } from "@material-ui/core";
@@ -32,45 +32,59 @@ const isValidJson = (val: any) => {
return true;
};
export default function JsonEditor({ form, field }: FieldProps) {
export interface IJsonEditorProps {
control: Control;
name: string;
}
export default function JsonEditor({ control, name }: IJsonEditorProps) {
const classes = useStyles();
const theme = useTheme();
const handleEdit = edit => {
form.setFieldValue(field.name, edit.updated_src);
};
return (
<div className={classes.root}>
<ReactJson
src={isValidJson(field.value) ? field.value : {}}
onEdit={handleEdit}
onAdd={handleEdit}
onDelete={handleEdit}
theme={{
base00: "rgba(0, 0, 0, 0)",
base01: theme.palette.background.default,
base02: theme.palette.divider,
base03: "#93a1a1",
base04: theme.palette.text.disabled,
base05: theme.palette.text.secondary,
base06: "#073642",
base07: theme.palette.text.primary,
base08: "#d33682",
base09: "#cb4b16",
base0A: "#dc322f",
base0B: "#859900",
base0C: "#6c71c4",
base0D: theme.palette.text.secondary,
base0E: "#2aa198",
base0F: "#268bd2",
}}
iconStyle="triangle"
style={{
fontFamily: "SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace",
backgroundColor: "transparent",
}}
/>
</div>
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
const handleEdit = edit => {
onChange(edit.updated_src);
};
return (
<div className={classes.root}>
<ReactJson
src={isValidJson(value) ? value : {}}
onEdit={handleEdit}
onAdd={handleEdit}
onDelete={handleEdit}
theme={{
base00: "rgba(0, 0, 0, 0)",
base01: theme.palette.background.default,
base02: theme.palette.divider,
base03: "#93a1a1",
base04: theme.palette.text.disabled,
base05: theme.palette.text.secondary,
base06: "#073642",
base07: theme.palette.text.primary,
base08: "#d33682",
base09: "#cb4b16",
base0A: "#dc322f",
base0B: "#859900",
base0C: "#6c71c4",
base0D: theme.palette.text.secondary,
base0E: "#2aa198",
base0F: "#268bd2",
}}
iconStyle="triangle"
style={{
fontFamily:
"SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace",
backgroundColor: "transparent",
}}
/>
</div>
);
}}
/>
);
}

View File

@@ -1,82 +0,0 @@
import React from "react";
import {
makeStyles,
createStyles,
FormControl,
FormControlLabel,
Radio as MuiRadio,
Divider,
} from "@material-ui/core";
import { RadioGroup, RadioGroupProps } from "formik-material-ui";
import Label from "../Label";
import ErrorMessage from "../ErrorMessage";
const useStyles = makeStyles(theme =>
createStyles({
root: { display: "flex" },
formControlLabel: {
padding: theme.spacing(1.25, 0),
marginLeft: theme.spacing(1),
},
divider: { marginLeft: theme.spacing(5) },
})
);
export interface IRadioProps extends RadioGroupProps {
options: (string | { value: string; label: React.ReactNode })[];
label?: React.ReactNode;
}
export default function Radio({ options, label, ...props }: IRadioProps) {
const classes = useStyles();
return (
<FormControl component="fieldset" className={classes.root}>
<Label
error={
!!(
props.form.errors[props.field.name] &&
props.form.touched[props.field.name]
)
}
>
{label}
</Label>
<RadioGroup {...props}>
{options.map(item => {
let option: { label: React.ReactNode; value: string } = {
label: "",
value: "",
};
if (typeof item === "object") option = item;
if (typeof item === "string") option = { label: item, value: item };
return (
<React.Fragment key={option.value}>
<FormControlLabel
key={option.value}
value={option.value}
label={option.label}
control={
<MuiRadio
//disabled={isSubmitting}
/>
}
classes={{ label: classes.formControlLabel }}
//disabled={isSubmitting}
/>
<Divider className={classes.divider} />
</React.Fragment>
);
})}
</RadioGroup>
<ErrorMessage name={props.field.name} />
</FormControl>
);
}

View File

@@ -1,12 +1,10 @@
import React from "react";
import { FieldProps } from "formik";
import { Controller, Control } from "react-hook-form";
import { makeStyles, createStyles, Grid } from "@material-ui/core";
import { Rating as MuiRating } from "@material-ui/lab";
import StarBorderIcon from "@material-ui/icons/StarBorder";
import ErrorMessage from "../ErrorMessage";
const useStyles = makeStyles(theme =>
createStyles({
root: {
@@ -27,32 +25,36 @@ const useStyles = makeStyles(theme =>
})
);
export interface IRatingProps extends FieldProps {
export interface IRatingProps {
control: Control;
name: string;
editable?: boolean;
}
export default function Rating(props: IRatingProps) {
export default function Rating({ control, name, editable }: IRatingProps) {
const classes = useStyles();
return (
<>
<Grid container alignItems="center" className={classes.root}>
<MuiRating
disabled={props.editable === false}
name={props.field.name}
id={`sidedrawer-field-${props.field.name}`}
value={typeof props.field.value === "number" ? props.field.value : 0}
onChange={(event, newValue) => {
props.form.setFieldValue(props.field.name, newValue);
}}
emptyIcon={<StarBorderIcon fontSize="inherit" />}
classes={{ root: classes.rating, iconEmpty: classes.iconEmpty }}
// TODO: Make this customisable in column settings
max={4}
/>
</Grid>
<ErrorMessage name={props.field.name} />
</>
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => (
<Grid container alignItems="center" className={classes.root}>
<MuiRating
disabled={editable === false}
name={name}
id={`sidedrawer-field-${name}`}
value={typeof value === "number" ? value : 0}
onChange={(event, newValue) => {
onChange(newValue);
}}
emptyIcon={<StarBorderIcon fontSize="inherit" />}
classes={{ root: classes.rating, iconEmpty: classes.iconEmpty }}
// TODO: Make this customisable in column settings
max={4}
/>
</Grid>
)}
/>
);
}

View File

@@ -1,16 +1,21 @@
import React from "react";
import { FieldProps } from "formik";
import { Control, Controller } from "react-hook-form";
import _RichText from "components/RichText";
import ErrorMessage from "../ErrorMessage";
export default function RichText({ form, field }: FieldProps) {
const handleChange = value => form.setFieldValue(field.name, value);
export interface IRichTextProps {
control: Control;
name: string;
}
export default function RichText({ control, name }: IRichTextProps) {
return (
<>
<_RichText value={field.value} onChange={handleChange} />
<ErrorMessage name={field.name} />
</>
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => (
<_RichText value={value} onChange={onChange} />
)}
/>
);
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { FieldProps } from "formik";
import { Controller, Control } from "react-hook-form";
import {
makeStyles,
@@ -11,8 +11,6 @@ import {
Typography,
} from "@material-ui/core";
import ErrorMessage from "../ErrorMessage";
const useStyles = makeStyles(theme =>
createStyles({
root: { display: "flex" },
@@ -45,17 +43,17 @@ const useStyles = makeStyles(theme =>
})
);
export interface ISliderProps extends FieldProps, SliderProps {
label: React.ReactNode;
export interface ISliderProps extends SliderProps {
control: Control;
name: string;
units?: string;
minLabel?: React.ReactNode;
maxLabel?: React.ReactNode;
}
export default function Slider({
field,
form,
label,
control,
name,
units,
minLabel,
maxLabel,
@@ -65,54 +63,67 @@ export default function Slider({
}: ISliderProps) {
const classes = useStyles();
const handleClick = () => form.setFieldTouched(field.name);
const handleChange = (event: any, value: number | number[]) => {
form.setFieldValue(field.name, value);
form.setFieldTouched(field.name);
};
const getAriaValueText = (value: number) =>
`${value}${units ? " " + units : ""}`;
const getValueLabelFormat = (value: number) =>
`${value}${units ? " " + units : ""}`;
return (
<FormControl className={classes.root}>
<Grid container spacing={2} alignItems="center">
<Grid item>
<Typography variant="overline" component="span" color="textSecondary">
{minLabel ?? `${min}${units ? " " + units : ""}`}
</Typography>
</Grid>
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
const handleChange = (_: any, value: number | number[]) => {
onChange(value);
onBlur();
};
<Grid item xs>
<MuiSlider
valueLabelDisplay="auto"
min={min}
max={max}
getAriaValueText={getAriaValueText}
valueLabelFormat={getValueLabelFormat}
{...props}
value={typeof field.value === "number" ? field.value : min}
onClick={handleClick}
onChange={handleChange}
classes={{
root: classes.slider,
thumb: classes.thumb,
valueLabel: classes.valueLabel,
}}
/>
</Grid>
const getAriaValueText = (value: number) =>
`${value}${units ? " " + units : ""}`;
<Grid item>
<Typography variant="overline" component="span" color="textSecondary">
{maxLabel ?? `${max}${units ? " " + units : ""}`}
</Typography>
</Grid>
</Grid>
const getValueLabelFormat = (value: number) =>
`${value}${units ? " " + units : ""}`;
<ErrorMessage name={field.name} />
</FormControl>
return (
<FormControl className={classes.root}>
<Grid container spacing={2} alignItems="center">
<Grid item>
<Typography
variant="overline"
component="span"
color="textSecondary"
>
{minLabel ?? `${min}${units ? " " + units : ""}`}
</Typography>
</Grid>
<Grid item xs>
<MuiSlider
valueLabelDisplay="auto"
min={min}
max={max}
getAriaValueText={getAriaValueText}
valueLabelFormat={getValueLabelFormat}
{...props}
value={value ?? min}
onClick={onBlur}
onChange={handleChange}
classes={{
root: classes.slider,
thumb: classes.thumb,
valueLabel: classes.valueLabel,
}}
/>
</Grid>
<Grid item>
<Typography
variant="overline"
component="span"
color="textSecondary"
>
{maxLabel ?? `${max}${units ? " " + units : ""}`}
</Typography>
</Grid>
</Grid>
</FormControl>
);
}}
/>
);
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { FieldProps } from "formik";
import { Control, useWatch } from "react-hook-form";
import { Link } from "react-router-dom";
import queryString from "query-string";
@@ -31,30 +31,41 @@ const useStyles = makeStyles(theme =>
})
);
export interface ISubTableProps {
control: Control;
name: string;
docRef: firebase.firestore.DocumentReference;
config: { parentLabel?: string[] };
label: string;
}
export default function SubTable({
form,
field,
control,
name,
docRef,
label,
config,
}: FieldProps<any> & { config: { parentLabel?: string[] }; label: string }) {
}: ISubTableProps) {
const classes = useStyles();
const values = useWatch({ control });
const router = useRouter();
const parentLabels = queryString.parse(router.location.search).parentLabel;
const _label = config?.parentLabel
? config.parentLabel.reduce((acc, curr) => {
if (acc !== "") return `${acc} - ${form.values[curr]}`;
else return form.values[curr];
if (acc !== "") return `${acc} - ${values[curr]}`;
else return values[curr];
}, "")
: "";
let subTablePath = "";
if (parentLabels)
subTablePath =
encodeURIComponent(`${form.values.ref.path}/${field.name}`) +
encodeURIComponent(`${docRef.path}/${name}`) +
`?parentLabel=${parentLabels},${label}`;
else
subTablePath =
encodeURIComponent(`${form.values.ref.path}/${field.name}`) +
encodeURIComponent(`${docRef.path}/${name}`) +
`?parentLabel=${encodeURIComponent(_label)}`;
return (

View File

@@ -1,4 +1,5 @@
import React from "react";
import { Controller, Control } from "react-hook-form";
import {
makeStyles,
@@ -13,12 +14,23 @@ const useStyles = makeStyles(theme =>
})
);
export interface ITextProps extends Omit<FilledTextFieldProps, "variant"> {
fieldVariant?: "short" | "long" | "email" | "phone" | "number" | "url";
export interface IFieldProps {
control: Control;
name: string;
docRef: firebase.firestore.DocumentReference;
editable?: boolean;
}
export interface ITextProps
extends IFieldProps,
Omit<FilledTextFieldProps, "variant" | "name"> {
fieldVariant?: "short" | "long" | "email" | "phone" | "number" | "url";
}
export default function Text({
control,
name,
docRef,
fieldVariant = "short",
editable,
...props
@@ -48,7 +60,7 @@ export default function Text({
break;
case "number":
variantProps = { type: "number" };
variantProps = { inputmode: "numeric", pattern: "[0-9]*" };
break;
case "short":
@@ -57,19 +69,33 @@ export default function Text({
}
return (
<TextField
//key={`${props.form.initialValues.id}-${props.field.name}`}
variant="filled"
fullWidth
margin="none"
placeholder={props.label as string}
{...variantProps}
{...props}
id={`sidedrawer-field-${props.name}`}
label=""
hiddenLabel
disabled={editable === false}
//InputProps={{ disableUnderline: true }}
<Controller
control={control}
name={name}
render={({ onChange, onBlur, value }) => {
const handleChange = e => {
if (fieldVariant === "number") onChange(parseInt(e.target.value, 10));
else onChange(e.target.value);
};
return (
<TextField
variant="filled"
fullWidth
margin="none"
placeholder={props.label as string}
{...variantProps}
{...props}
onChange={handleChange}
onBlur={onBlur}
value={value}
id={`sidedrawer-field-${name}`}
label=""
hiddenLabel
disabled={editable === false}
/>
);
}}
/>
);
}

View File

@@ -1,124 +0,0 @@
import React, { useState } from "react";
import { FieldProps } from "formik";
import {
makeStyles,
createStyles,
FormControl,
Grid,
TextField,
IconButton,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
Divider,
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import DeleteIcon from "@material-ui/icons/Cancel";
import Label from "../Label";
import ErrorMessage from "../ErrorMessage";
const useStyles = makeStyles(theme =>
createStyles({
root: { display: "flex" },
list: { marginBottom: theme.spacing(2) },
})
);
export interface ITextMultiProps extends FieldProps {
label: React.ReactNode;
addItemLabel?: string;
addItemPlaceholder?: string;
}
export default function TextMulti({
form,
field,
label,
addItemLabel,
addItemPlaceholder,
}: ITextMultiProps) {
const classes = useStyles();
const [itemToAdd, setItemToAdd] = useState("");
const handleAddToList = () => {
if (Array.isArray(field.value))
form.setFieldValue(field.name, [...field.value, itemToAdd]);
else form.setFieldValue(field.name, [itemToAdd]);
setItemToAdd("");
};
const handleDeleteFromList = (i: number) => {
if (!Array.isArray(field.value)) form.setFieldValue(field.name, []);
const newValues = [...field.value];
newValues.splice(i, 1);
form.setFieldValue(field.name, newValues);
};
return (
<FormControl className={classes.root}>
<Label error={!!(form.errors[field.name] && form.touched[field.name])}>
{label}
</Label>
<List className={classes.list} disablePadding>
{Array.isArray(field.value) &&
field.value.map((item: string, i: number) => (
<React.Fragment key={i}>
<ListItem>
<ListItemText primary={item} />
<ListItemSecondaryAction>
<IconButton
edge="end"
aria-label="Remove"
onClick={() => {
handleDeleteFromList(i);
form.setFieldTouched(field.name);
}}
size="small"
>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
<Divider />
</React.Fragment>
))}
</List>
<Grid container alignItems="center" spacing={1}>
<Grid item>
<IconButton
onClick={() => {
handleAddToList();
form.setFieldTouched(field.name);
}}
>
<AddIcon fontSize="small" />
</IconButton>
</Grid>
<Grid item xs>
<TextField
id={`${field.name}-temp`}
type="text"
onChange={e => setItemToAdd(e.target.value)}
variant="filled"
fullWidth
value={itemToAdd}
label={addItemLabel || `Add ${label}`}
placeholder={addItemPlaceholder}
onKeyPress={e => {
if (e.key === "Enter") handleAddToList();
}}
// NOTE: Field is not automatically touched, has to be set here
onBlur={() => form.setFieldTouched(field.name)}
/>
</Grid>
</Grid>
<ErrorMessage name={field.name} />
</FormControl>
);
}

View File

@@ -31,7 +31,7 @@ export default function Url({ control, name, ...props }: IUrlProps) {
{...props}
onChange={onChange}
onBlur={onBlur}
value={value}
value={value || ""}
id={`sidedrawer-field-${name}`}
label=""
hiddenLabel

View File

@@ -1,11 +1,9 @@
import React, { lazy, useEffect } from "react";
import { useForm, useWatch, Control } from "react-hook-form";
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
import DateFnsUtils from "@date-io/date-fns";
import React, { lazy } from "react";
import { useForm } from "react-hook-form";
import _isFunction from "lodash/isFunction";
import _isEmpty from "lodash/isEmpty";
import { useFiretableContext } from "contexts/firetableContext";
// import { useFiretableContext } from "contexts/firetableContext";
import { Grid } from "@material-ui/core";
@@ -143,16 +141,13 @@ export interface IFormProps {
export default function Form({ fields, values }: IFormProps) {
const initialValues = getInitialValues(fields);
const { ref: firestoreRef, ...rowValues } = values;
const { ref: docRef, ...rowValues } = values;
const defaultValues = { ...initialValues, ...rowValues };
const { register, handleSubmit, control, reset } = useForm({
const { register, control } = useForm({
mode: "onBlur",
defaultValues,
});
const onSubmit = data => {
console.log("SUBMIT", data);
};
// Update field values when Firestore document updates
// useEffect(() => {
@@ -173,181 +168,168 @@ export default function Form({ fields, values }: IFormProps) {
// }, [sideDrawerRef?.current]);
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<form onSubmit={handleSubmit(onSubmit)} key={values.id}>
<AutosaveTwo control={control} defaultValues={defaultValues} />
<form>
<Autosave
control={control}
defaultValues={defaultValues}
docRef={docRef}
/>
<Grid container spacing={4} direction="column" wrap="nowrap">
{fields.map((_field, i) => {
// Call the field function with values if necessary
// Otherwise, just use the field object
const field: Field = _isFunction(_field) ? _field(values) : _field;
const { type, ...fieldProps } = field;
let _type = type;
<Grid container spacing={4} direction="column" wrap="nowrap">
{fields.map((_field, i) => {
// Call the field function with values if necessary
// Otherwise, just use the field object
const field: Field = _isFunction(_field) ? _field(values) : _field;
const { type, ...fieldProps } = field;
let _type = type;
// Derivative field support
if (field.config && field.config.renderFieldType) {
_type = field.config.renderFieldType;
}
// Derivative field support
if (field.config && field.config.renderFieldType) {
_type = field.config.renderFieldType;
}
let renderedField: React.ReactNode = null;
let renderedField: React.ReactNode = null;
switch (_type) {
case FieldType.shortText:
case FieldType.longText:
case FieldType.email:
case FieldType.phone:
case FieldType.number:
renderedField = (
<Text {...fieldProps} hiddenLabel inputRef={register} />
);
break;
switch (_type) {
case FieldType.shortText:
case FieldType.longText:
case FieldType.email:
case FieldType.phone:
case FieldType.number:
renderedField = (
<Text {...fieldProps} control={control} docRef={docRef} />
);
break;
case FieldType.url:
renderedField = <Url {...fieldProps} control={control} />;
break;
case FieldType.url:
renderedField = <Url {...fieldProps} control={control} />;
break;
case FieldType.singleSelect:
renderedField = (
<SingleSelect {...fieldProps} control={control} />
);
break;
case FieldType.singleSelect:
renderedField = (
<SingleSelect {...fieldProps} control={control} />
);
break;
case FieldType.multiSelect:
renderedField = (
<MultiSelect {...fieldProps} control={control} />
);
break;
case FieldType.multiSelect:
renderedField = <MultiSelect {...fieldProps} control={control} />;
break;
case FieldType.date:
renderedField = (
<DatePicker {...fieldProps} control={control} />
);
break;
case FieldType.date:
renderedField = <DatePicker {...fieldProps} control={control} />;
break;
case FieldType.dateTime:
renderedField = (
<DateTimePicker {...fieldProps} control={control} />
);
break;
case FieldType.dateTime:
renderedField = (
<DateTimePicker {...fieldProps} control={control} />
);
break;
case FieldType.checkbox:
renderedField = <Checkbox {...fieldProps} control={control} />;
break;
case FieldType.checkbox:
renderedField = <Checkbox {...fieldProps} control={control} />;
break;
// case FieldType.color:
// renderedField = <Field {...fieldProps} component={Color} />;
// break;
case FieldType.color:
renderedField = <Color {...fieldProps} control={control} />;
break;
// case FieldType.slider:
// renderedField = (
// <Field {...fieldProps} component={Slider} />
// );
// break;
case FieldType.slider:
renderedField = <Slider {...fieldProps} control={control} />;
break;
// case FieldType.richText:
// renderedField = (
// <Field {...fieldProps} component={RichText} />
// );
// break;
case FieldType.richText:
renderedField = <RichText {...fieldProps} control={control} />;
break;
// case FieldType.image:
// renderedField = (
// <Field
// {...fieldProps}
// component={ImageUploader}
// docRef={values.ref}
// />
// );
// break;
case FieldType.image:
renderedField = (
<ImageUploader
{...fieldProps}
control={control}
docRef={values.ref}
/>
);
break;
// case FieldType.file:
// renderedField = (
// <Field
// {...fieldProps}
// component={FileUploader}
// docRef={values.ref}
// />
// );
// break;
case FieldType.file:
renderedField = (
<FileUploader
{...fieldProps}
control={control}
docRef={values.ref}
/>
);
break;
// case FieldType.rating:
// renderedField = (
// <Field {...fieldProps} component={Rating} />
// );
// break;
case FieldType.rating:
renderedField = <Rating {...fieldProps} control={control} />;
break;
case FieldType.percentage:
renderedField = (
<Percentage {...fieldProps} control={control} />
);
break;
case FieldType.percentage:
renderedField = <Percentage {...fieldProps} control={control} />;
break;
// case FieldType.connectTable:
// renderedField = (
// <Field {...fieldProps} component={ConnectTable} />
// );
// break;
case FieldType.connectTable:
renderedField = (
<ConnectTable {...fieldProps} control={control} />
);
break;
// case FieldType.subTable:
// renderedField = (
// <Field {...fieldProps} component={SubTable} />
// );
// break;
case FieldType.subTable:
renderedField = (
<SubTable
{...(fieldProps as any)}
control={control}
docRef={docRef}
/>
);
break;
// case FieldType.action:
// renderedField = (
// <Field {...fieldProps} component={Action} />
// );
// break;
case FieldType.action:
renderedField = (
<Action
{...(fieldProps as any)}
control={control}
docRef={docRef}
/>
);
break;
// case FieldType.json:
// renderedField = (
// <Field {...fieldProps} component={JsonEditor} />
// );
// break;
// case FieldType.code:
// renderedField = <Field {...fieldProps} component={Code} />;
// break;
case undefined:
// default:
return null;
case FieldType.json:
renderedField = <JsonEditor {...fieldProps} control={control} />;
break;
default:
break;
}
case FieldType.code:
renderedField = <Code {...fieldProps} control={control} />;
break;
return (
<FieldWrapper
key={fieldProps.name ?? i}
type={_type}
name={field.name}
label={field.label}
>
{renderedField}
</FieldWrapper>
);
})}
case undefined:
// default:
return null;
<FieldWrapper
type="debug"
name="_ft_debug_path"
label="Document Path"
debugText={values.ref?.path ?? values.id ?? "No ref"}
/>
</Grid>
</form>
</MuiPickersUtilsProvider>
default:
break;
}
return (
<FieldWrapper
key={fieldProps.name ?? i}
type={_type}
name={field.name}
label={field.label}
>
{renderedField}
</FieldWrapper>
);
})}
<FieldWrapper
type="debug"
name="_ft_debug_path"
label="Document Path"
debugText={values.ref?.path ?? values.id ?? "No ref"}
/>
</Grid>
</form>
);
}
function AutosaveTwo({
control,
}: {
control: Control;
defaultValues: { [key: string]: any };
}) {
const watchAll = useWatch({ control });
console.log("FORM VALUES", watchAll);
return null;
}