From 7e74eb8b0c3f153361aafd060abe11eb6ad677d3 Mon Sep 17 00:00:00 2001 From: Ruben <ruben@optoinvest.com> Date: Wed, 28 Feb 2024 11:59:31 -0800 Subject: [PATCH 1/5] feat: add support for step --- src/npm-fastui/src/components/FormField.tsx | 3 +- src/npm-fastui/src/models.d.ts | 1 + src/python-fastui/fastui/components/forms.py | 1 + src/python-fastui/fastui/json_schema.py | 9 ++++++ src/python-fastui/tests/test_forms.py | 34 ++++++++++++++++++++ 5 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/npm-fastui/src/components/FormField.tsx b/src/npm-fastui/src/components/FormField.tsx index d1b30c78..475ef750 100644 --- a/src/npm-fastui/src/components/FormField.tsx +++ b/src/npm-fastui/src/components/FormField.tsx @@ -24,7 +24,7 @@ interface FormFieldInputProps extends FormFieldInput { } export const FormFieldInputComp: FC<FormFieldInputProps> = (props) => { - const { name, placeholder, required, htmlType, locked, autocomplete } = props + const { name, placeholder, required, htmlType, locked, autocomplete, step } = props return ( <div className={useClassName(props)}> @@ -39,6 +39,7 @@ export const FormFieldInputComp: FC<FormFieldInputProps> = (props) => { disabled={locked} placeholder={placeholder} autoComplete={autocomplete} + step={step} aria-describedby={descId(props)} /> <ErrorDescription {...props} /> diff --git a/src/npm-fastui/src/models.d.ts b/src/npm-fastui/src/models.d.ts index a0ca91cd..e8aeefb2 100644 --- a/src/npm-fastui/src/models.d.ts +++ b/src/npm-fastui/src/models.d.ts @@ -353,6 +353,7 @@ export interface FormFieldInput { initial?: string | number placeholder?: string autocomplete?: string + step?: number | 'any' type: 'FormFieldInput' } export interface FormFieldTextarea { diff --git a/src/python-fastui/fastui/components/forms.py b/src/python-fastui/fastui/components/forms.py index a18a9c4c..29032e0e 100644 --- a/src/python-fastui/fastui/components/forms.py +++ b/src/python-fastui/fastui/components/forms.py @@ -32,6 +32,7 @@ class FormFieldInput(BaseFormField): initial: _t.Union[str, float, None] = None placeholder: _t.Union[str, None] = None autocomplete: _t.Union[str, None] = None + step: _t.Union[float, _t.Literal['any'], None] = None type: _t.Literal['FormFieldInput'] = 'FormFieldInput' diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index 49492796..21e01cda 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -197,6 +197,7 @@ def json_schema_field_to_field( initial=schema.get('default'), autocomplete=schema.get('autocomplete'), description=schema.get('description'), + step=schema.get('step', get_default_step(schema)), ) @@ -372,6 +373,14 @@ def input_html_type(schema: JsonSchemaField) -> InputHtmlType: raise ValueError(f'Unknown schema: {schema}') from e +def get_default_step(schema: JsonSchemaField) -> _t.Literal['any'] | None: + key = schema['type'] + if key == 'integer': + return None + if key == 'number': + return 'any' + + def schema_is_field(schema: JsonSchemaConcrete) -> _ta.TypeGuard[JsonSchemaField]: """ Determine if a schema is a field `JsonSchemaField` diff --git a/src/python-fastui/tests/test_forms.py b/src/python-fastui/tests/test_forms.py index b0919fad..2f74c585 100644 --- a/src/python-fastui/tests/test_forms.py +++ b/src/python-fastui/tests/test_forms.py @@ -469,3 +469,37 @@ def test_form_textarea_form_fields(): } ], } + + +class FormNumbersDefaultStep(BaseModel): + size: int + cost: float + + +def test_form_numbers_default_step(): + m = components.ModelForm(model=FormNumbersDefaultStep, submit_url='/foobar') + + assert m.model_dump(by_alias=True, exclude_none=True) == { + 'submitUrl': '/foobar', + 'method': 'POST', + 'type': 'ModelForm', + 'formFields': [ + { + 'name': 'size', + 'title': ['Size'], + 'required': True, + 'locked': False, + 'htmlType': 'number', + 'type': 'FormFieldInput', + }, + { + 'name': 'cost', + 'title': ['Cost'], + 'required': True, + 'locked': False, + 'htmlType': 'number', + 'step': 'any', + 'type': 'FormFieldInput', + }, + ], + } From b624c02de4cff5384b35cd47c3c0f61352d48a9e Mon Sep 17 00:00:00 2001 From: Ruben <ruben@optoinvest.com> Date: Wed, 28 Feb 2024 12:15:41 -0800 Subject: [PATCH 2/5] support older versions of python --- src/python-fastui/fastui/json_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index 21e01cda..9adeb9d1 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -373,7 +373,7 @@ def input_html_type(schema: JsonSchemaField) -> InputHtmlType: raise ValueError(f'Unknown schema: {schema}') from e -def get_default_step(schema: JsonSchemaField) -> _t.Literal['any'] | None: +def get_default_step(schema: JsonSchemaField) -> _t.Union[_t.Literal['any'], None]: key = schema['type'] if key == 'integer': return None From d61c0763e1cf86dedff2dd334ff85486f1f081a9 Mon Sep 17 00:00:00 2001 From: Ruben <ruben@optoinvest.com> Date: Sat, 1 Jun 2024 09:19:40 -0700 Subject: [PATCH 3/5] comments --- src/python-fastui/fastui/json_schema.py | 4 ++-- src/python-fastui/tests/test_forms.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index 60237b8e..562793c7 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -197,8 +197,8 @@ def json_schema_field_to_field( initial=schema.get('default'), autocomplete=schema.get('autocomplete'), description=schema.get('description'), - step=schema.get('step', get_default_step(schema)), placeholder=schema.get('placeholder'), + step=schema.get('step', _get_default_step(schema)), ) @@ -374,7 +374,7 @@ def input_html_type(schema: JsonSchemaField) -> InputHtmlType: raise ValueError(f'Unknown schema: {schema}') from e -def get_default_step(schema: JsonSchemaField) -> _t.Union[_t.Literal['any'], None]: +def _get_default_step(schema: JsonSchemaField) -> _t.Union[_t.Literal['any'], None]: key = schema['type'] if key == 'integer': return None diff --git a/src/python-fastui/tests/test_forms.py b/src/python-fastui/tests/test_forms.py index e2f52b11..631c7b0b 100644 --- a/src/python-fastui/tests/test_forms.py +++ b/src/python-fastui/tests/test_forms.py @@ -552,6 +552,7 @@ def test_form_fields(): class FormNumbersDefaultStep(BaseModel): size: int cost: float + fees: float = Field(json_schema_extra={'step': '0.01'}) def test_form_numbers_default_step(): @@ -579,5 +580,14 @@ def test_form_numbers_default_step(): 'step': 'any', 'type': 'FormFieldInput', }, + { + 'name': 'fees', + 'title': ['Fees'], + 'required': True, + 'locked': False, + 'htmlType': 'number', + 'step': '0.01', + 'type': 'FormFieldInput', + }, ], } From 48561db28767e3d0d9d8d601a12294bffb6865fe Mon Sep 17 00:00:00 2001 From: Ruben <ruben@optoinvest.com> Date: Sat, 1 Jun 2024 09:25:28 -0700 Subject: [PATCH 4/5] restore this file --- src/npm-fastui/src/models.d.ts | 109 +++++++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/src/npm-fastui/src/models.d.ts b/src/npm-fastui/src/models.d.ts index 119f3031..100af916 100644 --- a/src/npm-fastui/src/models.d.ts +++ b/src/npm-fastui/src/models.d.ts @@ -58,6 +58,9 @@ export type JsonData = } export type AnyEvent = PageEvent | GoToEvent | BackEvent | AuthEvent export type NamedStyle = 'primary' | 'secondary' | 'warning' +/** + * Display mode for a value. + */ export type DisplayMode = | 'auto' | 'plain' @@ -70,35 +73,47 @@ export type DisplayMode = | 'inline_code' export type SelectOptions = SelectOption[] | SelectGroup[] +/** + * Text component that displays a string. + */ export interface Text { text: string type: 'Text' } +/** + * Paragraph component that displays a string as a paragraph. + */ export interface Paragraph { text: string className?: ClassName type: 'Paragraph' } /** - * This sets the title of the HTML page via the `document.title` property. + * Sets the title of the HTML page via the `document.title` property. */ export interface PageTitle { text: string type: 'PageTitle' } +/** + * A generic container component. + */ export interface Div { components: FastProps[] className?: ClassName type: 'Div' } /** - * Similar to `container` in many UI frameworks, this should be a reasonable root component for most pages. + * Similar to `container` in many UI frameworks, this acts as a root component for most pages. */ export interface Page { components: FastProps[] className?: ClassName type: 'Page' } +/** + * Heading component. + */ export interface Heading { text: string level: 1 | 2 | 3 | 4 | 5 | 6 @@ -106,12 +121,18 @@ export interface Heading { className?: ClassName type: 'Heading' } +/** + * Markdown component that renders markdown text. + */ export interface Markdown { text: string codeStyle?: string className?: ClassName type: 'Markdown' } +/** + * Code component that renders code with syntax highlighting. + */ export interface Code { text: string language?: string @@ -119,11 +140,17 @@ export interface Code { className?: ClassName type: 'Code' } +/** + * JSON component that renders JSON data. + */ export interface Json { value: JsonData className?: ClassName type: 'JSON' } +/** + * Button component. + */ export interface Button { text: string onClick?: AnyEvent @@ -159,6 +186,9 @@ export interface AuthEvent { url?: string type: 'auth' } +/** + * Link component. + */ export interface Link { components: FastProps[] onClick?: PageEvent | GoToEvent | BackEvent | AuthEvent @@ -168,12 +198,18 @@ export interface Link { className?: ClassName type: 'Link' } +/** + * List of Link components. + */ export interface LinkList { links: Link[] mode?: 'tabs' | 'vertical' | 'pagination' className?: ClassName type: 'LinkList' } +/** + * Navbar component used for moving between pages. + */ export interface Navbar { title?: string titleEvent?: PageEvent | GoToEvent | BackEvent | AuthEvent @@ -182,12 +218,18 @@ export interface Navbar { className?: ClassName type: 'Navbar' } +/** + * Footer component. + */ export interface Footer { links: Link[] extraText?: string className?: ClassName type: 'Footer' } +/** + * Modal component that displays a modal dialog. + */ export interface Modal { title: string body: FastProps[] @@ -209,6 +251,9 @@ export interface ServerLoad { method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' type: 'ServerLoad' } +/** + * Image container component. + */ export interface Image { src: string alt?: string @@ -228,6 +273,9 @@ export interface Image { className?: ClassName type: 'Image' } +/** + * Iframe component that displays content from a URL. + */ export interface Iframe { src: string title?: string @@ -238,6 +286,9 @@ export interface Iframe { sandbox?: string type: 'Iframe' } +/** + * Video component that displays a video or multiple videos. + */ export interface Video { sources: string[] autoplay?: boolean @@ -247,14 +298,20 @@ export interface Video { poster?: string width?: string | number height?: string | number - type: 'Video' className?: ClassName + type: 'Video' } +/** + * Fire an event. + */ export interface FireEvent { event: AnyEvent message?: string type: 'FireEvent' } +/** + * Utility component used to display an error. + */ export interface Error { title: string description: string @@ -263,11 +320,17 @@ export interface Error { type: 'Error' children?: ReactNode } +/** + * Spinner component that displays a loading spinner. + */ export interface Spinner { text?: string className?: ClassName type: 'Spinner' } +/** + * Custom component that allows for special data to be rendered. + */ export interface Custom { data: JsonData subType: string @@ -275,6 +338,9 @@ export interface Custom { className?: ClassName type: 'Custom' } +/** + * Table component. + */ export interface Table { data: DataModel[] columns: DisplayLookup[] @@ -295,13 +361,16 @@ export interface DisplayLookup { field: string tableWidthPercent?: number } +/** + * Pagination component to use with tables. + */ export interface Pagination { page: number pageSize: number total: number + pageQueryParam?: string className?: ClassName type: 'Pagination' - pageQueryParam?: string pageCount: number } /** @@ -314,12 +383,18 @@ export interface Display { value: JsonData type: 'Display' } +/** + * Details associated with displaying a data model. + */ export interface Details { data: DataModel - fields: DisplayLookup[] + fields: (DisplayLookup | Display)[] className?: ClassName type: 'Details' } +/** + * Form component. + */ export interface Form { submitUrl: string initial?: { @@ -342,6 +417,9 @@ export interface Form { )[] type: 'Form' } +/** + * Form field for basic input. + */ export interface FormFieldInput { name: string title: string[] | string @@ -358,6 +436,9 @@ export interface FormFieldInput { step?: number | 'any' type: 'FormFieldInput' } +/** + * Form field for text area input. + */ export interface FormFieldTextarea { name: string title: string[] | string @@ -374,6 +455,9 @@ export interface FormFieldTextarea { autocomplete?: string type: 'FormFieldTextarea' } +/** + * Form field for boolean input. + */ export interface FormFieldBoolean { name: string title: string[] | string @@ -387,6 +471,9 @@ export interface FormFieldBoolean { mode?: 'checkbox' | 'switch' type: 'FormFieldBoolean' } +/** + * Form field for file input. + */ export interface FormFieldFile { name: string title: string[] | string @@ -400,6 +487,9 @@ export interface FormFieldFile { accept?: string type: 'FormFieldFile' } +/** + * Form field for select input. + */ export interface FormFieldSelect { name: string title: string[] | string @@ -425,6 +515,9 @@ export interface SelectGroup { label: string options: SelectOption[] } +/** + * Form field for searchable select input. + */ export interface FormFieldSelectSearch { name: string title: string[] | string @@ -441,6 +534,9 @@ export interface FormFieldSelectSearch { placeholder?: string type: 'FormFieldSelectSearch' } +/** + * Form component generated from a Pydantic model. + */ export interface ModelForm { submitUrl: string initial?: { @@ -463,6 +559,9 @@ export interface ModelForm { | FormFieldSelectSearch )[] } +/** + * Toast component that displays a toast message (small temporary message). + */ export interface Toast { title: string body: FastProps[] From 1c00ba06db93656881f0bbe4f6d1c41385791e76 Mon Sep 17 00:00:00 2001 From: Ruben <ruben@optoinvest.com> Date: Sat, 1 Jun 2024 09:30:03 -0700 Subject: [PATCH 5/5] fixup test --- src/python-fastui/tests/test_forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python-fastui/tests/test_forms.py b/src/python-fastui/tests/test_forms.py index 631c7b0b..2b45c26c 100644 --- a/src/python-fastui/tests/test_forms.py +++ b/src/python-fastui/tests/test_forms.py @@ -586,7 +586,7 @@ def test_form_numbers_default_step(): 'required': True, 'locked': False, 'htmlType': 'number', - 'step': '0.01', + 'step': 0.01, 'type': 'FormFieldInput', }, ],