Skip to content

Commit 6d007c3

Browse files
committed
add file upload example
1 parent 683a9bd commit 6d007c3

File tree

5 files changed

+206
-30
lines changed

5 files changed

+206
-30
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ Files in `src`:
1010
- [components/Layout.tsx](./src/components/Layout.tsx) app layout, routing and nav
1111
- [components/UISchema.tsx](./src/components/UISchema.tsx) UI-Schema widget setup
1212
- example pages:
13-
- [pages/PageSimpleForm.tsx](./src/pages/PageSimpleForm.tsx) with simple form, usage of `WidgetCountrySelect`
14-
- [pages/PageCustomForm.tsx](./src/pages/PageCustomForm.tsx) with custom rendered form, usage of `WidgetCountrySelect`
13+
- [pages/PageSimpleForm.tsx](./src/pages/PageSimpleForm.tsx), automatic rendered, usage of `WidgetCountrySelect`
14+
- [pages/PageCustomForm.tsx](./src/pages/PageCustomForm.tsx), custom rendered, usage of `WidgetCountrySelect`
15+
- [pages/PageCustomUpload.tsx](./src/pages/PageCustomUpload.tsx), custom rendered, native file upload widget
1516
- ~multiple~ widgets in: [Widgets/](./src/components/Widgets)
1617
- [WidgetCountrySelect.tsx](./src/components/Widgets/WidgetCountrySelect.tsx), uses an API to populate the `Select` options
1718

src/components/DataDebug.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import React from 'react'
22
import Typography from '@mui/material/Typography'
3+
import Paper from '@mui/material/Paper'
34
import Box from '@mui/material/Box'
45
import { useUIStore } from '@ui-schema/ui-schema/UIStore'
56

67
export const DataDebug = () => {
78
const {store} = useUIStore()
89

9-
return <Box mx={1} my={2}>
10-
<Typography variant={'caption'} gutterBottom>Data</Typography>
11-
<pre><code>{JSON.stringify(store?.valuesToJS() || null, undefined, 4)}</code></pre>
10+
return <Box mx={1} mt={4} mb={1}>
11+
<Paper variant={'outlined'} style={{borderRadius: 5}}>
12+
<Box p={1}>
13+
<Typography variant={'caption'} color={'textSecondary'}>Values Debug</Typography>
14+
<pre style={{margin: 0}}><code>{JSON.stringify(store?.valuesToJS() || null, undefined, 4)}</code></pre>
15+
</Box>
16+
</Paper>
1217
</Box>
1318
}

src/components/Layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import ListItemText from '@mui/material/ListItemText'
77
import { PageHome } from '../pages/PageHome'
88
import { PageSimpleForm } from '../pages/PageSimpleForm'
99
import { PageCustomForm } from '../pages/PageCustomForm'
10+
import { PageCustomUpload } from '../pages/PageCustomUpload'
1011

1112
export const Nav: React.FC<{}> = () => {
1213
const navigate = useNavigate()
@@ -19,6 +20,9 @@ export const Nav: React.FC<{}> = () => {
1920
<ListItemButton onClick={() => navigate('/custom')} selected={'/custom' === location.pathname}>
2021
<ListItemText primary={'Custom Rendering'}/>
2122
</ListItemButton>
23+
<ListItemButton onClick={() => navigate('/custom-upload')} selected={'/custom-upload' === location.pathname}>
24+
<ListItemText primary={'File Upload (Custom)'}/>
25+
</ListItemButton>
2226
</MuiList>
2327
</Box>
2428
}
@@ -42,6 +46,7 @@ export const Layout: React.ComponentType<{}> = () => {
4246
<Route path={'/'} element={<PageHome/>}/>
4347
<Route path={'/simple'} element={<PageSimpleForm/>}/>
4448
<Route path={'/custom'} element={<PageCustomForm/>}/>
49+
<Route path={'/custom-upload'} element={<PageCustomUpload/>}/>
4550
</Routes>
4651
</div>
4752
}

src/pages/PageCustomForm.tsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,31 +47,6 @@ const schema = createOrderedMap({
4747
},
4848
} as JsonSchema)
4949

