Skip to content

Commit 298d91b

Browse files
authored
Merge pull request #29 from workfloworchestrator/1785-npm-release
1785 npm release
2 parents 2c55366 + 844c11b commit 298d91b

32 files changed

+732
-566
lines changed

backend/main.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from fastapi import FastAPI
1515
from fastapi.middleware.cors import CORSMiddleware
1616

17-
from pydantic import ConfigDict, Field
17+
from pydantic import BaseModel, ConfigDict, Field
1818
from pydantic_forms.core import FormPage, post_form
1919
from pydantic_forms.types import State
2020
from pydantic_forms.exception_handlers.fastapi import form_error_handler
@@ -112,6 +112,17 @@ class ListChoices(Choice):
112112
_6 = ("6", "Option 6")
113113

114114

115+
class Education(BaseModel):
116+
degree: str | None
117+
year: int | None
118+
119+
120+
class Person(BaseModel):
121+
name: str
122+
age: Annotated[int, Ge(18), Le(99)]
123+
education: Education
124+
125+
115126
@app.post("/form")
116127
async def form(form_data: list[dict] = []):
117128
def form_generator(state: State):
@@ -120,18 +131,21 @@ class TestForm(FormPage):
120131

121132
number: NumberExample = 3
122133
text: Annotated[str, Field(min_length=3, max_length=12)] = "Default text"
123-
textArea: LongText = "Default text area"
134+
textArea: LongText = "Text area default"
124135
divider: Divider
125136
label: Label = "Label"
126137
hidden: Hidden = "Hidden"
127138
# When there are > 3 choices a dropdown will be rendered
128139
dropdown: DropdownChoices = "2"
129140
# When there are <= 3 choices a radio group will be rendered
130141
radio: RadioChoices = "3"
131-
checkbox: bool = True
142+
# checkbox: bool = True TODO: Fix validation errors on this
143+
132144
# When there are <= 5 choices in a list a set of checkboxes are rendered
133-
multicheckbox: choice_list(MultiCheckBoxChoices) = ["1", "2"]
134-
list: choice_list(ListChoices) = [0, 1]
145+
# multicheckbox: choice_list(MultiCheckBoxChoices, min_items=3) = ["1", "2"]
146+
# list: choice_list(ListChoices) = [0, 1]
147+
148+
person: Person
135149

136150
form_data_1 = yield TestForm
137151

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'pydantic-forms': patch
3+
---
4+
5+
Adds object field

frontend/apps/example/src/app/page.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
'use client';
22

3-
import { PydanticForm, PydanticFormFieldType } from 'pydantic-forms';
3+
import {
4+
PydanticForm,
5+
PydanticFormFieldFormat,
6+
PydanticFormFieldType,
7+
} from 'pydantic-forms';
48
import type {
59
PydanticComponentMatcher,
610
PydanticFormApiProvider,
@@ -60,19 +64,21 @@ export default function Home() {
6064
const componentMatcher = (
6165
currentMatchers: PydanticComponentMatcher[],
6266
): PydanticComponentMatcher[] => {
63-
return currentMatchers;
6467
return [
65-
...currentMatchers,
6668
{
6769
id: 'textarea',
6870
ElementMatch: {
6971
Element: TextArea,
7072
isControlledElement: true,
7173
},
7274
matcher(field) {
73-
return field.type === PydanticFormFieldType.STRING;
75+
return (
76+
field.type === PydanticFormFieldType.STRING &&
77+
field.format === PydanticFormFieldFormat.LONG
78+
);
7479
},
7580
},
81+
...currentMatchers,
7682
];
7783
};
7884

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useForm } from 'react-hook-form';
2+
3+
import { z } from 'zod';
4+
5+
import defaultComponentMatchers from '@/components/defaultComponentMatchers';
6+
import { TextField } from '@/components/fields';
7+
import type {
8+
ElementMatch,
9+
Properties,
10+
PydanticComponentMatcher,
11+
PydanticFormComponents,
12+
PydanticFormField,
13+
PydanticFormsContextConfig,
14+
} from '@/types';
15+
16+
export const getMatcher = (
17+
customComponentMatcher: PydanticFormsContextConfig['componentMatcher'],
18+
) => {
19+
const componentMatchers = customComponentMatcher
20+
? customComponentMatcher(defaultComponentMatchers)
21+
: defaultComponentMatchers;
22+
23+
return (field: PydanticFormField): PydanticComponentMatcher | undefined => {
24+
return componentMatchers.find(({ matcher }) => {
25+
return matcher(field);
26+
});
27+
};
28+
};
29+
30+
export const getClientSideValidationRule = (
31+
field: PydanticFormField,
32+
rhf?: ReturnType<typeof useForm>,
33+
customComponentMatcher?: PydanticFormsContextConfig['componentMatcher'],
34+
) => {
35+
const matcher = getMatcher(customComponentMatcher);
36+
37+
const componentMatch = matcher(field);
38+
39+
let validationRule = componentMatch?.validator?.(field, rhf) ?? z.string();
40+
41+
if (!field.required) {
42+
validationRule = validationRule.optional();
43+
}
44+
45+
if (field.validations.isNullable) {
46+
validationRule = validationRule.nullable();
47+
}
48+
49+
return validationRule;
50+
};
51+
52+
export const componentMatcher = (
53+
properties: Properties,
54+
customComponentMatcher: PydanticFormsContextConfig['componentMatcher'],
55+
): PydanticFormComponents => {
56+
const matcher = getMatcher(customComponentMatcher);
57+
58+
const components: PydanticFormComponents = Object.entries(properties).map(
59+
([, pydanticFormField]) => {
60+
const matchedComponent = matcher(pydanticFormField);
61+
62+
const ElementMatch: ElementMatch = matchedComponent
63+
? matchedComponent.ElementMatch
64+
: {
65+
Element: TextField,
66+
isControlledElement: true,
67+
};
68+
69+
// Defaults to textField when there are no matches
70+
return {
71+
Element: ElementMatch,
72+
pydanticFormField: pydanticFormField,
73+
};
74+
},
75+
);
76+
77+
return components;
78+
};

frontend/packages/pydantic-forms/src/components/defaultComponentMatchers.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
PydanticFormFieldType,
2222
} from '@/types';
2323

