feat: custom request (#439)

* feat: custom api request

* fix: fix review problem

* fix: add after request successful tip

* fix: add after request successful tip

* fix: add filterByTk

* fix: add validate

* fix: update locale

* fix: update locale

* fix: update locale

* fix: update locale

* fix: update locale
This commit is contained in:
SemmyWong 2022-05-31 18:56:51 +08:00 committed by GitHub
parent 373c2b9a2d
commit 9f6e6f22a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 381 additions and 103 deletions

View File

@ -2,6 +2,7 @@ import { useField, useFieldSchema, useForm } from '@formily/react';
import { message, Modal } from 'antd';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { useAPIClient } from '../../api-client';
import { useCollection } from '../../collection-manager';
import { useRecord } from '../../record-provider';
import { useActionContext, useCompile } from '../../schema-component';
@ -54,6 +55,50 @@ const filterValue = (value) => {
return obj;
};
function getFormValues(filterByTk, field, form, fieldNames, getField, resource) {
let values = {};
for (const key in form.values) {
if (fieldNames.includes(key)) {
const collectionField = getField(key);
if (filterByTk) {
if (collectionField.interface === 'subTable') {
values[key] = form.values[key];
continue;
}
if (field.added && !field.added.has(key)) {
continue;
}
}
const items = form.values[key];
if (collectionField.interface === 'linkTo') {
const targetKey = collectionField.targetKey || 'id';
if (resource instanceof TableFieldResource) {
if (Array.isArray(items)) {
values[key] = filterValue(items);
} else if (items && typeof items === 'object') {
values[key] = filterValue(items);
} else {
values[key] = items;
}
} else {
if (Array.isArray(items)) {
values[key] = items.map((item) => item[targetKey]);
} else if (items && typeof items === 'object') {
values[key] = items[targetKey];
} else {
values[key] = items;
}
}
} else {
values[key] = form.values[key];
}
} else {
values[key] = form.values[key];
}
}
return values;
}
export const useCreateActionProps = () => {
const form = useForm();
const { field, resource, __parent } = useBlockRequestContext();
@ -64,6 +109,7 @@ export const useCreateActionProps = () => {
const actionField = useField();
const { fields, getField } = useCollection();
const compile = useCompile();
const filterByTk = useFilterByTk();
return {
async onClick() {
const fieldNames = fields.map((field) => field.name);
@ -72,37 +118,7 @@ export const useCreateActionProps = () => {
if (!skipValidator) {
await form.submit();
}
let values = {};
for (const key in form.values) {
if (fieldNames.includes(key)) {
const items = form.values[key];
const collectionField = getField(key);
if (collectionField.interface === 'linkTo') {
const targetKey = collectionField.targetKey || 'id';
if (resource instanceof TableFieldResource) {
if (Array.isArray(items)) {
values[key] = filterValue(items);
} else if (items && typeof items === 'object') {
values[key] = filterValue(items);
} else {
values[key] = items;
}
} else {
if (Array.isArray(items)) {
values[key] = items.map((item) => item[targetKey]);
} else if (items && typeof items === 'object') {
values[key] = items[targetKey];
} else {
values[key] = items;
}
}
} else {
values[key] = form.values[key];
}
} else {
values[key] = form.values[key];
}
}
const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource);
actionField.data = field.data || {};
actionField.data.loading = true;
await resource.create({
@ -186,6 +202,64 @@ export const useCustomizeUpdateActionProps = () => {
};
};
export const useCustomizeRequestActionProps = () => {
const apiClient = useAPIClient();
const history = useHistory();
const filterByTk = useFilterByTk();
const actionSchema = useFieldSchema();
const compile = useCompile();
const form = useForm();
const { fields, getField } = useCollection();
const { field, resource } = useBlockRequestContext();
return {
async onClick() {
const { skipValidator, onSuccess, requestSettings } = actionSchema?.['x-action-settings'] ?? {};
if (!requestSettings['url']) {
return;
}
if (skipValidator === false) {
await form.submit();
}
const headers = requestSettings['headers'] ? JSON.parse(requestSettings['headers']) : {};
const params = requestSettings['params'] ? JSON.parse(requestSettings['params']) : {};
const data = requestSettings['data'] ? JSON.parse(requestSettings['data']) : {};
const methods = ['POST', 'PUT', 'PATCH'];
if (actionSchema['x-action'] === 'customize:form:request' && methods.includes(requestSettings['method'])) {
const fieldNames = fields.map((field) => field.name);
const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource);
Object.assign(data, values);
}
await apiClient.request({
...requestSettings,
headers,
params,
data,
});
if (!onSuccess?.successMessage) {
return;
}
if (onSuccess?.manualClose) {
Modal.success({
title: compile(onSuccess?.successMessage),
onOk: async () => {
if (onSuccess?.redirecting && onSuccess?.redirectTo) {
if (isURL(onSuccess.redirectTo)) {
window.location.href = onSuccess.redirectTo;
} else {
history.push(onSuccess.redirectTo);
}
}
},
});
} else {
message.success(compile(onSuccess?.successMessage));
}
},
};
};
export const useUpdateActionProps = () => {
const form = useForm();
const filterByTk = useFilterByTk();
@ -204,44 +278,7 @@ export const useUpdateActionProps = () => {
await form.submit();
}
const fieldNames = fields.map((field) => field.name);
let values = {};
for (const key in form.values) {
if (fieldNames.includes(key)) {
const collectionField = getField(key);
if (collectionField.interface === 'subTable') {
values[key] = form.values[key];
continue;
}
if (field.added && !field.added.has(key)) {
continue;
}
const items = form.values[key];
if (collectionField.interface === 'linkTo') {
const targetKey = collectionField.targetKey || 'id';
if (resource instanceof TableFieldResource) {
if (Array.isArray(items)) {
values[key] = filterValue(items);
} else if (items && typeof items === 'object') {
values[key] = filterValue(items);
} else {
values[key] = items;
}
} else {
if (Array.isArray(items)) {
values[key] = items.map((item) => item[targetKey]);
} else if (items && typeof items === 'object') {
values[key] = items[targetKey];
} else {
values[key] = items;
}
}
} else {
values[key] = form.values[key];
}
} else {
values[key] = form.values[key];
}
}
const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource);
actionField.data = field.data || {};
actionField.data.loading = true;
await resource.update({

View File

@ -301,5 +301,15 @@ export default {
'After successful save': 'After successful save',
'Button background color': 'Button background color',
'Highlight': 'Highlight',
'Danger red': 'Danger red'
'Danger red': 'Danger red',
'Custom request': 'Custom request',
'Request settings': 'Request settings',
'Request URL': 'Request URL',
'Request method': 'Request method',
'Request query parameters': 'Request query parameters(JSON)',
'Request headers': 'Request headers(JSON)',
'Request body': 'Request body(JSON)',
'Request success': 'Request success',
'Invalid JSON format': 'Invalid JSON format',
'After successful request': 'After successful request'
}

View File

@ -536,5 +536,15 @@ export default {
'After clicking the custom button, the following fields of the current record will be saved according to the following form.': '点击当前自定义按钮时,当前数据以下字段将按照以下表单保存。',
'Button background color': '按钮颜色',
'Highlight': '高亮',
'Danger red': '红色'
'Danger red': '红色',
'Custom request': '自定义请求',
'Request settings': '请求设置',
'Request URL': '请求地址',
'Request method': '请求方法',
'Request query parameters': '请求查询参数(JSON格式)',
'Request headers': '请求头参数(JSON格式)',
'Request body': '请求体(JSON格式)',
'Request success': '请求成功',
'Invalid JSON format': '非法JSON格式',
'After successful request': '请求成功之后'
}

View File

@ -6,6 +6,7 @@ import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useActionContext, useCompile, useDesignable } from '../..';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { requestSettingsSchema } from './utils';
const MenuGroup = (props) => {
const fieldSchema = useFieldSchema();
@ -15,8 +16,18 @@ const MenuGroup = (props) => {
'customize:popup': t('Popup'),
'customize:update': t('Update record'),
'customize:save': t('Save record'),
'customize:table:request': t('Custom request'),
'customize:form:request': t('Custom request'),
};
if (!['customize:popup', 'customize:update', 'customize:save'].includes(actionType)) {
if (
![
'customize:popup',
'customize:update',
'customize:save',
'customize:table:request',
'customize:form:request',
].includes(actionType)
) {
return <>{props.children}</>;
}
return <Menu.ItemGroup title={`${t('Customize')} > ${actionTitles[actionType]}`}>{props.children}</Menu.ItemGroup>;
@ -162,6 +173,23 @@ export const ActionDesigner = (props) => {
}}
/>
)}
{isValid(fieldSchema?.['x-action-settings']?.requestSettings) && (
<SchemaSettings.ActionModalItem
title={t('Request settings')}
schema={requestSettingsSchema}
initialValues={fieldSchema?.['x-action-settings']?.requestSettings}
onSubmit={(requestSettings) => {
fieldSchema['x-action-settings']['requestSettings'] = requestSettings;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-action-settings': fieldSchema['x-action-settings'],
},
});
dn.refresh();
}}
/>
)}
{isValid(fieldSchema?.['x-action-settings']?.skipValidator) && (
<SchemaSettings.SwitchItem
title={t('Skip required validation')}
@ -218,6 +246,8 @@ export const ActionDesigner = (props) => {
{
'customize:save': t('After successful save'),
'customize:update': t('After successful update'),
'customize:table:request': t('After successful request'),
'customize:form:request': t('After successful request'),
}[actionType]
}
initialValues={fieldSchema?.['x-action-settings']?.['onSuccess']}
@ -227,6 +257,8 @@ export const ActionDesigner = (props) => {
title: {
'customize:save': t('After successful save'),
'customize:update': t('After successful update'),
'customize:table:request': t('After successful request'),
'customize:form:request': t('After successful request'),
}[actionType],
properties: {
successMessage: {

View File

@ -0,0 +1,68 @@
import type { ISchema } from '@formily/react';
const validateJSON = {
validator: `{{(value, rule)=> {
if (!value) {
return '';
}
try {
const val = JSON.parse(value);
if(!isNaN(val)) {
return false;
}
return true;
} catch(error) {
console.error(error);
return false;
}
}}}`,
message: '{{t("Invalid JSON format")}}',
};
export const requestSettingsSchema: ISchema = {
type: 'object',
properties: {
url: {
type: 'string',
title: '{{t("Request URL")}}',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
method: {
type: 'string',
title: '{{t("Request method")}}',
'x-decorator': 'FormItem',
'x-component': 'Select',
default: 'POST',
enum: [
{ label: 'POST', value: 'POST' },
{ label: 'GET', value: 'GET' },
{ label: 'PUT', value: 'PUT' },
{ label: 'PATCH', value: 'PATCH' },
{ label: 'DELETE', value: 'DELETE' },
],
},
headers: {
type: 'string',
title: '{{t("Request headers")}}',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-validator': validateJSON,
},
params: {
type: 'string',
title: '{{t("Request query parameters")}}',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-validator': validateJSON,
},
data: {
type: 'string',
title: '{{t("Request body")}}',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-validator': validateJSON,
},
},
};

View File

@ -9,7 +9,7 @@ import {
SchemaInitializerButtonProps,
SchemaInitializerItemComponent,
SchemaInitializerItemOptions,
SchemaInitializerItemProps
SchemaInitializerItemProps,
} from './types';
const defaultWrap = (s: ISchema) => s;
@ -129,7 +129,7 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
...style,
}}
{...others}
icon={<Icon type={icon as string}/>}
icon={<Icon type={icon as string} />}
>
{compile(props.children || props.title)}
</Button>

View File

@ -101,6 +101,28 @@ export const FormActionInitializers = {
},
},
},
{
type: 'item',
title: '{{t("Custom request")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Custom request") }}',
'x-component': 'Action',
'x-action': 'customize:form:request',
'x-designer': 'Action.Designer',
'x-action-settings': {
requestSettings: {},
onSuccess: {
manualClose: false,
redirecting: false,
successMessage: '{{t("Request success")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeRequestActionProps }}',
},
},
},
],
},
],
@ -208,6 +230,28 @@ export const CreateFormActionInitializers = {
},
},
},
{
type: 'item',
title: '{{t("Custom request")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Custom request") }}',
'x-component': 'Action',
'x-action': 'customize:form:request',
'x-designer': 'Action.Designer',
'x-action-settings': {
requestSettings: {},
onSuccess: {
manualClose: false,
redirecting: false,
successMessage: '{{t("Request success")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeRequestActionProps }}',
},
},
},
],
},
],
@ -315,6 +359,28 @@ export const UpdateFormActionInitializers = {
},
},
},
{
type: 'item',
title: '{{t("Custom request")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Custom request") }}',
'x-component': 'Action',
'x-action': 'customize:form:request',
'x-designer': 'Action.Designer',
'x-action-settings': {
requestSettings: {},
onSuccess: {
manualClose: false,
redirecting: false,
successMessage: '{{t("Request success")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeRequestActionProps }}',
},
},
},
],
},
],

