Merge pull request #111 from ctk-hq/feat/super-form

Created super form
pull/109/merge
Artem Golub 3 years ago committed by GitHub
commit 1e85671688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -25,6 +25,7 @@ import ProtectedRoute from "./partials/ProtectedRoute";
import "./index.css"; import "./index.css";
import { lightTheme } from "./utils/theme"; import { lightTheme } from "./utils/theme";
import { SuperFormProvider } from "./components/SuperFormProvider";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -100,6 +101,7 @@ export default function App() {
return ( return (
<CssVarsProvider theme={lightTheme}> <CssVarsProvider theme={lightTheme}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<SuperFormProvider>
<div> <div>
<Toaster /> <Toaster />
<SideBar isAuthenticated={isAuthenticated} state={state} /> <SideBar isAuthenticated={isAuthenticated} state={state} />
@ -146,10 +148,13 @@ export default function App() {
<Route path="/signup" element={<Signup dispatch={dispatch} />} /> <Route path="/signup" element={<Signup dispatch={dispatch} />} />
<Route path="/login" element={<Login dispatch={dispatch} />} /> <Route path="/login" element={<Login dispatch={dispatch} />} />
<Route path="/github/cb" element={<GitHub dispatch={dispatch} />} /> <Route
path="/github/cb"
element={<GitHub dispatch={dispatch} />}
/>
</Routes> </Routes>
</div> </div>
</SuperFormProvider>
<ReactQueryDevtools initialIsOpen={true} /> <ReactQueryDevtools initialIsOpen={true} />
</QueryClientProvider> </QueryClientProvider>
</CssVarsProvider> </CssVarsProvider>

@ -0,0 +1,31 @@
import { styled } from "@mui/joy";
import { FunctionComponent, ReactElement } from "react";
import { useSuperForm } from "../hooks";
import { IFormField } from "../types";
interface IRootProps {
spans: number[];
}
const Root = styled("div", {
shouldForwardProp: (name) => name !== "spans"
})<IRootProps>`
grid-column: span ${({ spans }) => spans[0]};
@media (max-width: 640px) {
grid-column: span ${({ spans }) => spans[1]};
}
`;
export interface IGridColumnProps {
spans: number[];
fields: IFormField[];
}
export const GridColumn: FunctionComponent<IGridColumnProps> = (
props: IGridColumnProps
): ReactElement => {
const { spans, fields } = props;
const { renderField } = useSuperForm();
return <Root spans={spans}>{fields.map(renderField)}</Root>;
};

@ -0,0 +1,30 @@
import { styled } from "@mui/joy";
import { FunctionComponent, ReactElement, ReactNode } from "react";
import { useSuperForm } from "../hooks";
import { IFormField } from "../types";
const Root = styled("div")`
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 0px;
grid-row-gap: 0px;
@media (max-width: 640px) {
grid-template-columns: repeat(1, 1fr);
}
column-gap: ${({ theme }) => theme.spacing(1)};
width: 100%;
`;
export interface IGridProps {
fields: IFormField[];
children?: ReactNode;
}
export const GridRow: FunctionComponent<IGridProps> = (
props: IGridProps
): ReactElement => {
const { fields } = props;
const { renderField } = useSuperForm();
return <Root>{fields.map(renderField)}</Root>;
};

@ -0,0 +1,28 @@
import { styled } from "@mui/joy";
import { FunctionComponent, ReactElement } from "react";
import { useSuperForm } from "../hooks";
import { IFormField, TFinalFormField } from "../types";
const Root = styled("div")`
display: flex;
flex-direction: column;
row-gap: ${({ theme }) => theme.spacing(1)};
@media (max-width: 640px) {
row-gap: 0;
}
`;
export interface IRecordFormProps<T extends IFormField> {
fields: T[];
}
export const SuperForm: FunctionComponent<IRecordFormProps<TFinalFormField>> = <
T extends IFormField
>(
props: IRecordFormProps<T>
): ReactElement => {
const { fields } = props;
const { renderField } = useSuperForm();
return <Root>{fields.map(renderField)}</Root>;
};

@ -0,0 +1,43 @@
import { FunctionComponent, ReactElement, ReactNode, useMemo } from "react";
import { SuperFormContext } from "../contexts";
import { IFormField } from "../types";
import TextField from "./global/FormElements/TextField";
import Toggle from "./global/FormElements/Toggle";
import { GridColumn } from "./GridColumn";
import { GridRow } from "./GridRow";
import Records from "./Records";
export interface ISuperFormProviderProps {
children?: ReactNode;
}
const types: Record<string, FunctionComponent<any>> = {
text: TextField,
"grid-row": GridRow,
"grid-column": GridColumn,
toggle: Toggle,
records: Records
};
export const SuperFormProvider: FunctionComponent<ISuperFormProviderProps> = (
props: ISuperFormProviderProps
): ReactElement => {
const { children } = props;
const value = useMemo(
() => ({
types,
renderField: (field: IFormField) => {
const Component = types[field.type];
return <Component key={field.id} {...field} />;
}
}),
[]
);
return (
<SuperFormContext.Provider value={value}>
{children}
</SuperFormContext.Provider>
);
};

@ -1,7 +1,6 @@
import { styled } from "@mui/joy"; import { styled } from "@mui/joy";
import TextField from "../../../global/FormElements/TextField"; import { TFinalFormField } from "../../../../types";
import Toggle from "../../../global/FormElements/Toggle"; import { SuperForm } from "../../../SuperForm";
import Records from "../../../Records";
const Root = styled("div")` const Root = styled("div")`
display: flex; display: flex;
@ -12,77 +11,145 @@ const Root = styled("div")`
} }
`; `;
const Group = styled("div")`
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 0px;
grid-row-gap: 0px;
@media (max-width: 640px) {
grid-template-columns: repeat(1, 1fr);
}
column-gap: ${({ theme }) => theme.spacing(1)};
width: 100%;
`;
const SpanTwo = styled("div")`
grid-column: span 2;
@media (max-width: 640px) {
grid-column: span 3;
}
`;
const General = () => { const General = () => {
return ( return (
<Root> <Root>
<Group> <SuperForm
<TextField label="Service name" name="serviceName" required={true} /> fields={[
</Group> {
id: "row1",
<Group> type: "grid-row",
<TextField label="Image name" name="imageName" required={false} /> fields: [
<TextField label="Image tag" name="imageTag" /> {
</Group> id: "serviceName",
type: "text",
<Group> name: "serviceName",
<TextField label: "Service name",
label="Container name" required: true
name="containerName" }
required={false} ]
/> },
</Group> {
id: "row2",
<Group> type: "grid-row",
<SpanTwo> fields: [
<TextField label="Command" name="command" required={false} /> {
</SpanTwo> id: "imageName",
</Group> type: "text",
name: "imageName",
<Group> label: "Image name"
<SpanTwo> },
<TextField label="Entrypoint" name="entrypoint" required={false} /> {
</SpanTwo> id: "imageTag",
</Group> type: "text",
name: "imageTag",
<Group> label: "Image tag"
<TextField label="Env file" name="envFile" required={false} /> }
</Group> ]
},
<Group> {
<SpanTwo> id: "row3",
<TextField type: "grid-row",
label="Working directory" fields: [
name="workingDir" {
required={false} id: "containerName",
/> type: "text",
</SpanTwo> name: "containerName",
</Group> label: "Container name"
}
<Group> ]
<SpanTwo> },
<Toggle {
name="restart" id: "row4",
label="Restart policy" type: "grid-row",
options={[ fields: [
{
id: "row4-column1",
type: "grid-column",
spans: [2, 3],
fields: [
{
id: "command",
type: "text",
name: "command",
label: "Command"
}
]
}
]
},
{
id: "row5",
type: "grid-row",
fields: [
{
id: "row5-column1",
type: "grid-column",
spans: [2, 3],
fields: [
{
id: "entrypoint",
type: "text",
name: "entrypoint",
label: "Entrypoint"
}
]
}
]
},
{
id: "row6",
type: "grid-row",
fields: [
{
id: "row6-column1",
type: "grid-column",
spans: [2, 3],
fields: [
{
id: "envFile",
type: "text",
name: "envFile",
label: "Env file"
}
]
}
]
},
{
id: "row7",
type: "grid-row",
fields: [
{
id: "row7-column1",
type: "grid-column",
spans: [2, 3],
fields: [
{
id: "workingDir",
type: "text",
name: "workingDir",
label: "Working directory"
}
]
}
]
},
{
id: "row8",
type: "grid-row",
fields: [
{
id: "row8-column1",
type: "grid-column",
spans: [2, 3],
fields: [
{
id: "restart",
type: "toggle",
name: "restart",
label: "Restart Policy",
options: [
{ {
value: "no", value: "no",
text: "no" text: "no"
@ -99,30 +166,37 @@ const General = () => {
value: "unless-stopped", value: "unless-stopped",
text: "unless-stopped" text: "unless-stopped"
} }
]} ]
/> }
</SpanTwo> ]
</Group> }
]
<Records },
name="ports" {
title="Ports" id: "ports",
defaultOpen={true} type: "records",
fields={(index: number) => [ name: "ports",
title: "Ports",
defaultOpen: true,
fields: (index: number): TFinalFormField[] => [
{ {
id: `ports[${index}].hostPort`,
type: "text",
name: `ports[${index}].hostPort`, name: `ports[${index}].hostPort`,
placeholder: "Host port", placeholder: "Host port",
required: true, required: true
type: "text"
}, },
{ {
id: `ports[${index}].containerPort`,
type: "text",
name: `ports[${index}].containerPort`, name: `ports[${index}].containerPort`,
placeholder: "Container port", placeholder: "Container port"
type: "text"
}, },
{ {
name: `ports[${index}].protocol`, id: `ports[${index}].protocol`,
type: "toggle", type: "toggle",
name: `ports[${index}].protocol`,
label: "Protocol",
options: [ options: [
{ {
value: "tcp", value: "tcp",
@ -134,74 +208,85 @@ const General = () => {
} }
] ]
} }
]} ],
newValue={{ newValue: {
hostPort: "", hostPort: "",
containerPort: "", containerPort: "",
protocol: "" protocol: ""
}} }
/> },
{
<Records id: "dependsOn",
name="dependsOn" type: "records",
title="Depends on" name: "dependsOn",
fields={(index: number) => [ title: "Depends on",
fields: (index: number): TFinalFormField[] => [
{ {
id: `dependsOn[${index}]`,
type: "text",
name: `dependsOn[${index}]`, name: `dependsOn[${index}]`,
placeholder: "Service name", placeholder: "Service name",
required: false, required: false
type: "text"
} }
]} ],
newValue={""} newValue: ""
/> },
<Records
name="networks"
title="Networks"
fields={(index: number) => [
{ {
id: "networks",
type: "records",
title: "Networks",
name: "networks",
fields: (index: number): TFinalFormField[] => [
{
id: `networks[${index}]`,
type: "text",
name: `networks[${index}]`, name: `networks[${index}]`,
placeholder: "Network name", placeholder: "Network name",
required: false, required: false
type: "text"
} }
]} ],
newValue={""} newValue: ""
/> },
{
<Records id: "labels",
name="labels" type: "records",
title="Labels" title: "Labels",
fields={(index: number) => [ name: "labels",
fields: (index: number): TFinalFormField[] => [
{ {
id: `labels[${index}].key`,
type: "text",
name: `labels[${index}].key`, name: `labels[${index}].key`,
placeholder: "Key", placeholder: "Key",
required: true, required: true
type: "text"
}, },
{ {
id: `labels[${index}].value`,
type: "text",
name: `labels[${index}].value`, name: `labels[${index}].value`,
placeholder: "Value", placeholder: "Value",
required: true, required: true
type: "text"
} }
]} ],
newValue={{ key: "", value: "" }} newValue: { key: "", value: "" }
/> },
{
<Records id: "profiles",
name="profiles" type: "records",
title="Profiles" title: "Profiles",
fields={(index: number) => [ name: "profiles",
fields: (index: number): TFinalFormField[] => [
{ {
id: `profiles[${index}]`,
name: `profiles[${index}]`, name: `profiles[${index}]`,
placeholder: "Name", placeholder: "Name",
required: true, required: true,
type: "text" type: "text"
} }
],
newValue: ""
}
]} ]}
newValue={""}
/> />
</Root> </Root>
); );

@ -0,0 +1,4 @@
import { createContext } from "react";
import { ISuperFormContext } from "../types";
export const SuperFormContext = createContext<ISuperFormContext | null>(null);

@ -1 +1,2 @@
export * from "./TabContext"; export * from "./TabContext";
export * from "./SuperFormContext";

@ -1,3 +1,4 @@
export * from "./useTitle"; export * from "./useTitle";
export * from "./useAccordionState"; export * from "./useAccordionState";
export * from "./useTabContext"; export * from "./useTabContext";
export * from "./useSuperForm";

@ -0,0 +1,12 @@
import { useContext } from "react";
import { SuperFormContext } from "../contexts";
import { ISuperFormContext } from "../types";
export const useSuperForm = (): ISuperFormContext => {
const context = useContext(SuperFormContext);
if (!context) {
throw new Error("Cannot find super form context!");
}
return context;
};

@ -1,5 +1,6 @@
import { AnchorId } from "@jsplumb/common"; import { AnchorId } from "@jsplumb/common";
import { Dictionary } from "lodash"; import { Dictionary } from "lodash";
import { FunctionComponent, ReactNode } from "react";
import { KeyValuePair } from "tailwindcss/types/config"; import { KeyValuePair } from "tailwindcss/types/config";
import { string } from "yup"; import { string } from "yup";
import { NodeGroupType } from "./enums"; import { NodeGroupType } from "./enums";
@ -520,3 +521,85 @@ export interface ITabContext {
value: string; value: string;
onChange: (newValue: string) => void; onChange: (newValue: string) => void;
} }
/* -- SuperForm -- */
export interface ISuperFormContext {
types: Record<string, FunctionComponent>;
renderField: (field: IFormField) => ReactNode;
}
export interface IFormField {
id: string;
type:
| "grid-row"
| "grid-column"
| "text"
| "integer"
| "toggle"
| "accordion"
| "records";
}
export interface IGridRowField<T extends IFormField> extends IFormField {
type: "grid-row";
fields: T[];
}
export interface IGridColumnField<T extends IFormField> extends IFormField {
type: "grid-column";
spans: number[];
fields: T[];
}
export interface IValueField extends IFormField {
name: string;
}
export interface ISingleRowField extends IValueField {
help?: string;
}
export interface ITextField extends ISingleRowField {
type: "text";
label?: string;
placeholder?: string;
required?: boolean;
}
export interface IIntegerField extends ISingleRowField {
type: "integer";
label: string;
required: boolean;
}
export interface IToggleField extends ISingleRowField {
type: "toggle";
label: string;
options: {
text: string;
value: string;
}[];
}
export interface IRecordsField<T extends IFormField> extends IValueField {
type: "records";
title: string;
defaultOpen?: boolean;
fields: (index: number) => T[];
newValue: any;
}
export interface IAccordionField extends IFormField {
type: "accordion";
title: string;
}
export type TFinalFormField =
| IGridColumnField<TFinalFormField>
| IGridRowField<TFinalFormField>
| ITextField
| IIntegerField
| IToggleField
| IRecordsField<TFinalFormField>
| IAccordionField;

Loading…
Cancel
Save