Skip to content

Commit

Permalink
refactor: improve dynamic import ux and typesafety for form element I…
Browse files Browse the repository at this point in the history
…nputComponents (#704)

* refactor: make loading component config more efficient and slightly easier to read

* refactor: almost make the form config components more typesafe

* fix: simplify the types and remove all type errors

* Discard changes to core/lib/types.ts

* Discard changes to core/app/components/FormBuilder/ElementPanel/index.tsx
  • Loading branch information
tefkah authored Oct 10, 2024
1 parent 5d75b1f commit e0d594d
Show file tree
Hide file tree
Showing 12 changed files with 69 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { InputComponent } from "db/public";
import { Checkbox } from "ui/checkbox";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "ui/form";
import { Input } from "ui/input";

import type { InnerFormProps } from "./types";
import type { ComponentConfigFormProps } from "./types";

export default ({ form }: InnerFormProps) => {
export default ({ form }: ComponentConfigFormProps<InputComponent.checkbox>) => {
return (
<>
<FormField
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { InputComponent } from "db/public";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "ui/form";
import { Input } from "ui/input";

import type { InnerFormProps } from "./types";
import type { ComponentConfigFormProps } from "./types";

export default ({ form, schemaName }: InnerFormProps) => {
export default ({ form }: ComponentConfigFormProps<InputComponent.confidenceInterval>) => {
return (
<>
<FormField
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { InputComponent } from "db/public";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "ui/form";
import { Input } from "ui/input";

import type { InnerFormProps } from "./types";
import type { ComponentConfigFormProps } from "./types";

export default ({ form, schemaName }: InnerFormProps) => {
export default ({ form }: ComponentConfigFormProps<InputComponent.datePicker>) => {
return (
<>
<FormField
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { InputComponent } from "db/public";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "ui/form";
import { Input } from "ui/input";

import type { InnerFormProps } from "./types";
import type { ComponentConfigFormProps } from "./types";

export default ({ form, schemaName }: InnerFormProps) => {
export default ({ form }: ComponentConfigFormProps<InputComponent.fileUpload>) => {
return (
<>
<FormField
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { InputComponent } from "db/public";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "ui/form";
import { Input } from "ui/input";

import type { InnerFormProps } from "./types";
import type { ComponentConfigFormProps } from "./types";

export default ({ form, schemaName }: InnerFormProps) => {
export default ({ form }: ComponentConfigFormProps<InputComponent.memberSelect>) => {
return (
<>
<FormField
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { InputComponent } from "db/public";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "ui/form";
import { Input } from "ui/input";

import type { InnerFormProps } from "./types";
import type { ComponentConfigFormProps } from "./types";

export default ({ form, schemaName }: InnerFormProps) => {
export default ({ form }: ComponentConfigFormProps<InputComponent.textArea>) => {
return (
<>
<FormField
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { InputComponent } from "db/public";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "ui/form";
import { Input } from "ui/input";

import type { InnerFormProps } from "./types";
import type { ComponentConfigFormProps } from "./types";

export default ({ form, schemaName }: InnerFormProps) => {
export default ({ form }: ComponentConfigFormProps<InputComponent.textArea>) => {
return (
<>
<FormField
Expand Down
Original file line number Diff line number Diff line change
@@ -1,61 +1,40 @@
"use client";

import type { ComponentType } from "react";
import type React from "react";

import dynamic from "next/dynamic";

import { InputComponent } from "db/public";
import { Skeleton } from "ui/skeleton";

import type { ComponentConfigFormProps, InnerFormProps } from "./types.ts";

export const ComponentConfig = ({ component, ...props }: ComponentConfigFormProps) => {
let ConfigForm: ComponentType<InnerFormProps>;
switch (component) {
case InputComponent.checkbox:
ConfigForm = dynamic(() => import("./Checkbox.tsx"), {
ssr: false,
loading: () => <Skeleton className="h-full w-full" />,
});
break;
case InputComponent.confidenceInterval:
ConfigForm = dynamic(() => import("./ConfidenceInterval.tsx"), {
ssr: false,
loading: () => <Skeleton className="h-full w-full" />,
});
break;
case InputComponent.datePicker:
ConfigForm = dynamic(() => import("./DatePicker.tsx"), {
ssr: false,
loading: () => <Skeleton className="h-full w-full" />,
});
break;
case InputComponent.fileUpload:
ConfigForm = dynamic(() => import("./FileUpload.tsx"), {
ssr: false,
loading: () => <Skeleton className="h-full w-full" />,
});
break;
case InputComponent.memberSelect:
ConfigForm = dynamic(() => import("./MemberSelect.tsx"), {
ssr: false,
loading: () => <Skeleton className="h-full w-full" />,
});
break;
case InputComponent.textArea:
ConfigForm = dynamic(() => import("./TextArea.tsx"), {
ssr: false,
loading: () => <Skeleton className="h-full w-full" />,
});
break;
case InputComponent.textInput:
ConfigForm = dynamic(() => import("./TextInput.tsx"), {
ssr: false,
loading: () => <Skeleton className="h-full w-full" />,
});
break;
default:
return null;
}
return <ConfigForm {...props} />;
import type { ComponentConfigFormProps } from "./types.ts";

const toDynamic = (path: string) =>
// this dynamic import path needs to provide enough information for webpack/turbopack
// to be able to find it. The relative path and the extension are enough, but something like
// `import(path)` will not work.
dynamic(() => import(`./${path}.tsx`), {
ssr: false,
loading: () => <Skeleton className="h-full w-full" />,
});

const InputCompomentMap = {
[InputComponent.checkbox]: toDynamic("Checkbox"),
[InputComponent.confidenceInterval]: toDynamic("ConfidenceInterval"),
[InputComponent.datePicker]: toDynamic("DatePicker"),
[InputComponent.fileUpload]: toDynamic("FileUpload"),
[InputComponent.memberSelect]: toDynamic("MemberSelect"),
[InputComponent.textArea]: toDynamic("TextArea"),
[InputComponent.textInput]: toDynamic("TextInput"),
};

export const ComponentConfig = <I extends InputComponent>(props: ComponentConfigFormProps<I>) => {
// ideally the compenent would be selected through some (generic) function, but for `dynamic`
// to work properly the components need to be defined already outside of the react tree,
// hence the map and the type cast
const ConfigComponent = InputCompomentMap[props.component] as React.FC<
ComponentConfigFormProps<I>
>;

return <ConfigComponent {...props} />;
};
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import type { CoreSchemaType } from "@prisma/client";
import type { Static } from "@sinclair/typebox";
import type { UseFormReturn } from "react-hook-form";
import type { componentConfigSchemas } from "schemas";

import type { CoreSchemaType, InputComponent } from "db/public";
import type { InputComponent } from "db/public";

export type ComponentConfigFormProps = {
form: UseFormReturn<ConfigFormData>;
export type ComponentConfigFormProps<I extends InputComponent> = {
form: UseFormReturn<ConfigFormData<I>>;
schemaName: CoreSchemaType;
component: InputComponent;
component: I;
};

export type InnerFormProps = Omit<ComponentConfigFormProps, "component">;

export type ConfigFormData = {
export type ConfigFormData<I extends InputComponent> = {
required: boolean | null;
component: InputComponent;
config: any;
component: I;
config: Static<(typeof componentConfigSchemas)[I]>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const InputComponentConfigurationForm = ({ index }: Props) => {
const { schemaName } = selectedElement;
const allowedComponents = componentsBySchema[schemaName];

const form = useForm<ConfigFormData>({
const form = useForm<ConfigFormData<(typeof allowedComponents)[number]>>({
// Dynamically set the resolver so that the schema can update based on the selected component
resolver: (values, context, options) => {
const schema = Type.Object({
Expand All @@ -135,7 +135,7 @@ export const InputComponentConfigurationForm = ({ index }: Props) => {

const component = form.watch("component");

const onSubmit = (values: ConfigFormData) => {
const onSubmit = (values: ConfigFormData<typeof component>) => {
update(index, { ...selectedElement, ...values, updated: true, configured: true });
dispatch({ eventName: "save" });
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useFormContext } from "react-hook-form";
import { componentsBySchema, defaultComponent, SCHEMA_TYPES_WITH_ICONS } from "schemas";
import { defaultComponent, SCHEMA_TYPES_WITH_ICONS } from "schemas";

import { ElementType, StructuralFormElement } from "db/public";
import { Button } from "ui/button";
Expand All @@ -11,7 +11,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "ui/tabs";
import type { FormElementData, PanelState } from "../types";
import { useFormBuilder } from "../FormBuilderContext";
import { structuralElements } from "../StructuralElements";
import { isFieldInput } from "../types";

export const SelectElement = ({ panelState }: { panelState: PanelState }) => {
const fields = usePubFieldContext();
Expand Down
8 changes: 4 additions & 4 deletions packages/schemas/src/schemaComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CoreSchemaType, InputComponent } from "db/public";

export const defaultComponent = (schemaName: CoreSchemaType) => componentsBySchema[schemaName][0];

export const componentsBySchema: Record<CoreSchemaType, InputComponent[]> = {
export const componentsBySchema = {
[CoreSchemaType.Boolean]: [InputComponent.checkbox],
[CoreSchemaType.String]: [InputComponent.textInput, InputComponent.textArea],
[CoreSchemaType.DateTime]: [InputComponent.datePicker],
Expand All @@ -19,7 +19,7 @@ export const componentsBySchema: Record<CoreSchemaType, InputComponent[]> = {
[CoreSchemaType.MemberId]: [InputComponent.memberSelect],
[CoreSchemaType.Vector3]: [InputComponent.confidenceInterval],
[CoreSchemaType.Null]: [],
} as const;
} as const satisfies Record<CoreSchemaType, InputComponent[]>;

export const checkboxConfigSchema = Type.Object({
checkboxLabel: Type.Optional(Type.String()),
Expand Down Expand Up @@ -57,12 +57,12 @@ export const confidenceIntervalConfigSchema = Type.Object({
help: Type.Optional(Type.String()),
});

export const componentConfigSchemas: Record<InputComponent, TObject> = {
export const componentConfigSchemas = {
[InputComponent.checkbox]: checkboxConfigSchema,
[InputComponent.textArea]: textAreaConfigSchema,
[InputComponent.textInput]: textInputConfigSchema,
[InputComponent.datePicker]: datePickerConfigSchema,
[InputComponent.fileUpload]: fileUploadConfigSchema,
[InputComponent.memberSelect]: memberSelectConfigSchema,
[InputComponent.confidenceInterval]: confidenceIntervalConfigSchema,
} as const;
} as const satisfies Record<InputComponent, TObject>;

0 comments on commit e0d594d

Please sign in to comment.