View File

@ -113,6 +113,28 @@ export const ReadPrettyFormActionInitializers = {
},
},
},
{
type: 'item',
title: '{{t("Custom request")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Custom request") }}',
'x-component': 'Action',
'x-action': 'customize:form:request',
'x-designer': 'Action.Designer',
'x-action-settings': {
requestSettings: {},
onSuccess: {
manualClose: false,
redirecting: false,
successMessage: '{{t("Request success")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeRequestActionProps }}',
},
},
},
],
},
],

View File

@ -149,6 +149,28 @@ export const TableActionColumnInitializers = (props: any) => {
},
},
},
{
type: 'item',
title: '{{t("Custom request")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Custom request") }}',
'x-component': 'Action.Link',
'x-action': 'customize:table:request',
'x-designer': 'Action.Designer',
'x-action-settings': {
requestSettings: {},
onSuccess: {
manualClose: false,
redirecting: false,
successMessage: '{{t("Request success")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeRequestActionProps }}',
},
},
},
],
},
]}

View File

@ -7,6 +7,7 @@ import { Alert, Button, Dropdown, Menu, MenuItemProps, Modal, Select, Space, Swi
import classNames from 'classnames';
import { cloneDeep } from 'lodash';
import React, { createContext, useContext, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import {
ActionContext,
@ -19,7 +20,7 @@ import {
useActionContext,
useAPIClient,
useCollection,
useCompile
useCompile,
} from '..';
import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
@ -466,7 +467,7 @@ SchemaSettings.PopupItem = (props) => {
};
SchemaSettings.ActionModalItem = React.memo((props: any) => {
const { title, onSubmit, initialValues, initialSchema, modalTip, ...others } = props;
const { title, onSubmit, initialValues, initialSchema, schema, modalTip, components, ...others } = props;
const [visible, setVisible] = useState(false);
const [schemaUid, setSchemaUid] = useState<string>(props.uid);
const { t } = useTranslation();
@ -487,13 +488,14 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
setVisible(false);
};
const submitHandler = () => {
const submitHandler = async () => {
await form.submit();
onSubmit?.(cloneDeep(form.values));
setVisible(false);
};
const openAssignedFieldValueHandler = async () => {
if (!schemaUid && initialSchema['x-uid']) {
if (!schemaUid && initialSchema?.['x-uid']) {
fieldSchema['x-action-settings'].schemaUid = initialSchema['x-uid'];
dn.emit('patch', { schema: fieldSchema });
await api.resource('uiSchemas').insert({ values: initialSchema });
@ -509,31 +511,40 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
<SchemaSettings.Item {...others} onClick={openAssignedFieldValueHandler}>
{props.children || props.title}
</SchemaSettings.Item>
<Modal
width={'50%'}
title={compile(title)}
{...others}
destroyOnClose
visible={visible}
onCancel={cancelHandler}
footer={
<Space>
<Button onClick={cancelHandler}>{t('Cancel')}</Button>
<Button type="primary" onClick={submitHandler}>
{t('Submit')}
</Button>
</Space>
}
>
<FormProvider form={form}>
<FormLayout layout={'vertical'}>
{modalTip && <Alert message={modalTip} />}
{modalTip && <br />}
{visible && <RemoteSchemaComponent noForm uid={schemaUid} />}
</FormLayout>
</FormProvider>
</Modal>
{createPortal(
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<Modal
width={'50%'}
title={compile(title)}
{...others}
destroyOnClose
visible={visible}
onCancel={cancelHandler}
footer={
<Space>
<Button onClick={cancelHandler}>{t('Cancel')}</Button>
<Button type="primary" onClick={submitHandler}>
{t('Submit')}
</Button>
</Space>
}
>
<FormProvider form={form}>
<FormLayout layout={'vertical'}>
{modalTip && <Alert message={modalTip} />}
{modalTip && <br />}
{visible && schemaUid && <RemoteSchemaComponent noForm components={components} uid={schemaUid} />}
{visible && schema && <SchemaComponent components={components} schema={schema} />}
</FormLayout>
</FormProvider>
</Modal>
</div>,
document.body,
)}
</>
);
});