Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui v2: move resolver to accordions #770

Merged
merged 13 commits into from
Dec 11, 2020
80 changes: 22 additions & 58 deletions frontend/packages/core/src/Resolver/hydrator.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,9 @@
import React from "react";
import type { clutch } from "@clutch-sh/api";
import {
FormControl as MuiFormControl,
InputLabel as MuiInputLabel,
MenuItem,
Select as MuiSelect,
} from "@material-ui/core";
import styled from "styled-components";

import { Select } from "../Input/select";
import { TextField } from "../Input/text-field";

const maxWidth = "500px";
const InputLabel = styled(MuiInputLabel)`
${({ theme }) => `
color: ${theme.palette.text.primary};
`}
`;

const FormControl = styled(MuiFormControl)`
display: flex;
width: 100%;
margin-top: 5px;
max-width: ${maxWidth};
`;

const Select = styled(MuiSelect)`
display: flex;
width: 100%;
max-width: ${maxWidth};
`;

export interface ResolverChangeEvent {
target: {
name: string;
Expand Down Expand Up @@ -88,45 +62,35 @@ const OptionField = (
field: clutch.resolver.v1.IField,
onChange: (e: ResolverChangeEvent) => void
): React.ReactElement => {
const options = field.metadata.optionField.options.map(option => {
return option.displayName;
});
const [selectedIdx, setSelectedIdx] = React.useState(0);
const updateSelectedOption = (event: React.ChangeEvent<ChangeEventTarget>) => {
setSelectedIdx(options.indexOf(event.target.value));
onChange(convertChangeEvent(event));
};

React.useEffect(() => {
const fieldName = field.metadata.displayName || field.name;
onChange({
target: {
name: fieldName,
value: field.metadata.optionField.options?.[selectedIdx]?.stringValue,
name: field.name,
value: field.metadata.optionField.options?.[0]?.stringValue,
},
initialLoad: true,
});
}, []);

const options = field.metadata.optionField.options.map(option => {
return { label: option.displayName, value: option.stringValue };
});
const updateSelectedOption = (value: string) => {
onChange({
target: {
danielhochman marked this conversation as resolved.
Show resolved Hide resolved
name: field.name,
value,
},
});
};

return (
<FormControl
key={field.metadata.displayName || field.name}
required={field.metadata.required || false}
>
<InputLabel color="secondary">{field.metadata.displayName || field.name}</InputLabel>
<Select
value={options[selectedIdx] || ""}
onChange={updateSelectedOption}
name={field.metadata.displayName || field.name}
inputProps={{ style: { minWidth: "100px" } }}
>
{options.map(option => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
<Select
label={field.metadata.displayName}
onChange={updateSelectedOption}
name={field.name}
options={options}
/>
);
};

Expand All @@ -151,4 +115,4 @@ const hydrateField = (
return component(field, onChange, validation);
};

export { convertChangeEvent, FormControl, hydrateField, InputLabel };
export { convertChangeEvent, hydrateField };
81 changes: 27 additions & 54 deletions frontend/packages/core/src/Resolver/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import React from "react";
import { useForm } from "react-hook-form";
import { DevTool } from "@hookform/devtools";
import _ from "lodash";
import styled from "styled-components";

import { AccordionGroup } from "../accordion";
import { Button } from "../button";
import { useWizardContext } from "../Contexts";
import { CompressedError, Error } from "../error";
import { HorizontalRule } from "../horizontal-rule";
import Loadable from "../loading";

import { fetchResourceSchemas, resolveResource } from "./fetch";
import type { ResolverChangeEvent } from "./hydrator";
import { QueryResolver, SchemaResolver } from "./input";
import type { DispatchAction } from "./state";
import { ResolverAction, useResolverState } from "./state";
Expand Down Expand Up @@ -51,37 +50,24 @@ const Resolver: React.FC<ResolverProps> = ({ type, searchLimit, onResolve, varia
const [state, dispatch] = useResolverState();
const { displayWarnings } = useWizardContext();

const queryValidation = useForm({
mode: "onSubmit",
reValidateMode: "onSubmit",
shouldFocusError: false,
});
const schemaValidation = useForm({
mode: "onSubmit",
reValidateMode: "onSubmit",
shouldFocusError: false,
});
const [validation, setValidation] = React.useState(() => queryValidation);

React.useEffect(() => loadSchemas(type, dispatch), []);

const submitHandler = () => {
const [queryData, setQueryData] = React.useState({ query: "" });

const submitHandler = data => {
// Move to loading state.
dispatch({ type: ResolverAction.RESOLVING });

// Copy incoming data, trimming whitespace from any string values (usually artifact of cut and paste into tool).
const data = _.mapValues(state.queryData, v => (_.isString(v) && _.trim(v)) || v);

// Set desired type.
data["@type"] = state.allSchemas[state.selectedSchema]?.typeUrl;
const inputData = _.mapValues(data, v => (_.isString(v) && _.trim(v)) || v);

// Resolve!
resolveResource(
type,
searchLimit,
data,
inputData,
(results, failures) => {
onResolve({ results, input: data });
onResolve({ results, input: inputData });
if (!_.isEmpty(failures)) {
displayWarnings(failures);
}
Expand All @@ -91,24 +77,14 @@ const Resolver: React.FC<ResolverProps> = ({ type, searchLimit, onResolve, varia
);
};

const updateResolverData = (e: ResolverChangeEvent) => {
validation.clearErrors();
if (e.target.name !== "query") {
setValidation(() => schemaValidation);
} else {
setValidation(() => queryValidation);
}
dispatch({
type: ResolverAction.UPDATE_QUERY_DATA,
data: { [e.target.name.toLowerCase()]: e.target.value },
});
if (e.initialLoad) {
setValidation(() => queryValidation);
}
};
const queryValidation = useForm({
mode: "onSubmit",
reValidateMode: "onSubmit",
shouldFocusError: false,
});

const setSelectedSchema = (e: React.ChangeEvent<{ name?: string; value: unknown }>) => {
dispatch({ type: ResolverAction.SET_SELECTED_SCHEMA, schema: e.target.value });
const queryOnChange = e => {
setQueryData({ query: e.target.value });
};

return (
Expand All @@ -117,30 +93,27 @@ const Resolver: React.FC<ResolverProps> = ({ type, searchLimit, onResolve, varia
<Error message={state.schemaFetchError} onRetry={() => loadSchemas(type, dispatch)} />
) : (
<Loadable variant="overlay" isLoading={state.resolverLoading}>
{process.env.REACT_APP_DEBUG_FORMS === "true" && <DevTool control={validation.control} />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add back the debug form?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't work correctly with multiple validations at once afaict

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yeah it would just be the focused one

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we no longer know which is focused, which i think we will need to add if we want to do dynamic disable on focus, but complicated enough to defer to another PR. wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add in a follow up PR

<CompressedError title="Error" message={state.resolverFetchError} />
danielhochman marked this conversation as resolved.
Show resolved Hide resolved
{(variant === "dual" || variant === "query") && (
<Form onSubmit={validation.handleSubmit(submitHandler)} noValidate>
<Form
onSubmit={queryValidation.handleSubmit(() => submitHandler(queryData))}
noValidate
>
<QueryResolver
schemas={state.searchableSchemas}
onChange={updateResolverData}
onChange={queryOnChange}
validation={queryValidation}
/>
<Button text="Search" type="submit" />
</Form>
)}
{variant === "dual" && <HorizontalRule>OR</HorizontalRule>}
{(variant === "dual" || variant === "schema") && (
<Form onSubmit={validation.handleSubmit(submitHandler)} noValidate>
<SchemaResolver
schemas={state.allSchemas}
selectedSchema={state.selectedSchema}
onSelect={setSelectedSchema}
onChange={updateResolverData}
validation={schemaValidation}
/>
</Form>
)}
<Button text="Continue" onClick={validation.handleSubmit(submitHandler)} />
<CompressedError title="Error" message={state.resolverFetchError} />
Advanced Search
<AccordionGroup defaultExpandedIdx={0}>
{state.allSchemas.map(schema => (
<SchemaResolver key={schema.typeUrl} schema={schema} submitHandler={submitHandler} />
))}
</AccordionGroup>
</Loadable>
)}
</Loadable>
Expand Down
82 changes: 49 additions & 33 deletions frontend/packages/core/src/Resolver/input.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import React from "react";
import type { UseFormMethods } from "react-hook-form";
import { useForm } from "react-hook-form";
import type { clutch } from "@clutch-sh/api";
import { MenuItem, Select } from "@material-ui/core";

import {
Accordion,
AccordionActions,
AccordionDetails,
AccordionDivider,
AccordionProps,
} from "../accordion";
import { Button } from "../button";
import { Error } from "../error";
import { TextField } from "../Input/text-field";

import type { ChangeEventTarget, ResolverChangeEvent } from "./hydrator";
import { convertChangeEvent, FormControl, hydrateField, InputLabel } from "./hydrator";
import { convertChangeEvent, hydrateField } from "./hydrator";

interface QueryResolverProps {
schemas: clutch.resolver.v1.Schema[];
Expand Down Expand Up @@ -39,38 +47,46 @@ const QueryResolver: React.FC<QueryResolverProps> = ({ schemas, onChange, valida
);
};

interface SchemaResolverProps {
schemas: clutch.resolver.v1.Schema[];
selectedSchema: number;
onSelect: (e: React.ChangeEvent<{ name?: string; value: unknown }>) => void;
onChange: (e: ResolverChangeEvent) => void;
validation: UseFormMethods;
// TODO: update and use
interface SchemaResolverProps extends Pick<AccordionProps, "expanded" | "onClick"> {
schema: clutch.resolver.v1.Schema;
submitHandler: any;
}

const SchemaResolver: React.FC<SchemaResolverProps> = ({
schemas,
selectedSchema,
onSelect,
onChange,
validation,
}) => (
<>
<FormControl>
<InputLabel>Resolver</InputLabel>
<Select value={schemas?.[selectedSchema]?.typeUrl || ""} onChange={onSelect}>
{schemas.map(schema => (
<MenuItem key={schema.metadata.displayName} value={schema.typeUrl}>
{schema.metadata.displayName}
</MenuItem>
))}
</Select>
</FormControl>
{schemas[selectedSchema]?.error ? (
<Error message={`Schema Error: ${schemas[selectedSchema].error.message}`} />
) : (
schemas[selectedSchema]?.fields.map(field => hydrateField(field, onChange, validation))
)}
</>
);
const SchemaResolver = ({ schema, expanded, onClick, submitHandler }: SchemaResolverProps) => {
const [data, setData] = React.useState({ "@type": schema.typeUrl });

const schemaValidation = useForm({
mode: "onSubmit",
reValidateMode: "onSubmit",
shouldFocusError: false,
});

const onChange = e => {
setData({ ...data, [e.target.name]: e.target.value });
};

return (
<form noValidate onSubmit={schemaValidation.handleSubmit(() => submitHandler(data))}>
<Accordion
title={`Search by ${schema.metadata.displayName}`}
expanded={expanded}
onClick={onClick}
>
<AccordionDetails>
{schema.error ? (
<Error message={`Schema Error: ${schema.error.message}`} />
) : (
schema.fields.map(field => hydrateField(field, onChange, schemaValidation))
)}
</AccordionDetails>
<AccordionDivider />
<AccordionActions>
<Button text="Submit" type="submit" />
</AccordionActions>
</Accordion>
</form>
);
};

export { SchemaResolver, QueryResolver };
Loading