24-
import { zodValidationPresets } from './zodValidations';
24+
import { ObjectField } from './fields/ObjectField';
25+
import { zodValidationPresets } from './zodValidationsPresets';
2526

2627
const defaultComponentMatchers: PydanticComponentMatcher[] = [
2728
{
@@ -152,6 +153,16 @@ const defaultComponentMatchers: PydanticComponentMatcher[] = [
152153
},
153154
validator: zodValidationPresets.array,
154155
},
156+
{
157+
id: 'object',
158+
ElementMatch: {
159+
Element: ObjectField,
160+
isControlledElement: false,
161+
},
162+
matcher(field) {
163+
return field.type === PydanticFormFieldType.OBJECT;
164+
},
165+
},
155166
];
156167

157168
// If nothing matches, it defaults to Text field in the mapToComponent function
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from 'react';
22

3+
import { usePydanticFormContext } from '@/core';
34
import { PydanticFormElementProps } from '@/types';
45

56
export const HiddenField = ({
67
pydanticFormField,
7-
rhf,
88
}: PydanticFormElementProps) => {
9+
const { rhf } = usePydanticFormContext();
910
return <input type="hidden" {...rhf.register(pydanticFormField.id)} />;
1011
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
3+
import { usePydanticFormContext } from '@/core';
4+
import { componentMatcher } from '@/core';
5+
import { PydanticFormElementProps } from '@/types';
6+
7+
import { RenderFields } from '../render';
8+
9+
export const ObjectField = ({
10+
pydanticFormField,
11+
}: PydanticFormElementProps) => {
12+
const { config } = usePydanticFormContext();
13+
14+
const components = componentMatcher(
15+
pydanticFormField.properties || {},
16+
config?.componentMatcher,
17+
);
18+
19+
return (
20+
<div>
21+
<h1>{pydanticFormField.title}</h1>
22+
<RenderFields components={components} />
23+
</div>
24+
);
25+
};

frontend/packages/pydantic-forms/src/components/form/Footer.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66
import React from 'react';
77

8-
import RenderReactHookFormErrors from '@/components/render/RenderReactHookFormErrors';
98
import { usePydanticFormContext } from '@/core';
109

1110
const Footer = () => {
@@ -20,17 +19,9 @@ const Footer = () => {
2019
allowUntouchedSubmit,
2120
} = usePydanticFormContext();
2221

23-
const hasErrors = !!Object.keys(rhf.formState.errors).length;
24-
2522
return (
2623
<div style={{ height: '200px' }}>
27-
{(!!footerComponent || hasErrors) && (
28-
<div>
29-
{footerComponent}
30-
31-
{<RenderReactHookFormErrors />}
32-
</div>
33-
)}
24+
{footerComponent && <div>{footerComponent}</div>}{' '}
3425
<div>
3526
{resetButtonAlternative ?? (
3627
<button

frontend/packages/pydantic-forms/src/components/render/Fields.tsx

Lines changed: 0 additions & 43 deletions
This file was deleted.

frontend/packages/pydantic-forms/src/components/render/FormRenderer.tsx

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,17 @@ import React from 'react';
22

33
import { RenderFields, RenderSections } from '@/components/render';
44
import { getFieldBySection } from '@/core/helper';
5-
import type { PydanticFormData, FormRenderer as Renderer } from '@/types';
6-
7-
export const FormRenderer: Renderer = ({
8-
pydanticFormData,
9-
}: {
10-
pydanticFormData: PydanticFormData;
11-
}) => {
12-
const formSections = getFieldBySection(pydanticFormData.fields);
5+
import type { FormRenderer as Renderer } from '@/types';
136

7+
export const FormRenderer: Renderer = ({ pydanticFormComponents }) => {
8+
const formSections = getFieldBySection(pydanticFormComponents);
149
const sections = formSections.map((section) => (
15-
<RenderSections section={section} key={section.id}>
16-
{({ fields }) => (
17-
<div>
18-
<RenderFields fields={fields} />
19-
</div>
20-
)}
10+
<RenderSections
11+
section={section}
12+
key={section.id}
13+
components={pydanticFormComponents}
14+
>
15+
{({ components }) => <RenderFields components={components} />}
2116
</RenderSections>
2217
));
2318

0 commit comments

Comments
 (0)