add fetch trait form

This commit is contained in:
Bowen Tan 2021-10-25 17:47:35 +08:00
parent 2c7efc149c
commit 4ea4b9f8dc
21 changed files with 321 additions and 85 deletions

View File

@ -1,7 +1,7 @@
import React from 'react';
import _ from 'lodash';
import { EmotionJSX } from '@emotion/react/types/jsx-namespace';
import { FormControl, FormLabel, Input, VStack } from '@chakra-ui/react';
import { EmotionJSX } from '@emotion/react/types/jsx-namespace';
import { TSchema } from '@sinclair/typebox';
import { Application } from '@meta-ui/core';
import { parseType, parseTypeBox } from '@meta-ui/runtime';
@ -14,6 +14,7 @@ import {
} from '../../operations/Operations';
import { EventTraitForm } from './EventTraitForm';
import { GeneralTraitFormList } from './GeneralTraitFormList';
import { FetchTraitForm } from './FetchTraitForm';
type Props = { selectedId: string; app: Application };
@ -69,6 +70,7 @@ export const ComponentForm: React.FC<Props> = props => {
parseTypeBox(cImpl.spec.properties as TSchema),
selectedComponent.properties
);
const propertyFields = Object.keys(properties || []).map(key => {
const value = properties![key];
return renderField({ key, value, fullKey: key, selectedId });
@ -109,6 +111,7 @@ export const ComponentForm: React.FC<Props> = props => {
</FormControl>
{propertyFields.length > 0 ? propertyForm : null}
<EventTraitForm component={selectedComponent} />
<FetchTraitForm component={selectedComponent} />
<GeneralTraitFormList component={selectedComponent} />
</VStack>
);

View File

@ -16,7 +16,6 @@ import { EventHandlerSchema } from '@meta-ui/runtime';
import { registry } from '../../../metaUI';
import { useAppModel } from '../../../operations/useAppModel';
import { formWrapperCSS } from '../style';
import produce from 'immer';
import { KeyValueEditor } from '../../KeyValueEditor';
type Props = {
@ -24,10 +23,11 @@ type Props = {
handler: Static<typeof EventHandlerSchema>;
onChange: (hanlder: Static<typeof EventHandlerSchema>) => void;
onRemove: () => void;
hideEventType?: boolean;
};
export const EventHandlerForm: React.FC<Props> = props => {
const { handler, eventTypes, onChange, onRemove } = props;
const { handler, eventTypes, onChange, onRemove, hideEventType } = props;
const { app } = useAppModel();
const [methods, setMethods] = useState<string[]>([]);
@ -117,7 +117,7 @@ export const EventHandlerForm: React.FC<Props> = props => {
<FormControl>
<FormLabel>Parameters</FormLabel>
<KeyValueEditor
initValue={handler.method.parameters}
initValue={formik.values.method.parameters}
onChange={json => {
formik.setFieldValue('method.parameters', json);
formik.submitForm();
@ -169,7 +169,7 @@ export const EventHandlerForm: React.FC<Props> = props => {
return (
<Box position="relative">
<VStack css={formWrapperCSS}>
{typeField}
{hideEventType ? null : typeField}
{targetField}
{methodField}
{parametersField}

View File

@ -0,0 +1,199 @@
import {
Box,
FormControl,
FormLabel,
HStack,
IconButton,
Input,
Select,
VStack,
} from '@chakra-ui/react';
import { Static } from '@sinclair/typebox';
import { AddIcon, CloseIcon } from '@chakra-ui/icons';
import { useFormik } from 'formik';
import produce from 'immer';
import { ApplicationComponent } from '@meta-ui/core';
import { EventHandlerSchema, FetchTraitPropertiesSchema } from '@meta-ui/runtime';
import { formWrapperCSS } from '../style';
import { KeyValueEditor } from '../../KeyValueEditor';
import { EventHandlerForm } from '../EventTraitForm/EventHandlerForm';
import {
ModifyTraitPropertiesOperation,
RemoveTraitOperation,
} from '../../../operations/Operations';
import { eventBus } from '../../../eventBus';
type EventHandler = Static<typeof EventHandlerSchema>;
type Props = {
component: ApplicationComponent;
};
const httpMethods = ['get', 'post', 'put', 'delete', 'patch'];
export const FetchTraitForm: React.FC<Props> = props => {
const { component } = props;
const fetchTrait = component.traits.find(t => t.type === 'core/v1/fetch')
?.properties as Static<typeof FetchTraitPropertiesSchema>;
if (!fetchTrait) {
return null;
}
const formik = useFormik({
initialValues: fetchTrait,
onSubmit: values => {
eventBus.send(
'operation',
new ModifyTraitPropertiesOperation(component.id, 'core/v1/fetch', values)
);
},
});
const urlField = (
<FormControl>
<FormLabel>URL</FormLabel>
<Input
name="url"
onChange={formik.handleChange}
onBlur={() => formik.submitForm()}
value={formik.values.url}
/>
</FormControl>
);
const methodField = (
<FormControl>
<FormLabel>Method</FormLabel>
<Select
name="method"
placeholder="Select Method"
onChange={formik.handleChange}
onBlur={() => formik.submitForm()}
value={formik.values.method}
>
{httpMethods.map(v => (
<option key={v} value={v}>
{v}
</option>
))}
</Select>
</FormControl>
);
const bodyField = (
<FormControl>
<FormLabel>Body</FormLabel>
<KeyValueEditor
initValue={formik.values.body}
onChange={json => {
formik.setFieldValue('body', json);
formik.submitForm();
}}
/>
</FormControl>
);
const headersField = (
<FormControl>
<FormLabel>Headers</FormLabel>
<KeyValueEditor
initValue={formik.values.headers}
onChange={json => {
formik.setFieldValue('headers', json);
formik.submitForm();
}}
/>
</FormControl>
);
const onAddHandler = () => {
const newHandler: EventHandler = {
type: '',
componentId: '',
method: {
name: '',
parameters: {},
},
disabled: false,
wait: {
type: 'delay',
time: 0,
},
};
formik.setFieldValue('onComplete', [...formik.values.onComplete, newHandler]);
};
const onCompleteField = (
<FormControl>
<HStack width="full" justify="space-between">
<FormLabel>onComplete</FormLabel>
<IconButton
aria-label="add event"
size="sm"
variant="ghost"
colorScheme="blue"
icon={<AddIcon />}
onClick={onAddHandler}
/>
</HStack>
{formik.values.onComplete.map((handler, i) => {
const onChange = (handler: EventHandler) => {
const newOnComplete = produce(formik.values.onComplete, draft => {
draft[i] = handler;
});
formik.setFieldValue('onComplete', newOnComplete);
formik.submitForm();
};
const onRemove = () => {
const newOnComplete = produce(formik.values.onComplete, draft => {
draft.splice(i, 1);
});
formik.setFieldValue('onComplete', newOnComplete);
formik.submitForm();
};
return (
<EventHandlerForm
key={i}
eventTypes={[]}
handler={handler}
hideEventType={true}
onChange={onChange}
onRemove={onRemove}
/>
);
})}
</FormControl>
);
return (
<Box width="full" position="relative">
<strong>Fetch</strong>
<HStack width="full" justify="space-between">
<VStack css={formWrapperCSS}>
{urlField}
{methodField}
{bodyField}
{headersField}
{onCompleteField}
</VStack>
</HStack>
<IconButton
position="absolute"
right="0"
top="0"
aria-label="remove event handler"
variant="ghost"
colorScheme="red"
size="xs"
icon={<CloseIcon />}
onClick={() => {
const i = component.traits.findIndex(t => t.type === 'core/v1/fetch');
eventBus.send('operation', new RemoveTraitOperation(component.id, i));
}}
/>
</Box>
);
};

View File

@ -0,0 +1 @@
export * from './FetchTraitForm';

View File

@ -1,7 +1,6 @@
import { AddIcon, ChevronDownIcon } from '@chakra-ui/icons';
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
import { useMemo } from 'react';
import { ignoreTraitsList } from '../../../constants';
import { registry } from '../../../metaUI';
type Props = {
@ -12,7 +11,7 @@ export const AddTraitButton: React.FC<Props> = props => {
const { onAddTrait } = props;
const traitTypes = useMemo(() => {
return registry.getAllTraitTypes().filter(type => !ignoreTraitsList.includes(type));
return registry.getAllTraitTypes();
}, []);
const menuItems = traitTypes.map(type => {

View File

@ -1,8 +1,11 @@
import { ApplicationComponent, ComponentTrait } from '@meta-ui/core';
import { HStack, IconButton, VStack } from '@chakra-ui/react';
import { parseTypeBox } from '@meta-ui/runtime';
import { CloseIcon } from '@chakra-ui/icons';
import { TSchema } from '@sinclair/typebox';
import { renderField } from '../ComponentForm';
import { formWrapperCSS } from '../style';
import { registry } from '../../../metaUI';
type Props = {
component: ApplicationComponent;
@ -13,7 +16,13 @@ type Props = {
export const GeneralTraitForm: React.FC<Props> = props => {
const { trait, component, onRemove } = props;
const fields = Object.keys(trait.properties || []).map(key => {
const tImpl = registry.getTraitByType(trait.type);
const properties = Object.assign(
parseTypeBox(tImpl.spec.properties as TSchema),
trait.properties
);
const fields = Object.keys(properties || []).map((key: string) => {
const value = trait.properties[key];
return renderField({
key,

View File

@ -1,6 +1,7 @@
import { CloseIcon } from '@chakra-ui/icons';
import { Button, Flex, HStack, IconButton, Input, VStack } from '@chakra-ui/react';
import { Button, HStack, IconButton, Input, VStack } from '@chakra-ui/react';
import produce from 'immer';
import { fromPairs, toPairs } from 'lodash';
import React, { useState } from 'react';
type Props = {
@ -10,21 +11,11 @@ type Props = {
export const KeyValueEditor: React.FC<Props> = props => {
const [rows, setRows] = useState<Array<[string, string]>>(() => {
if (!props.initValue) return [];
const res: Array<[string, string]> = [];
for (const key in props.initValue) {
res.push([key, props.initValue[key]]);
}
return res;
return toPairs(props.initValue);
});
const emitDataChange = (newRows: Array<[string, string]>) => {
const json = newRows.reduce<Record<string, string>>((res, curr) => {
if (curr[0]) {
res[curr[0]] = curr[1];
}
return res;
}, {});
const json = fromPairs(newRows);
props.onChange(json);
};

View File

@ -1,6 +1,6 @@
import { Application } from '@meta-ui/core';
export const ignoreTraitsList = ['core/v1/slot', 'core/v1/event'];
export const ignoreTraitsList = ['core/v1/slot', 'core/v1/event', 'core/v1/fetch'];
export const DefaultAppSchema: Application = {
kind: 'Application',

View File

@ -9,6 +9,7 @@ import {
ModifyComponentIdOperation,
AddTraitOperation,
RemoveTraitOperation,
ModifyTraitPropertiesOperation,
} from './Operations';
import { produce } from 'immer';
import { registry } from '../metaUI';
@ -175,6 +176,30 @@ export class AppModelManager {
this.undoStack.push(undoOperation);
}
break;
case 'modifyTraitProperties':
const mtpo = o as ModifyTraitPropertiesOperation;
let oldProperties;
newApp = produce(this.app, draft => {
draft.spec.components.forEach(c => {
if (c.id === mtpo.componentId) {
c.traits.forEach(t => {
if (t.type === mtpo.traitType) {
oldProperties = t.properties;
t.properties = mtpo.properties;
}
});
}
});
});
if (!noEffect) {
const undoOperation = new ModifyTraitPropertiesOperation(
mtpo.componentId,
mtpo.traitType,
oldProperties || {}
);
this.undoStack.push(undoOperation);
}
break;
case 'addTraitOperation':
const ato = o as AddTraitOperation;
let i = 0;
@ -203,10 +228,6 @@ export class AppModelManager {
}
});
});
if (!noEffect) {
// const removeTraitOperation = new AddTraitOperation(rto.componentId, trait.type);
// this.undoStack.push(removeTraitOperation);
}
break;
}
this.updateApp(newApp);

View File

@ -55,3 +55,12 @@ export class ModifyTraitPropertyOperation {
public propertyValue: any
) {}
}
export class ModifyTraitPropertiesOperation {
kind = 'modifyTraitProperties';
constructor(
public componentId: string,
public traitType: string,
public properties: Record<string, any>
) {}
}

View File

@ -77,13 +77,11 @@
name: 'query',
url: `{{ "https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com/apiservices/browseroutes/v1.0/"+fetch_list.country+"/CNY/zh-CN/CSHA/SZX/anytime/anytime"}}`,
method: 'get',
headers: [
{ key: 'x-rapidapi-key', value: `{{fetch_list.token}}` },
{
key: 'x-rapidapi-host',
value: 'skyscanner-skyscanner-flight-search-v1.p.rapidapi.com',
},
],
headers: {
'x-rapidapi-key': `{{fetch_list.token}}`,
'x-rapidapi-host':
'skyscanner-skyscanner-flight-search-v1.p.rapidapi.com',
},
lazy: true,
},
},

View File

@ -80,13 +80,11 @@
name: 'query',
url: `{{ "https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com/apiservices/browseroutes/v1.0/"+fetch_list.country+"/CNY/zh-CN/CSHA/SZX/anytime/anytime"}}`,
method: 'get',
headers: [
{ key: 'x-rapidapi-key', value: `{{fetch_btn.token}}` },
{
key: 'x-rapidapi-host',
value: 'skyscanner-skyscanner-flight-search-v1.p.rapidapi.com',
},
],
headers: {
'x-rapidapi-key': `{{fetch_btn.token}}`,
'x-rapidapi-host':
'skyscanner-skyscanner-flight-search-v1.p.rapidapi.com',
},
},
},
],

View File

@ -45,7 +45,9 @@
url: 'https://61373521eac1410017c18209.mockapi.io/Volume',
method: 'post',
lazy: true,
headers: [{ key: 'Content-Type', value: 'application/json' }],
headers: {
'Content-Type': 'application/json',
},
body: '{{ form.data }}',
onComplete: [
{

View File

@ -45,7 +45,9 @@
url: 'https://61373521eac1410017c18209.mockapi.io/Volume',
method: 'post',
lazy: true,
headers: [{ key: 'Content-Type', value: 'application/json' }],
headers: {
'Content-Type': 'application/json',
},
body: '{{ form.data }}',
onComplete: [
{

View File

@ -72,7 +72,9 @@
name: 'query',
url: 'https://61373521eac1410017c18209.mockapi.io/users/{{ table.selectedItem ? table.selectedItem.id : "" }}',
method: 'put',
headers: [{ key: 'Content-Type', value: 'application/json' }],
headers: {
'Content-Type': 'application/json',
},
body: '{{ {...table.selectedItem, name: nameInput.value } }}',
lazy: true,
onComplete: [

View File

@ -1,5 +1,5 @@
import { Type } from '@sinclair/typebox';
import { EventHandlerSchema } from '../../../types/EventHandlerSchema';
import { EventHandlerSchema } from '../../../types/TraitPropertiesSchema';
export const MajorKeyPropertySchema = Type.String();
export const RowsPerPagePropertySchema = Type.Number();

View File

@ -25,5 +25,5 @@ export * from './utils/parseType';
export * from './utils/parseTypeBox';
export * from './utils/encodeDragDataTransfer';
export * from './types/RuntimeSchema';
export * from './types/EventHandlerSchema';
export * from './types/TraitPropertiesSchema';
export * from './constants';

View File

@ -2,7 +2,7 @@ import { createTrait } from '@meta-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { debounce, throttle, delay } from 'lodash';
import { CallbackMap, TraitImplementation } from 'src/types/RuntimeSchema';
import { EventHandlerSchema } from '../../types/EventHandlerSchema';
import { EventHandlerSchema } from '../../types/TraitPropertiesSchema';
const useEventTrait: TraitImplementation<Static<typeof PropsSchema>> = ({
handlers,

View File

@ -1,11 +1,11 @@
import { createTrait } from '@meta-ui/core';
import { Static, Type } from '@sinclair/typebox';
import { TraitImplementation } from 'src/types/RuntimeSchema';
import { EventHandlerSchema } from '../../types/EventHandlerSchema';
import { FetchTraitPropertiesSchema } from '../../types/TraitPropertiesSchema';
const hasFetchedMap = new Map<string, boolean>();
const useFetchTrait: TraitImplementation<Static<typeof PropsSchema>> = ({
const useFetchTrait: TraitImplementation<Static<typeof FetchTraitPropertiesSchema>> = ({
url,
method,
lazy: _lazy,
@ -27,9 +27,8 @@ const useFetchTrait: TraitImplementation<Static<typeof PropsSchema>> = ({
// FIXME: listen to the header change
const headers = new Headers();
if (_headers) {
for (let i = 0; i < _headers.length; i++) {
const header = _headers[i];
headers.append(header.key, _headers[i].value);
for (const key in _headers) {
headers.append(key, _headers[key]);
}
}
@ -37,7 +36,7 @@ const useFetchTrait: TraitImplementation<Static<typeof PropsSchema>> = ({
fetch(url, {
method,
headers,
body: JSON.stringify(body),
body: method === 'get' ? undefined : JSON.stringify(body),
}).then(
async response => {
if (response.ok) {
@ -105,15 +104,6 @@ const useFetchTrait: TraitImplementation<Static<typeof PropsSchema>> = ({
};
};
const PropsSchema = Type.Object({
url: Type.String(), // {format:uri}?;
method: Type.String(), // {pattern: /^(get|post|put|delete)$/i}
lazy: Type.Boolean(),
headers: Type.Array(Type.Object({ key: Type.String(), value: Type.String() })),
body: Type.Any(),
onComplete: Type.Array(EventHandlerSchema),
});
export default {
...createTrait({
version: 'core/v1',
@ -122,7 +112,7 @@ export default {
description: 'fetch data to store',
},
spec: {
properties: PropsSchema,
properties: FetchTraitPropertiesSchema,
state: Type.Object({
fetch: Type.Object({
loading: Type.Boolean(),

View File

@ -1,23 +0,0 @@
import { Type } from '@sinclair/typebox';
export const EventHandlerSchema = Type.Object({
type: Type.String(),
componentId: Type.String(),
method: Type.Object({
name: Type.String(),
parameters: Type.Any(),
}),
wait: Type.Optional(
Type.Object({
type: Type.KeyOf(
Type.Object({
debounce: Type.String(),
throttle: Type.String(),
delay: Type.String(),
})
),
time: Type.Number(),
})
),
disabled: Type.Optional(Type.Boolean()),
});

View File

@ -0,0 +1,35 @@
import { Type } from '@sinclair/typebox';
export const EventHandlerSchema = Type.Object(
{
type: Type.String(),
componentId: Type.String(),
method: Type.Object({
name: Type.String(),
parameters: Type.Record(Type.String(), Type.String()),
}),
wait: Type.Optional(
Type.Object({
type: Type.KeyOf(
Type.Object({
debounce: Type.String(),
throttle: Type.String(),
delay: Type.String(),
})
),
time: Type.Number(),
})
),
disabled: Type.Optional(Type.Boolean()),
},
{ $id: 'eventHanlder' }
);
export const FetchTraitPropertiesSchema = Type.Object({
url: Type.String(), // {format:uri}?;
method: Type.String(), // {pattern: /^(get|post|put|delete)$/i}
lazy: Type.Boolean(),
headers: Type.Record(Type.String(), Type.String()),
body: Type.Record(Type.String(), Type.String()),
onComplete: Type.Array(EventHandlerSchema),
});