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>),
});
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(() => {
if (api.id) {
setName(api.id);

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { Box, Select, Text, VStack } from '@chakra-ui/react';
import {
KeyValueWidget,
WidgetProps,
@ -28,19 +28,36 @@ export const Body: React.FC<Props> = props => {
formik.submitForm();
};
const onBodyTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
formik.setFieldValue('bodyType', e.target.value);
formik.submitForm();
};
return (
<Box>
<KeyValueWidget
component={api}
schema={mergeWidgetOptionsIntoSchema(schema, {
minNum: 1,
isShowHeader: true,
})}
level={1}
value={values.body}
services={services}
onChange={onChange}
/>
</Box>
<VStack alignItems="start">
<Text fontSize="lg" fontWeight="bold">
BodyType
</Text>
<Select value={values.bodyType} onChange={onBodyTypeChange}>
<option value="json">JSON</option>
<option value="formData">Form Data</option>
</Select>
<Text fontSize="lg" fontWeight="bold">
Body
</Text>
<Box width="full">
<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">
<Flex padding="4" paddingBottom="0">
<Text fontSize="lg" fontWeight="bold">
Datasource
DataSource
</Text>
<Spacer />
<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 CoreModuleContainer from '../components/core/ModuleContainer';
import CoreStack from '../components/core/Stack';
import CoreFileInput from '../components/core/FileInput';
// traits
import CoreArrayState from '../traits/core/ArrayState';
@ -201,6 +202,7 @@ export function initRegistry(
registry.registerComponent(CoreDummy);
registry.registerComponent(CoreModuleContainer);
registry.registerComponent(CoreStack);
registry.registerComponent(CoreFileInput);
registry.registerTrait(CoreState);
registry.registerTrait(CoreArrayState);

View File

@ -6,7 +6,6 @@ import { FetchTraitPropertiesSchema } from '../../types/traitPropertiesSchema';
const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSchema>> =
() => {
const hasFetchedMap = new Map<string, boolean>();
return ({
trait,
url,
@ -14,6 +13,7 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSche
lazy: _lazy,
headers: _headers,
body,
bodyType,
mergeState,
services,
subscribeMethods,
@ -24,7 +24,7 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSche
const lazy = _lazy === undefined ? true : _lazy;
const fetchData = () => {
// TODO: clear when component destory
// TODO: clear when component destroy
hasFetchedMap.set(hashId, true);
// FIXME: listen to the header change
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(url, {
method,
headers,
body: method === 'get' ? undefined : JSON.stringify(body),
body: reqBody,
}).then(
async response => {
if (response.ok) {
// 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({
fetch: {
loading: false,
@ -94,6 +113,7 @@ const FetchTraitFactory: TraitImplFactory<Static<typeof FetchTraitPropertiesSche
});
}
},
async error => {
console.warn(error);
mergeState({

View File

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