From 13a1d462a5bbfe9d5b76997a61cb166fd39d38fc Mon Sep 17 00:00:00 2001 From: cswamy <101974014+cswamy@users.noreply.github.com> Date: Wed, 7 Feb 2024 13:04:53 +0000 Subject: [PATCH 01/27] Fix for #148: open links in new tab (#157) --- demo/components_list.py | 4 ++++ src/npm-fastui/src/events.ts | 6 +++++- src/npm-fastui/src/models.d.ts | 1 + src/python-fastui/fastui/events.py | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/demo/components_list.py b/demo/components_list.py index 6e3622c1..b329c1b9 100644 --- a/demo/components_list.py +++ b/demo/components_list.py @@ -78,6 +78,10 @@ class Delivery(BaseModel): components=[c.Text(text='Pydantic (External link)')], on_click=GoToEvent(url='https://pydantic.dev'), ), + c.Link( + components=[c.Text(text='FastUI repo (New tab)')], + on_click=GoToEvent(url='https://github.com/pydantic/FastUI', target='_blank'), + ), ], ), ], diff --git a/src/npm-fastui/src/events.ts b/src/npm-fastui/src/events.ts index 45e3de90..45300aac 100644 --- a/src/npm-fastui/src/events.ts +++ b/src/npm-fastui/src/events.ts @@ -35,7 +35,11 @@ export function useFireEvent(): { fireEvent: (event?: AnyEvent) => void } { } case 'go-to': if (event.url) { - location.goto(event.url) + if (event.target) { + window.open(event.url, event.target) + } else { + location.goto(event.url) + } } if (event.query) { location.setQuery(event.query) diff --git a/src/npm-fastui/src/models.d.ts b/src/npm-fastui/src/models.d.ts index a083f8b7..ff5ee388 100644 --- a/src/npm-fastui/src/models.d.ts +++ b/src/npm-fastui/src/models.d.ts @@ -141,6 +141,7 @@ export interface GoToEvent { query?: { [k: string]: string | number } + target?: '_blank' type: 'go-to' } export interface BackEvent { diff --git a/src/python-fastui/fastui/events.py b/src/python-fastui/fastui/events.py index f4a91c33..c572ef87 100644 --- a/src/python-fastui/fastui/events.py +++ b/src/python-fastui/fastui/events.py @@ -18,6 +18,7 @@ class GoToEvent(BaseModel): # can be a path or a full URL url: Union[str, None] = None query: Union[Dict[str, Union[str, float, None]], None] = None + target: Union[Literal['_blank'], None] = None type: Literal['go-to'] = 'go-to' From 99dc253661e7e806dc13683419969f55e4f4a7bb Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 7 Feb 2024 13:07:27 +0000 Subject: [PATCH 02/27] Error improvements (#137) --- src/npm-fastui-bootstrap/src/index.tsx | 6 +++ src/npm-fastui-prebuilt/src/main.scss | 11 ++++- src/npm-fastui-prebuilt/vite.config.ts | 15 ++++++- src/npm-fastui/src/components/Custom.tsx | 5 +-- src/npm-fastui/src/components/error.tsx | 24 +++++++++++ src/npm-fastui/src/components/index.tsx | 10 +++-- src/npm-fastui/src/hooks/error.tsx | 43 +++++++++++-------- src/npm-fastui/src/index.tsx | 16 +++---- src/npm-fastui/src/models.d.ts | 9 ++++ src/npm-fastui/src/tools.ts | 7 ++- .../fastui/components/__init__.py | 19 ++++++++ 11 files changed, 126 insertions(+), 39 deletions(-) create mode 100644 src/npm-fastui/src/components/error.tsx diff --git a/src/npm-fastui-bootstrap/src/index.tsx b/src/npm-fastui-bootstrap/src/index.tsx index cf355c90..91f5fb27 100644 --- a/src/npm-fastui-bootstrap/src/index.tsx +++ b/src/npm-fastui-bootstrap/src/index.tsx @@ -137,5 +137,11 @@ export const classNameGenerator: ClassNameGenerator = ({ } case 'Code': return 'rounded' + case 'Error': + if (props.statusCode === 502) { + return 'm-3 text-muted' + } else { + return 'alert alert-danger m-3' + } } } diff --git a/src/npm-fastui-prebuilt/src/main.scss b/src/npm-fastui-prebuilt/src/main.scss index 1a1e07fc..037ae1ac 100644 --- a/src/npm-fastui-prebuilt/src/main.scss +++ b/src/npm-fastui-prebuilt/src/main.scss @@ -3,7 +3,9 @@ $link-color: #0d6efd; // bootstrap primary @import 'bootstrap/scss/bootstrap'; -html, body, #root { +html, +body, +#root { height: 100%; } @@ -33,7 +35,12 @@ body { backdrop-filter: blur(8px); } -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { scroll-margin-top: 60px; } diff --git a/src/npm-fastui-prebuilt/vite.config.ts b/src/npm-fastui-prebuilt/vite.config.ts index c73c49a1..ee73325c 100644 --- a/src/npm-fastui-prebuilt/vite.config.ts +++ b/src/npm-fastui-prebuilt/vite.config.ts @@ -1,14 +1,25 @@ import path from 'path' import react from '@vitejs/plugin-react-swc' -import { defineConfig } from 'vite' +import { defineConfig, HttpProxy } from 'vite' export default () => { const serverConfig = { host: true, port: 3000, proxy: { - '/api': 'http://localhost:8000', + '/api': { + target: 'http://localhost:8000', + configure: (proxy: HttpProxy.Server) => { + proxy.on('error', (err, _, res) => { + const { code } = err as any + if (code === 'ECONNREFUSED') { + res.writeHead(502, { 'content-type': 'text/plain' }) + res.end('vite-proxy: Proxy connection refused') + } + }) + }, + }, }, } diff --git a/src/npm-fastui/src/components/Custom.tsx b/src/npm-fastui/src/components/Custom.tsx index 8ef1fb5f..f875fbaf 100644 --- a/src/npm-fastui/src/components/Custom.tsx +++ b/src/npm-fastui/src/components/Custom.tsx @@ -1,14 +1,13 @@ -import { FC, useContext } from 'react' +import { FC } from 'react' import type { Custom } from '../models' -import { ErrorContext } from '../hooks/error' +import { DisplayError } from '../hooks/error' import { JsonComp } from './Json' export const CustomComp: FC = (props) => { const { data, subType, library } = props - const { DisplayError } = useContext(ErrorContext) const description = [`The custom component "${subType}"`] if (library) { diff --git a/src/npm-fastui/src/components/error.tsx b/src/npm-fastui/src/components/error.tsx new file mode 100644 index 00000000..f078255c --- /dev/null +++ b/src/npm-fastui/src/components/error.tsx @@ -0,0 +1,24 @@ +import { FC } from 'react' + +import type { Error } from '../models' + +import { useClassName } from '../hooks/className' + +export const ErrorComp: FC = (props) => { + const { title, description, statusCode, children } = props + return ( + <> +
+ {statusCode === 502 ? ( + <>Backend server down. + ) : ( + <> +

{title}

+ {description} + + )} +
+ {children} + + ) +} diff --git a/src/npm-fastui/src/components/index.tsx b/src/npm-fastui/src/components/index.tsx index bfccec08..7419b3ae 100644 --- a/src/npm-fastui/src/components/index.tsx +++ b/src/npm-fastui/src/components/index.tsx @@ -1,8 +1,8 @@ -import { useContext, FC } from 'react' +import { FC } from 'react' import type { FastProps, Display, Text, ServerLoad, PageTitle, FireEvent } from '../models' -import { ErrorContext } from '../hooks/error' +import { DisplayError } from '../hooks/error' import { useCustomRender } from '../hooks/config' import { unreachable } from '../tools' @@ -37,6 +37,7 @@ import { ImageComp } from './image' import { IframeComp } from './Iframe' import { VideoComp } from './video' import { FireEventComp } from './FireEvent' +import { ErrorComp } from './error' import { CustomComp } from './Custom' // TODO some better way to export components @@ -70,6 +71,7 @@ export { IframeComp, VideoComp, FireEventComp, + ErrorComp, CustomComp, LinkRender, } @@ -85,8 +87,6 @@ export const AnyCompList: FC<{ propsList: FastProps[] }> = ({ propsList }) => ( ) export const AnyComp: FC = (props) => { - const { DisplayError } = useContext(ErrorContext) - const CustomRenderComp = useCustomRender(props) if (CustomRenderComp) { return @@ -155,6 +155,8 @@ export const AnyComp: FC = (props) => { return case 'FireEvent': return + case 'Error': + return case 'Custom': return default: diff --git a/src/npm-fastui/src/hooks/error.tsx b/src/npm-fastui/src/hooks/error.tsx index 94a36256..aaa84f76 100644 --- a/src/npm-fastui/src/hooks/error.tsx +++ b/src/npm-fastui/src/hooks/error.tsx @@ -1,40 +1,49 @@ import { createContext, FC, ReactNode, useCallback, useContext, useState } from 'react' +import type { Error as ErrorProps } from '../models' + +import { ErrorComp } from '../components' + +import { useCustomRender } from './config' + interface ErrorDetails { title: string description: string + statusCode?: number } interface ErrorDisplayProps extends ErrorDetails { children?: ReactNode } -export type ErrorDisplayType = FC - interface ErrorContextType { error: ErrorDetails | null setError: (error: ErrorDetails | null) => void - DisplayError: ErrorDisplayType } -const DefaultErrorDisplay: ErrorDisplayType = ({ title, description, children }) => ( - <> -
-

