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',
             },
         ],