50-
const DemoComponent = () => {
51-
const showValidity = true
52-
const [store, setStore] = React.useState(() => createStore(OrderedMap({})))
53-
54-
const onChange: onChangeHandler = React.useCallback(
55-
(actions) => setStore(storeUpdater(actions)),
56-
[setStore],
57-
)
58-
59-
return <React.Fragment>
60-
<UIStoreProvider
61-
store={store}
62-
onChange={onChange}
63-
showValidity={showValidity}
64-
>
65-
<CustomFormContent
66-
schema={schema}
67-
showValidity={showValidity}
68-
/>
69-
70-
<DataDebug/>
71-
</UIStoreProvider>
72-
</React.Fragment>
73-
}
74-
7550
const WidgetTextField = applyPluginStack(StringRenderer)
7651
const CountrySelect = applyPluginStack(WidgetCountrySelect)
7752

@@ -118,6 +93,31 @@ const CustomFormContent: React.FC<{
11893
</ObjectGroup>
11994
}
12095

96+
const DemoComponent = () => {
97+
const showValidity = true
98+
const [store, setStore] = React.useState(() => createStore(OrderedMap({})))
99+
100+
const onChange: onChangeHandler = React.useCallback(
101+
(actions) => setStore(storeUpdater(actions)),
102+
[setStore],
103+
)
104+
105+
return <React.Fragment>
106+
<UIStoreProvider
107+
store={store}
108+
onChange={onChange}
109+
showValidity={showValidity}
110+
>
111+
<CustomFormContent
112+
schema={schema}
113+
showValidity={showValidity}
114+
/>
115+
116+
<DataDebug/>
117+
</UIStoreProvider>
118+
</React.Fragment>
119+
}
120+
121121
export const PageCustomForm: React.ComponentType = () => {
122122
return <>
123123
<Container maxWidth={'md'} fixed style={{display: 'flex'}}>

src/pages/PageCustomUpload.tsx

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import React from 'react'
2+
import Container from '@mui/material/Container'
3+
import Link from '@mui/material/Link'
4+
import Box from '@mui/material/Box'
5+
import Grid from '@mui/material/Grid'
6+
import Typography from '@mui/material/Typography'
7+
import CircularProgress from '@mui/material/CircularProgress'
8+
import { Nav } from '../components/Layout'
9+
import {
10+
createOrderedMap, createStore, JsonSchema,
11+
onChangeHandler, StoreSchemaType, storeUpdater,
12+
UIStoreProvider, StoreKeys, ObjectGroup,
13+
extractValue, applyPluginStack, WidgetProps, WithValue,
14+
PROGRESS_START, PROGRESS_DONE, PROGRESS_ERROR, PROGRESS_NONE,
15+
PROGRESS,
16+
} from '@ui-schema/ui-schema'
17+
import { List, OrderedMap } from 'immutable'
18+
import { DataDebug } from '../components/DataDebug'
19+
20+
const schema = createOrderedMap({
21+
type: 'object',
22+
properties: {
23+
file: {
24+
type: 'object',
25+
properties: {
26+
link: {
27+
type: 'string',
28+
},
29+
expiry: {
30+
type: 'string',
31+
},
32+
name: {
33+
type: 'string',
34+
},
35+
},
36+
},
37+
},
38+
} as JsonSchema)
39+
40+
const FileUpload: React.ComponentType<WidgetProps & WithValue> = ({storeKeys, onChange, schema, required, value}) => {
41+
const [loading, setLoading] = React.useState<PROGRESS>(PROGRESS_NONE)
42+
return <Box style={{display: 'flex', flexDirection: 'column'}}>
43+
<input
44+
type={'file'}
45+
onChange={e => {
46+
setLoading(PROGRESS_START)
47+
const formData = new FormData()
48+
// @ts-ignore
49+
formData.append('file', e.target.files[0])
50+
// todo: handle `is uploading`
51+
fetch('https://file.io', {method: 'POST', body: formData})
52+
.then(r => r.json())
53+
.then(data => {
54+
if(data.status === 200) {
55+
setLoading(PROGRESS_DONE)
56+
console.log('file uploaded!', data)
57+
onChange({
58+
type: 'set',
59+
scopes: ['value'],
60+
storeKeys: storeKeys,
61+
data: {
62+
value: OrderedMap({
63+
link: data.link,
64+
expires: data.expires,
65+
name: data.name,
66+
}),
67+
},
68+
schema,
69+
required,
70+
})
71+
} else {
72+
// todo: implement validity handling on error
73+
console.error('File upload error!', data)
74+
setLoading(PROGRESS_ERROR)
75+
}
76+
})
77+
.catch((e) => {
78+
console.error('File upload fetch error!', e)
79+
setLoading(PROGRESS_ERROR)
80+
})
81+
}}
82+
/>
83+
84+
{loading === PROGRESS_START || loading === PROGRESS_ERROR ?
85+
<Box style={{display: 'flex', flexDirection: 'column', alignItems: 'center'}} my={1}>
86+
<CircularProgress/>
87+
<Typography variant={'caption'} color={loading === PROGRESS_ERROR ? 'error' : 'textSecondary'}>
88+
{loading === PROGRESS_ERROR ? 'upload failed' : 'uploading...'}
89+
</Typography>
90+
</Box> : null}
91+
{value?.get('link') ?
92+
<Box p={2}>
93+
<Typography variant={'subtitle2'} gutterBottom>Uploaded File:</Typography>
94+
<Box px={2}>
95+
<pre style={{margin: 0}}><code>{JSON.stringify(value?.toJS(), undefined, 4)}</code></pre>
96+
<Link href={value?.get('link')} target={'_blank'} rel={'noreferrer noopener'}>open download page</Link>
97+
</Box>
98+
</Box> :
99+
<Typography variant={'caption'} color={'textSecondary'}>no file uploaded</Typography>}
100+
</Box>
101+
}
102+
103+
const WidgetFileUpload = applyPluginStack(extractValue(FileUpload))
104+
105+
const CustomFormContent: React.FC<{
106+
storeKeys?: StoreKeys
107+
schema: StoreSchemaType
108+
showValidity?: boolean
109+
}> = ({storeKeys = List(), schema}) => {
110+
const [objectSchema, setObjectSchema] = React.useState<StoreSchemaType>(() => schema)
111+
return <ObjectGroup
112+
storeKeys={storeKeys}
113+
schema={schema} parentSchema={undefined}
114+
onSchema={setObjectSchema}
115+
>
116+
<Grid container dir={'columns'} spacing={4}>
117+
<WidgetFileUpload
118+
level={1}
119+
storeKeys={storeKeys.push('file') as StoreKeys}
120+
schema={objectSchema.getIn(['properties', 'file']) as unknown as StoreSchemaType}
121+
parentSchema={objectSchema}
122+
/>
123+
</Grid>
124+
</ObjectGroup>
125+
}
126+
127+
const DemoComponent = () => {
128+
const showValidity = true
129+
const [store, setStore] = React.useState(() => createStore(OrderedMap({})))
130+
131+
const onChange: onChangeHandler = React.useCallback(
132+
(actions) => setStore(storeUpdater(actions)),
133+
[setStore],
134+
)
135+
136+
return <React.Fragment>
137+
<UIStoreProvider
138+
store={store}
139+
onChange={onChange}
140+
showValidity={showValidity}
141+
>
142+
<CustomFormContent
143+
schema={schema}
144+
showValidity={showValidity}
145+
/>
146+
147+
<DataDebug/>
148+
</UIStoreProvider>
149+
</React.Fragment>
150+
}
151+
152+
export const PageCustomUpload: React.ComponentType = () => {
153+
return <>
154+
<Container maxWidth={'md'} fixed style={{display: 'flex'}}>
155+
<Nav/>
156+
<Box mx={2} py={1} style={{flexGrow: 1}}>
157+
<Box mb={2}>
158+
<Typography variant={'h1'} gutterBottom>UI-Schema Custom Upload</Typography>
159+
<Typography variant={'body2'} gutterBottom>Custom rendering with a native file upload field.</Typography>
160+
</Box>
161+
<DemoComponent/>
162+
</Box>
163+
</Container>
164+
</>
165+
}

0 commit comments

Comments
 (0)