{title}

- {description} -
- {children} - -) +export const DisplayError: FC = ({ title, description, statusCode, children }) => { + const props: ErrorProps = { + title, + description, + statusCode, + children, + type: 'Error', + } + const CustomRenderComp = useCustomRender(props) + if (CustomRenderComp) { + return + } else { + return + } +} export const ErrorContext = createContext({ error: null, setError: () => null, - DisplayError: DefaultErrorDisplay, }) const MaybeError: FC<{ children: ReactNode }> = ({ children }) => { - const { error, DisplayError } = useContext(ErrorContext) + const { error } = useContext(ErrorContext) if (error) { return {children} } else { @@ -43,11 +52,10 @@ const MaybeError: FC<{ children: ReactNode }> = ({ children }) => { } interface Props { - DisplayError?: ErrorDisplayType children: ReactNode } -export const ErrorContextProvider: FC = ({ DisplayError, children }) => { +export const ErrorContextProvider: FC = ({ children }) => { const [error, setErrorState] = useState(null) const setError = useCallback( @@ -59,10 +67,9 @@ export const ErrorContextProvider: FC = ({ DisplayError, children }) => { }, [setErrorState], ) - const contextValue: ErrorContextType = { error, setError, DisplayError: DisplayError ?? DefaultErrorDisplay } return ( - + {children} ) diff --git a/src/npm-fastui/src/index.tsx b/src/npm-fastui/src/index.tsx index 921854b5..005736c5 100644 --- a/src/npm-fastui/src/index.tsx +++ b/src/npm-fastui/src/index.tsx @@ -1,6 +1,5 @@ import { FC, ReactNode } from 'react' -import type { ErrorDisplayType } from './hooks/error' import type { FastProps } from './models' import { LocationProvider } from './hooks/locationContext' @@ -26,7 +25,6 @@ export interface FastUIProps { Spinner?: FC NotFound?: FC<{ url: string }> Transition?: FC<{ children: ReactNode; transitioning: boolean }> - DisplayError?: ErrorDisplayType classNameGenerator?: ClassNameGenerator customRender?: CustomRender // defaults to `process.env.NODE_ENV === 'development' @@ -34,17 +32,17 @@ export interface FastUIProps { } export function FastUI(props: FastUIProps) { - const { classNameGenerator, DisplayError, devMode, ...rest } = props + const { classNameGenerator, devMode, ...rest } = props return ( - - - + + + - - - + + + ) } diff --git a/src/npm-fastui/src/models.d.ts b/src/npm-fastui/src/models.d.ts index ff5ee388..cf27c35c 100644 --- a/src/npm-fastui/src/models.d.ts +++ b/src/npm-fastui/src/models.d.ts @@ -25,6 +25,7 @@ export type FastProps = | Iframe | Video | FireEvent + | Error | Custom | Table | Pagination @@ -243,6 +244,14 @@ export interface FireEvent { message?: string type: 'FireEvent' } +export interface Error { + title: string + description: string + statusCode?: number + className?: ClassName + type: 'Error' + children?: ReactNode +} export interface Custom { data: JsonData subType: string diff --git a/src/npm-fastui/src/tools.ts b/src/npm-fastui/src/tools.ts index 9a9b56b9..dd698f2f 100644 --- a/src/npm-fastui/src/tools.ts +++ b/src/npm-fastui/src/tools.ts @@ -12,7 +12,12 @@ export function useRequest(): (args: RequestArgs) => Promise<[number, any]> { try { return await request(args) } catch (e) { - setError({ title: 'Request Error', description: (e as any)?.message }) + const title = 'Request Error' + if (e instanceof RequestError) { + setError({ title, description: e.message, statusCode: e.status }) + } else { + setError({ title, description: (e as any)?.message }) + } throw e } }, diff --git a/src/python-fastui/fastui/components/__init__.py b/src/python-fastui/fastui/components/__init__.py index 863b2eca..d628a63e 100644 --- a/src/python-fastui/fastui/components/__init__.py +++ b/src/python-fastui/fastui/components/__init__.py @@ -47,6 +47,7 @@ 'Image', 'Iframe', 'FireEvent', + 'Error', 'Custom', 'Table', 'Pagination', @@ -271,6 +272,23 @@ class FireEvent(_p.BaseModel, extra='forbid'): type: _t.Literal['FireEvent'] = 'FireEvent' +class Error(_p.BaseModel, extra='forbid'): + title: str + description: str + status_code: _t.Union[int, None] = _p.Field(None, serialization_alias='statusCode') + class_name: _class_name.ClassNameField = None + type: _t.Literal['Error'] = 'Error' + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: _core_schema.CoreSchema, handler: _p.GetJsonSchemaHandler + ) -> _t.Any: + # add `children` to the schema so it can be used in the client + json_schema = handler(core_schema) + json_schema['properties']['children'] = {'tsType': 'ReactNode'} + return json_schema + + class Custom(_p.BaseModel, extra='forbid'): data: _types.JsonData sub_type: str = _p.Field(serialization_alias='subType') @@ -301,6 +319,7 @@ class Custom(_p.BaseModel, extra='forbid'): Iframe, Video, FireEvent, + Error, Custom, Table, Pagination, From 4037679efdc9566819cab390bb99ad7402c72c34 Mon Sep 17 00:00:00 2001 From: Dominik Vogt <44237223+Dejiah@users.noreply.github.com> Date: Wed, 7 Feb 2024 16:25:08 +0100 Subject: [PATCH 03/27] Added textarea as form field (#139) --- demo/forms.py | 3 +- src/npm-fastui-bootstrap/src/index.tsx | 2 ++ src/npm-fastui/src/components/FormField.tsx | 28 ++++++++++++++++ src/npm-fastui/src/components/index.tsx | 3 ++ src/npm-fastui/src/models.d.ts | 34 ++++++++++++++++++-- src/python-fastui/fastui/components/forms.py | 12 ++++++- src/python-fastui/fastui/forms.py | 7 +++- src/python-fastui/fastui/json_schema.py | 23 ++++++++++++- src/python-fastui/tests/test_forms.py | 25 +++++++++++++- 9 files changed, 130 insertions(+), 7 deletions(-) diff --git a/demo/forms.py b/demo/forms.py index c94a60db..f0f7c7ef 100644 --- a/demo/forms.py +++ b/demo/forms.py @@ -9,7 +9,7 @@ from fastui import AnyComponent, FastUI from fastui import components as c from fastui.events import GoToEvent, PageEvent -from fastui.forms import FormFile, SelectSearchResponse, fastui_form +from fastui.forms import FormFile, SelectSearchResponse, Textarea, fastui_form from httpx import AsyncClient from pydantic import BaseModel, EmailStr, Field, SecretStr, field_validator from pydantic_core import PydanticCustomError @@ -143,6 +143,7 @@ class BigModel(BaseModel): name: str | None = Field( None, description='This field is not required, it must start with a capital letter if provided' ) + info: Annotated[str | None, Textarea(rows=5)] = Field(None, description='Optional free text information about you.') profile_pic: Annotated[UploadFile, FormFile(accept='image/*', max_size=16_000)] = Field( description='Upload a profile picture, must not be more than 16kb' ) diff --git a/src/npm-fastui-bootstrap/src/index.tsx b/src/npm-fastui-bootstrap/src/index.tsx index 91f5fb27..b4dfe0c1 100644 --- a/src/npm-fastui-bootstrap/src/index.tsx +++ b/src/npm-fastui-bootstrap/src/index.tsx @@ -66,11 +66,13 @@ export const classNameGenerator: ClassNameGenerator = ({ } } case 'FormFieldInput': + case 'FormFieldTextarea': case 'FormFieldBoolean': case 'FormFieldSelect': case 'FormFieldSelectSearch': case 'FormFieldFile': switch (subElement) { + case 'textarea': case 'input': return { 'form-control': type !== 'FormFieldBoolean', diff --git a/src/npm-fastui/src/components/FormField.tsx b/src/npm-fastui/src/components/FormField.tsx index 31eeca50..ea6375db 100644 --- a/src/npm-fastui/src/components/FormField.tsx +++ b/src/npm-fastui/src/components/FormField.tsx @@ -4,6 +4,7 @@ import Select, { StylesConfig } from 'react-select' import type { FormFieldInput, + FormFieldTextarea, FormFieldBoolean, FormFieldFile, FormFieldSelect, @@ -44,6 +45,32 @@ export const FormFieldInputComp: FC = (props) => { ) } +interface FormFieldTextareaProps extends FormFieldTextarea { + onChange?: PrivateOnChange +} + +export const FormFieldTextareaComp: FC = (props) => { + const { name, placeholder, required, locked, rows, cols } = props + return ( +
+