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>),
|
...(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);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
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 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);
|
||||||
|
@ -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({
|
||||||
|
@ -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),
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user