Merge pull request #336 from webzard-io/feat/upload-file

Add FileInput Component
This commit is contained in:
tanbowensg 2022-03-08 18:14:37 +08:00 committed by GitHub
commit 4c00b8b38c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 179 additions and 20 deletions

View File

@ -132,7 +132,9 @@ export const ApiForm: React.FC<Props> = props => {
...(trait?.properties as Static<typeof FetchTraitPropertiesSchema>), ...(trait?.properties as Static<typeof FetchTraitPropertiesSchema>),
}); });
setTabIndex(0); setTabIndex(0);
}, [trait.properties]); // do not add formik into dependencies, otherwise it will cause infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [trait?.properties]);
useEffect(() => { useEffect(() => {
if (api.id) { if (api.id) {
setName(api.id); setName(api.id);

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Box } from '@chakra-ui/react'; import { Box, Select, Text, VStack } from '@chakra-ui/react';
import { import {
KeyValueWidget, KeyValueWidget,
WidgetProps, WidgetProps,
@ -28,19 +28,36 @@ export const Body: React.FC<Props> = props => {
formik.submitForm(); formik.submitForm();
}; };
const onBodyTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
formik.setFieldValue('bodyType', e.target.value);
formik.submitForm();
};
return ( return (
<Box> <VStack alignItems="start">
<KeyValueWidget <Text fontSize="lg" fontWeight="bold">
component={api} BodyType
schema={mergeWidgetOptionsIntoSchema(schema, { </Text>
minNum: 1, <Select value={values.bodyType} onChange={onBodyTypeChange}>
isShowHeader: true, <option value="json">JSON</option>
})} <option value="formData">Form Data</option>
level={1} </Select>
value={values.body} <Text fontSize="lg" fontWeight="bold">
services={services} Body
onChange={onChange} </Text>
/> <Box width="full">
</Box> <KeyValueWidget
component={api}
schema={mergeWidgetOptionsIntoSchema(schema, {
minNum: 1,
isShowHeader: true,
})}
level={1}
value={values.body}
services={services}
onChange={onChange}
/>
</Box>
</VStack>
); );
}; };

View File

@ -69,7 +69,7 @@ export const DataSource: React.FC<Props> = props => {
<VStack spacing="2" alignItems="stretch"> <VStack spacing="2" alignItems="stretch">
<Flex padding="4" paddingBottom="0"> <Flex padding="4" paddingBottom="0">
<Text fontSize="lg" fontWeight="bold"> <Text fontSize="lg" fontWeight="bold">
Datasource DataSource
</Text> </Text>
<Spacer /> <Spacer />
<Menu> <Menu>

View File

@ -0,0 +1,111 @@
import { Type } from '@sinclair/typebox';
import { css } from '@emotion/css';
import { implementRuntimeComponent } from '../../utils/buildKit';
import React, { useEffect, useRef } from 'react';
const PropsSchema = Type.Object({
multiple: Type.Boolean({
title: 'Select Multiple Files',
category: 'Basic',
}),
hideDefaultInput: Type.Boolean({
title: 'Hide Default Input',
category: 'Basic',
}),
fileTypes: Type.Array(Type.String(), {
title: 'File Types',
description: 'The accept value of Input. Example: ["jpg", "png", "svg", "gif"].',
category: 'Basic',
}),
});
const StateSchema = Type.Object({
// actually, the type of files is 'File[]'
// but JSON schema dose not has this type, so I mock it.
files: Type.Array(
Type.Object({
lastModified: Type.Number(),
name: Type.String(),
size: Type.Number(),
type: Type.String(),
})
),
});
export default implementRuntimeComponent({
version: 'core/v1',
metadata: {
name: 'fileInput',
displayName: 'File Input',
description: 'Select file',
isDraggable: true,
isResizable: false,
exampleProperties: {
multiple: false,
hideDefaultInput: false,
fileTypes: [],
},
exampleSize: [1, 1],
annotations: {
category: 'Input',
},
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {
selectFile: Type.Object({}),
},
slots: ['content'],
styleSlots: ['content'],
events: [],
},
})(
({
hideDefaultInput,
multiple,
fileTypes,
mergeState,
subscribeMethods,
customStyle,
elementRef,
slotsElements,
}) => {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
mergeState({ files: [] });
subscribeMethods({
selectFile: () => {
inputRef.current?.click();
},
});
});
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
mergeState({
files: Array.prototype.slice.call(e.target.files) || [],
});
};
return (
<div
ref={elementRef}
className={css`
${customStyle?.content}
`}
>
<input
style={{ display: hideDefaultInput ? 'none' : 'block' }}
ref={inputRef}
type="file"
multiple={multiple}
accept={fileTypes.join(',')}
onChange={onChange}
/>
{slotsElements.content}
</div>
);
}
);

View File

@ -7,6 +7,7 @@ import CoreRouter from '../components/core/Router';
import CoreDummy from '../components/core/Dummy'; import CoreDummy from '../components/core/Dummy';
import CoreModuleContainer from '../components/core/ModuleContainer'; import CoreModuleContainer from '../components/core/ModuleContainer';
import CoreStack from '../components/core/Stack'; import CoreStack from '../components/core/Stack';
import CoreFileInput from '../components/core/FileInput';
// traits // traits
import CoreArrayState from '../traits/core/ArrayState'; import CoreArrayState from '../traits/core/ArrayState';
@ -201,6 +202,7 @@ export function initRegistry(
registry.registerComponent(CoreDummy); registry.registerComponent(CoreDummy);
registry.registerComponent(CoreModuleContainer); registry.registerComponent(CoreModuleContainer);
registry.registerComponent(CoreStack); registry.registerComponent(CoreStack);
registry.registerComponent(CoreFileInput);
registry.registerTrait(CoreState); registry.registerTrait(CoreState);
registry.registerTrait(CoreArrayState); registry.registerTrait(CoreArrayState);

View File

@ -6,7 +6,6 @@ import { FetchTraitPropertiesSchema } from '../../types/traitPropertiesSchema';
const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSchema>> = const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSchema>> =
() => { () => {
const hasFetchedMap = new Map<string, boolean>(); const hasFetchedMap = new Map<string, boolean>();
return ({ return ({
trait, trait,
url, url,
@ -14,6 +13,7 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSche
lazy: _lazy, lazy: _lazy,
headers: _headers, headers: _headers,
body, body,
bodyType,
mergeState, mergeState,
services, services,
subscribeMethods, subscribeMethods,
@ -24,7 +24,7 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSche
const lazy = _lazy === undefined ? true : _lazy; const lazy = _lazy === undefined ? true : _lazy;
const fetchData = () => { const fetchData = () => {
// TODO: clear when component destory // TODO: clear when component destroy
hasFetchedMap.set(hashId, true); hasFetchedMap.set(hashId, true);
// FIXME: listen to the header change // FIXME: listen to the header change
const headers = new Headers(); const headers = new Headers();
@ -42,16 +42,35 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSche
}, },
}); });
let reqBody: string | FormData = '';
switch (bodyType) {
case 'json':
reqBody = JSON.stringify(body);
break;
case 'formData':
reqBody = new FormData();
for (const key in body) {
reqBody.append(key, body[key]);
}
break;
}
// fetch data // fetch data
fetch(url, { fetch(url, {
method, method,
headers, headers,
body: method === 'get' ? undefined : JSON.stringify(body), body: reqBody,
}).then( }).then(
async response => { async response => {
if (response.ok) { if (response.ok) {
// handle 20x/30x // handle 20x/30x
const data = await response.json(); let data: any;
if (response.headers.get('Content-Type') === 'application/json') {
data = await response.json();
} else {
data = await response.text();
}
mergeState({ mergeState({
fetch: { fetch: {
loading: false, loading: false,
@ -94,6 +113,7 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSche
}); });
} }
}, },
async error => { async error => {
console.warn(error); console.warn(error);
mergeState({ mergeState({

View File

@ -56,6 +56,13 @@ export const FetchTraitPropertiesSchema = Type.Object({
body: Type.Record(Type.String(), Type.String(), { body: Type.Record(Type.String(), Type.String(), {
title: 'Body', title: 'Body',
}), }),
bodyType: Type.KeyOf(
Type.Object({
json: Type.String(),
formData: Type.String(),
}),
{ title: 'Body Type' },
),
onComplete: Type.Array(EventCallBackHandlerSchema), onComplete: Type.Array(EventCallBackHandlerSchema),
onError: Type.Array(EventCallBackHandlerSchema), onError: Type.Array(EventCallBackHandlerSchema),
}); });