mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-27 08:39:59 +08:00
Merge pull request #336 from webzard-io/feat/upload-file
Add FileInput Component
This commit is contained in:
commit
4c00b8b38c
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
111
packages/runtime/src/components/core/FileInput.tsx
Normal file
111
packages/runtime/src/components/core/FileInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
);
|
@ -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);
|
||||
|
@ -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({
|
||||
|
@ -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),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user