feat(数据表): REST API (#1567)

Reviewed-on: daoyoucloud/tachybase#1567
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: bai.zixv <bai.zixv@foxmail.com>
Co-committed-by: bai.zixv <bai.zixv@foxmail.com>
This commit is contained in:
bai.zixv 2024-10-01 21:45:10 +08:00 committed by sealday
parent 4d619cffe5
commit 71caef1b2b
93 changed files with 4179 additions and 47 deletions

View File

@ -10,3 +10,4 @@ export * from './types';
export * from './data-source-with-database'; export * from './data-source-with-database';
export * from './utils'; export * from './utils';
export * from './collection';

View File

@ -17,11 +17,15 @@ import {
SyncFieldsAction, SyncFieldsAction,
SyncFieldsActionCom, SyncFieldsActionCom,
SyncSQLFieldsAction, SyncSQLFieldsAction,
usePlugin,
ViewCollectionField, ViewCollectionField,
ViewFieldAction, ViewFieldAction,
} from '@tachybase/client'; } from '@tachybase/client';
import { ISchema, uid } from '@tachybase/schema'; import { ISchema, uid } from '@tachybase/schema';
import { useLocation } from 'react-router-dom';
import PluginDatabaseConnectionsClient from '../../';
import { ConfigurationTable } from './ConfigurationTable'; import { ConfigurationTable } from './ConfigurationTable';
import { ConfigurationTabs } from './ConfigurationTabs'; import { ConfigurationTabs } from './ConfigurationTabs';
@ -35,6 +39,11 @@ const schema2: ISchema = {
}; };
export const CollectionManagerPage = () => { export const CollectionManagerPage = () => {
const plugin = usePlugin(PluginDatabaseConnectionsClient);
const location = useLocation();
const dataSourceType = new URLSearchParams(location.search).get('type');
const type = dataSourceType && plugin.types.get(dataSourceType);
return ( return (
<SchemaComponent <SchemaComponent
schema={schema2} schema={schema2}
@ -44,11 +53,11 @@ export const CollectionManagerPage = () => {
ConfigurationTabs, ConfigurationTabs,
AddFieldAction, AddFieldAction,
AddCollectionField, AddCollectionField,
AddCollection, AddCollection: type?.AddCollection || AddCollection,
AddCollectionAction, AddCollectionAction,
EditCollection, EditCollection: type?.EditCollection || EditCollection,
EditCollectionAction, EditCollectionAction,
DeleteCollection, DeleteCollection: type?.DeleteCollection || DeleteCollection,
DeleteCollectionAction, DeleteCollectionAction,
EditFieldAction, EditFieldAction,
EditCollectionField, EditCollectionField,
@ -60,6 +69,12 @@ export const CollectionManagerPage = () => {
SyncFieldsActionCom, SyncFieldsActionCom,
SyncSQLFieldsAction, SyncSQLFieldsAction,
}} }}
scope={{
allowCollectionDeletion: !!type?.allowCollectionDeletion,
disabledConfigureFields: type?.disabledConfigureFields,
disableAddFields: type?.disableAddFields,
allowCollectionCreate: !!type?.allowCollectionCreate,
}}
/> />
); );
}; };

View File

@ -168,7 +168,7 @@ export const collectionTableSchema: ISchema = {
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
}, },
'x-visible': false, 'x-visible': '{{allowCollectionCreate}}',
}, },
}, },
}, },

View File

@ -30,7 +30,7 @@ export const ViewDatabaseConnectionAction = () => {
style={{ padding: '0px' }} style={{ padding: '0px' }}
disabled={!record.enabled} disabled={!record.enabled}
onClick={() => { onClick={() => {
navigate(getConnectionCollectionPath(record.key)); navigate(getConnectionCollectionPath(record));
}} }}
role="button" role="button"
aria-label={`${record?.key}-Configure`} aria-label={`${record?.key}-Configure`}

View File

@ -1,2 +1,2 @@
export const getConnectionCollectionPath = (name: string | number) => export const getConnectionCollectionPath = ({ key, type }: { key: string | number; type: string }) =>
`/admin/settings/data-source-manager/${name}/collections`; `/admin/settings/data-source-manager/${key}/collections?type=${type}`;

View File

@ -3,11 +3,13 @@
"version": "0.22.5", "version": "0.22.5",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.4.0",
"antd": "5.19.4", "antd": "5.19.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mysql2": "^3.9.1", "mysql2": "^3.9.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"react-i18next": "^14.1.2" "react-i18next": "^14.1.2",
"react-router-dom": "^6.25.1"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.17.5" "@types/lodash": "^4.17.5"

View File

@ -0,0 +1,9 @@
import React from 'react';
import { useRecord } from '@tachybase/client';
import { AddCollectionAction } from './AddCollectionAction.component';
export const AddCollection = (props) => {
const record = useRecord();
return <AddCollectionAction item={record} {...props} />;
};

View File

@ -0,0 +1,26 @@
import React, { useState } from 'react';
import { ActionContextProvider, RecordProvider } from '@tachybase/client';
import { PlusOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { useTranslation } from '../../../locale';
import { ViewCreateCollection } from './CreateCollection.view';
export const AddCollectionAction = (props) => {
const { t } = useTranslation(true);
const { scope, getContainer, item } = props;
const [visible, setVisible] = useState(false);
const handleClick = () => setVisible(true);
return (
<RecordProvider record={item}>
<ActionContextProvider value={{ visible, setVisible }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleClick}>
{t('Create collection')}
</Button>
<ViewCreateCollection scope={scope} getContainer={getContainer} item={item} />
</ActionContextProvider>
</RecordProvider>
);
};

View File

@ -0,0 +1,139 @@
import { useRequest } from '@tachybase/client';
import { uid } from '@tachybase/schema';
import lodash from 'lodash';
import { NAMESPACE, tval } from '../../../locale';
import { PreviewComponent } from './PreviewComponent';
import { PreviewFields } from './PreviewFields';
import { getSchemaRequestAction } from './getSchemaRequestAction';
export function getSchemaCollection(title, useAction, item: Record<string, any> = {}) {
const cloneItem = lodash.cloneDeep(item);
const data: Record<string, any> = {
name: `t_${uid()}`,
...cloneItem,
};
if (data.reverseField) {
data.reverseField.name = `f_${uid()}`;
}
return {
type: 'object',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Drawer',
'x-component-props': {
getContainer: '{{ getContainer }}',
},
'x-decorator': 'Form',
'x-decorator-props': {
useValues(val) {
return useRequest(() => Promise.resolve({ data }), val);
},
},
title,
properties: {
title: {
type: 'string',
title: '{{ t("Collection display name") }}',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
name: {
type: 'string',
title: '{{t("Collection name")}}',
required: true,
'x-disabled': '{{ !createOnly }}',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'uid',
description:
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
},
description: {
title: '{{t("Description")}}',
type: 'string',
name: 'description',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
actions: {
type: 'void',
title: tval('Request actions'),
'x-decorator': 'FormItem',
'x-component': 'FormCollapse',
'x-component-props': {
size: 'small',
accordion: true,
defaultActiveKey: [],
},
properties: {
list: getSchemaRequestAction('list', `{{t("List",{ ns: "${NAMESPACE}" })}}`),
get: getSchemaRequestAction('get', `{{t("Get",{ ns: "${NAMESPACE}" })}}`),
create: getSchemaRequestAction('create', `{{t("Create",{ ns: "${NAMESPACE}" })}}`),
update: getSchemaRequestAction('update', `{{t("Update",{ ns: "${NAMESPACE}" })}}`),
destroy: getSchemaRequestAction('destroy', `{{t("Destroy",{ ns: "${NAMESPACE}" })}}`),
},
},
fields: {
type: 'array',
required: true,
'x-component': PreviewFields,
'x-decorator': 'FormItem',
title: tval('Fields', true),
},
filterTargetKey: {
title: tval('Record unique key'),
required: true,
type: 'single',
description: tval(
'If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.',
),
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
},
preview: {
type: 'void',
'x-visible': '{{ createOnly }}',
'x-component': PreviewComponent,
'x-reactions': {
dependencies: ['fields'],
fulfill: {
schema: {
'x-component-props': '{{$form.values}}',
},
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
action1: {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
action2: {
title: '{{ t("Submit") }}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction,
},
},
},
},
},
},
},
};
}

View File

@ -0,0 +1,37 @@
import React from 'react';
import { FormItem, SchemaComponent, TemplateSummary, useCancelAction } from '@tachybase/client';
import { ArrayItems, ArrayTable, FormCollapse, FormLayout } from '@tachybase/components';
import { tval } from '../../../locale';
import { getSchemaCollection } from './CreateCollection.schema';
import { useActionCreateCollection } from './useActionCreateCollection';
export const ViewCreateCollection = (props) => {
const { scope, getContainer, item } = props;
const title = tval('Create collection', true);
const schema = getSchemaCollection(title, useActionCreateCollection);
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
// NOTE: 依赖的组件内部命名有误, 在这里进行命名映射
TemplateSummay: TemplateSummary,
FormCollapse,
ArrayItems,
FormLayout,
FormItem,
}}
scope={{
createOnly: true,
record: item,
showReverseFieldConfig: true,
presetFieldsDisabled: true,
getContainer,
useCancelAction,
...scope,
}}
/>
);
};

View File

@ -0,0 +1,105 @@
import React, { useEffect, useState } from 'react';
import { EllipsisWithTooltip, useCollectionManager_deprecated, useCompile } from '@tachybase/client';
import { RecursionField, uid, useForm } from '@tachybase/schema';
import { Table } from 'antd';
import { useTranslation } from '../../../locale';
export const PreviewComponent = (props) => {
const form = useForm();
const compile = useCompile();
const { t } = useTranslation();
const { fields, preview } = props;
const [columns, setColumns] = useState([]);
const [dataSource, setDataSource] = useState(preview);
const { getInterface } = useCollectionManager_deprecated();
const filterAndMapSchemas = (schemas) => {
// 过滤出有 source 或 interface 属性的项
const filteredSchemas = schemas.filter((schema) => schema.source || schema.interface);
// 如果没有过滤出的项,则返回 undefined
if (!filteredSchemas) {
return undefined;
}
// 映射过滤出的项,创建新的表格列配置数组
const columnsConfig = filteredSchemas.map((schema) => {
const schemaTitle = schema?.uiSchema?.title || schema.name;
const defaultUiSchema = getInterface(schema.interface)?.default.uiSchema;
return {
key: schema.name,
title: compile(schemaTitle),
dataIndex: schema.name,
width: 200,
render: (text, record, name) => {
const value = record[schema.name];
const mySchema = {
type: 'object',
properties: {
[schema.name]: {
name: `${schema.name}`,
...defaultUiSchema,
'x-read-pretty': true,
default: schema.interface === 'json' || typeof value == 'object' ? JSON.stringify(value) : value,
},
},
};
return (
<EllipsisWithTooltip ellipsis>
<RecursionField schema={mySchema} name={name} onlyRenderProperties />
</EllipsisWithTooltip>
);
},
};
});
return columnsConfig;
};
useEffect(() => {
setDataSource(preview);
}, [preview, fields]);
useEffect(() => {
setColumns([]);
const schemaColumns = filterAndMapSchemas(fields);
setColumns(schemaColumns);
}, [form.values.fields]);
return (
<div key={uid()} style={{ marginBottom: 'var(--nb-spacing)' }}>
{dataSource?.length > 0 && (
<>
<div
className="ant-formily-item-label"
style={{ marginTop: 'var(--nb-spacing)', display: 'flex', padding: '0 0 8px' }}
>
<div className="ant-formily-item-label-content">
<span>
<label>{t('Preview')}</label>
</span>
</div>
<span className="ant-formily-item-colon">:</span>
</div>
<Table
key="preview"
size="middle"
pagination={false}
bordered
columns={columns}
dataSource={dataSource}
scroll={{
x: 1000,
y: 300,
}}
/>
</>
)}
</div>
);
};

View File

@ -0,0 +1,211 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useCollectionManager_deprecated, useCompile, useFieldInterfaceOptions } from '@tachybase/client';
import { useField, useForm } from '@tachybase/schema';
import { App, Button, Input, Select, Table, Tag } from 'antd';
import lodash from 'lodash';
import jsxRuntime from 'react/jsx-runtime';
import { useTranslation } from '../../../locale';
import { filterTree } from '../utils/filterTree';
export const PreviewFields = (props) => {
const compile = useCompile();
const { getInterface } = useCollectionManager_deprecated();
const { t } = useTranslation();
const form = useForm();
const field: any = useField();
const [dataSource, setDataSource] = useState(field.value);
const fieldInterfaceOptions = useFieldInterfaceOptions().filter(
(options) => !['relation', 'systemInfo'].includes(options.key),
);
const { modal } = App.useApp();
const updateDataSource = (item, index) => {
setTimeout(() => {
dataSource.splice(index, 1, item);
setDataSource(dataSource);
field.value = dataSource.concat();
}, 300);
};
const deleteDataSoureItem = (item) => {
dataSource.splice(item, 1);
const newDataSource = dataSource.concat();
field.value = newDataSource;
setDataSource(newDataSource);
};
const onClick = () => {
modal.confirm({
title: t('Are you sure you want to clear fields?'),
onOk: () => {
form.setValuesIn('fields', []);
form.setValuesIn('preview', []);
},
});
};
const columnsData: any = useMemo(
() => [
{
dataIndex: 'index',
width: 60,
key: 'index',
render: (text, record, index) => index + 1,
},
{
title: t('Field display name'),
dataIndex: 'title',
key: 'title',
width: 150,
render: (text, record, index) => {
const targetDataSource = dataSource[index];
return jsxRuntime.jsx(Input, {
defaultValue: targetDataSource.uiSchema.title,
onChange: lodash.debounce((changeValue) => {
updateDataSource(
{
...targetDataSource,
uiSchema: {
...lodash.omit(targetDataSource?.uiSchema, 'rawTitle'),
title: changeValue.target.value,
},
},
index,
);
}, 300),
});
},
},
{
title: t('Field name'),
dataIndex: 'name',
key: 'name',
width: 130,
},
{
title: t('Field type'),
dataIndex: 'type',
width: 140,
key: 'type',
render: (text, record, index) => {
const targetDataSource = dataSource[index];
return targetDataSource?.source || !targetDataSource?.possibleTypes ? (
<Tag> {text}</Tag>
) : (
<Select
defaultValue={text}
popupMatchSelectWidth={false}
style={{ width: '100%' }}
options={
targetDataSource?.possibleTypes.map((type) => ({
label: type,
value: type,
})) || []
}
onChange={(type) => {
updateDataSource(
{
...targetDataSource,
type,
},
index,
);
}}
/>
);
},
},
{
title: t('Field interface'),
dataIndex: 'interface',
key: 'interface',
width: 150,
render: (text, record, index) => {
const targetItem = dataSource[index];
const treeChild = targetItem?.type && filterTree(fieldInterfaceOptions, targetItem?.type);
const handleSelectChange = (interfaceVal) => {
const fieldInterface = getInterface(interfaceVal);
updateDataSource(
{
...targetItem,
interface: interfaceVal,
uiSchema: {
...targetItem.uiSchema,
...fieldInterface?.default?.uiSchema,
},
},
index,
);
};
return (
<Select
defaultValue={text}
style={{ width: '100%' }}
popupMatchSelectWidth={false}
onChange={handleSelectChange}
>
{treeChild?.map((child) => (
<Select.OptGroup key={child.key} label={compile(child.label)}>
{child.children.map(({ label, value, name }) => (
<Select.Option key={value} value={name}>
{compile(label)}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
);
},
},
{
title: t('Actions'),
dataIndex: 'actions',
fixed: 'right',
key: 'actions',
align: 'center',
width: 100,
render: (text, record, index) => <a onClick={() => deleteDataSoureItem(index)}> {t('Delete')}</a>,
},
],
[dataSource],
);
useEffect(() => {
setDataSource(field.value);
}, [field.value]);
return (
<>
{dataSource.length ? (
<Button
style={{
position: 'absolute',
top: '-10px',
right: '0px',
}}
onClick={onClick}
>
{t('Clear')}
</Button>
) : null}
<Table
bordered
size="middle"
columns={columnsData}
dataSource={dataSource}
scroll={{
y: 300,
}}
pagination={false}
/>
</>
);
};
PreviewFields.displayName = 'PreviewFields';

View File

@ -0,0 +1,29 @@
import React from 'react';
import { createForm, useForm } from '@tachybase/schema';
import { ProviderContextRequestInfo } from '../contexts/RequestForm.context';
export const ProviderRequestActionItems = (props) => {
const form = useForm();
const { actionKey } = props;
const requestActionForm = React.useMemo(() => createForm({}), []);
const [responseTransformer, setResponseTransformer] = React.useState(null);
return (
<ProviderContextRequestInfo
value={{
form: {
...form,
[actionKey]: requestActionForm,
},
actionKey,
requestActionForm,
responseTransformer,
setResponseTransformer,
}}
>
{props.children}
</ProviderContextRequestInfo>
);
};

View File

@ -0,0 +1,107 @@
import React from 'react';
import { css, useRequest } from '@tachybase/client';
import { onFieldChange, uid, useForm } from '@tachybase/schema';
import { unflatten } from '@tachybase/utils/client';
import lodash from 'lodash';
import { NAMESPACE } from '../../../locale';
import { DebugComponent } from '../request-configs/debug-area/DebugComponent';
import { MethodPathComponent } from '../request-configs/method-path/MethodPathComponent';
import { RequestTab } from '../request-configs/request-tab/RequestTab';
import { ResponseTransformerComponent } from '../request-configs/request-transformer/ResponseTransformerComponent';
import { ProviderRequestActionItems } from './RequestActionItems.provider';
export const getSchemaRequestAction = (key, header) => {
const ref: any = React.useRef();
return {
type: 'void',
'x-decorator': ProviderRequestActionItems,
'x-decorator-props': {
actionKey: key,
},
'x-component': 'FormCollapse.CollapsePanel',
'x-component-props': {
header,
key,
},
properties: {
form: {
'x-decorator': 'Form',
'x-decorator-props': {
className: css`
.ant-formily-item-feedback-layout-loose {
margin-bottom: 10px;
}
`,
useValues(val) {
ref.current = useForm();
return useRequest(() => Promise.resolve(), val);
},
effects: () => {
const updateForm = (targetField) => {
const form = ref.current;
const { actions } = form.values || {};
const { path, value } = targetField.getState();
path.includes('add') ||
form.setValuesIn('actions', {
...actions,
[key]: unflatten({
...actions?.[key],
[path]: value,
}),
});
};
const updateFunc = lodash.debounce(async (field, form) => {
if (form.modified) {
await updateForm(field);
}
}, 400);
onFieldChange('*', (field, form) => {
if (form.modified) {
updateFunc(field, form);
}
});
},
},
type: 'void',
properties: {
[uid()]: {
type: 'void',
properties: {
requestAction: {
type: 'string',
'x-component': MethodPathComponent,
},
requestTab: {
type: 'void',
'x-decorator': 'FormItem',
title: `{{t("Adapt request parameters",{ ns: "${NAMESPACE}" })}}`,
'x-component': RequestTab,
'x-decorator-props': {
tooltip: `{{t("Provide request variables from TachyBase for use by third-party APIs.",{ ns: "${NAMESPACE}" })}}`,
},
},
responseTransformer: {
type: 'string',
'x-decorator': 'FormItem',
title: `{{t("Convert third-party response results to NocoBase standard",{ ns: "${NAMESPACE}" })}}`,
'x-component': ResponseTransformerComponent,
'x-decorator-props': {
tooltip: `{{t("The response results from third-party APIs need to be converted to the NocoBase standard to display correctly on the frontend.",{ ns: "${NAMESPACE}" })}}`,
},
},
debug: {
type: 'void',
'x-component': DebugComponent,
},
},
},
},
},
},
};
};

View File

@ -0,0 +1,78 @@
import {
useActionContext,
useAPIClient,
useCollectionManager_deprecated,
useDataSourceManager,
useResourceActionContext,
} from '@tachybase/client';
import { useField, useForm } from '@tachybase/schema';
import lodash from 'lodash';
import { useParams } from 'react-router-dom';
import { useTranslation } from '../../../locale';
import { filterObjectWithMethodAndPath } from '../utils/filterObjectWithMethodAndPath';
import { getRequestActions } from '../utils/getRequestActions';
export function useActionCreateCollection() {
const form = useForm();
const { refreshCM: refreshCM } = useCollectionManager_deprecated();
const ctx = useActionContext();
const { refresh: refresh } = useResourceActionContext();
const apiClient = useAPIClient();
const { name: name } = useParams();
const targetCollectionRepo = apiClient.resource('dataSources.collections', name);
const field = useField();
const dm = useDataSourceManager();
useTranslation();
return {
async run() {
field.data = field.data || {};
field.data.loading = true;
try {
await form.submit();
const cloneFormValues = lodash.cloneDeep(form.values);
const requestActionsForm = filterObjectWithMethodAndPath(cloneFormValues?.actions);
const requestActionsRestMethod = getRequestActions(Object.keys(requestActionsForm));
const pickedFormValues = lodash.pick(cloneFormValues, ['fields', 'name', 'title', 'filterTargetKey']);
const targetMethod = requestActionsRestMethod[0];
if (targetMethod) {
form.query(`*.actions.${targetMethod}`).take((field) => {
field.setComponentProps({
style: { border: '1px solid #ff4d4f' },
});
});
await form[targetMethod].submit();
} else {
await targetCollectionRepo.create({
values: {
logging: true,
...pickedFormValues,
actions: requestActionsForm,
},
});
ctx.setVisible(false);
await form.reset();
field.data.loading = false;
refresh();
await refreshCM();
dm.getDataSource(name).reload();
}
} catch (error) {
console.error(error);
field.data.loading = false;
}
},
};
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import { useRecord } from '@tachybase/client';
import { DeleteCollectionAction } from './DeleteCollectionAction.component';
export const DeleteCollection = (props) => {
const item = useRecord();
return <DeleteCollectionAction item={item} {...props} />;
};

View File

@ -0,0 +1,41 @@
import React, { useState } from 'react';
import { ActionContextProvider, RecordProvider } from '@tachybase/client';
import { DeleteOutlined } from '@ant-design/icons';
import { App, Button } from 'antd';
import _ from 'lodash';
import { useTranslation } from '../../../locale';
import { useBulkDestroyActionAndRefreshCM } from './useBulkDestroyActionAndRefreshCM';
import { useDestroyActionAndRefreshCM } from './useDestroyActionAndRefreshCM';
export const DeleteCollectionAction = (props) => {
const { t } = useTranslation();
const { modal } = App.useApp();
const { item, isBulk, children, ...restProps } = props;
const targetProps = _.omit(restProps, ['scope', 'getContainer', 'useAction']);
const [visible, setVisible] = useState(false);
const { run } = isBulk ? useBulkDestroyActionAndRefreshCM() : useDestroyActionAndRefreshCM();
const onClick = () => {
modal.confirm({
title: t('Delete collection'),
content: t('Are you sure you want to delete it?'),
onOk: run,
});
};
return (
<RecordProvider record={item}>
<ActionContextProvider value={{ visible, setVisible }}>
{isBulk ? (
<Button icon={<DeleteOutlined />} onClick={onClick} children={children || t('Delete')} />
) : (
<a onClick={onClick} {...targetProps}>
{children || t('Delete')}
</a>
)}
</ActionContextProvider>
</RecordProvider>
);
};
DeleteCollectionAction.displayName = 'DeleteCollectionAction';

View File

@ -0,0 +1,52 @@
import {
useActionContext,
useAPIClient,
useCollectionManager_deprecated,
useResourceActionContext,
} from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import { message } from 'antd';
import _ from 'lodash';
import { useParams } from 'react-router-dom';
import { useTranslation } from '../../../locale';
export function useBulkDestroyActionAndRefreshCM() {
const { run: runFunc } = useBulkDestroyAction();
const { refreshCM } = useCollectionManager_deprecated();
return {
async run() {
await runFunc();
await refreshCM();
},
};
}
function useBulkDestroyAction() {
const apiClient = useAPIClient();
const { t } = useTranslation();
const form = useForm();
const ctx = useActionContext();
const { name } = useParams();
const { state, setState, refresh } = useResourceActionContext();
return {
async run() {
if (!state?.selectedRowKeys || state.selectedRowKeys.length === 0) {
return message.error(t('Please select the records you want to delete'));
}
// 调用 API 客户端删除选中的记录
await apiClient.resource('dataSources.collections', name).destroy({ filterByTk: state?.selectedRowKeys || [] });
// 重置表单
form.reset();
// 如果存在 setVisible 方法,则调用它关闭对话框
ctx?.setVisible?.(false);
// 重置选中的行键状态
setState?.({ selectedRowKeys: [] });
refresh();
},
};
}

View File

@ -0,0 +1,31 @@
import { useCollectionManager_deprecated, useAPIClient, useActionContext, useResourceContext, useRecord, useResourceActionContext } from "@tachybase/client";
import { useParams } from "react-router-dom";
export function useDestroyActionAndRefreshCM() {
const { run: runFunc } = useDestroyAction();
const { refreshCM } = useCollectionManager_deprecated();
return {
async run() {
await runFunc();
await refreshCM();
},
};
}
function useDestroyAction() {
const apiClient = useAPIClient();
const ctx = useActionContext();
const { targetKey } = useResourceContext();
const { [targetKey]: filterByTk } = useRecord();
const { name } = useParams();
const { refresh } = useResourceActionContext();
return {
async run() {
await apiClient.resource('dataSources.collections', name).destroy({ filterByTk });
ctx?.setVisible?.(false);
refresh();
},
};
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import { useRecord } from '@tachybase/client';
import { EditCollectionAction } from './EditCollectionAction.component';
export const EditCollection = (props) => {
const record = useRecord();
return <EditCollectionAction item={record} {...props} />;
};

View File

@ -0,0 +1,20 @@
import React, { useState } from 'react';
import { ActionContextProvider, RecordProvider } from '@tachybase/client';
import { useTranslation } from '../../../locale';
import { ViewEditCollectionForm } from './EditCollectionForm.view';
export const EditCollectionAction = (props) => {
const { t } = useTranslation();
const { scope, getContainer, item } = props;
const [visible, setVisible] = useState(false);
return (
<RecordProvider record={item}>
<ActionContextProvider value={{ visible, setVisible }}>
<a onClick={() => setVisible(true)}>{t('Edit')}</a>
<ViewEditCollectionForm scope={scope} item={item} getContainer={getContainer} />
</ActionContextProvider>
</RecordProvider>
);
};

View File

@ -0,0 +1,34 @@
import React from 'react';
import { FormItem, SchemaComponent, TemplateSummary, useCancelAction } from '@tachybase/client';
import { ArrayItems, ArrayTable, FormCollapse, FormLayout } from '@tachybase/components';
import { getSchemaCollection } from '../collection-add/CreateCollection.schema';
import { useActionEditCollection } from './useActionEditCollection';
export const ViewEditCollectionForm = (props) => {
const { scope, getContainer, item } = props;
const schema = getSchemaCollection('{{ t("Edit collection") }}', useActionEditCollection, item);
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
TemplateSummay: TemplateSummary,
FormCollapse,
ArrayItems,
FormLayout,
FormItem,
}}
scope={{
createOnly: true,
record: item,
showReverseFieldConfig: true,
presetFieldsDisabled: true,
getContainer,
useCancelAction: useCancelAction,
...scope,
}}
/>
);
};

View File

@ -0,0 +1,77 @@
import {
useActionContext,
useAPIClient,
useCollectionManager_deprecated,
useDataSourceManager,
useRecord,
useResourceActionContext,
useResourceContext,
} from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import lodash from 'lodash';
import { useParams } from 'react-router-dom';
import { filterObjectWithMethodAndPath } from '../utils/filterObjectWithMethodAndPath';
import { getRequestActions } from '../utils/getRequestActions';
export const useActionEditCollection = (e) => {
const apiClient = useAPIClient();
const dm = useDataSourceManager();
const { refreshCM } = useCollectionManager_deprecated();
const { refresh } = useResourceActionContext();
const form = useForm();
const ctx = useActionContext();
const { targetKey } = useResourceContext();
const { name } = useParams();
const { [targetKey]: keyValue } = useRecord();
const targetRepo = apiClient.resource('dataSources.collections', name);
return {
async run() {
await form.submit();
const cloneValue = lodash.cloneDeep(form.values);
const actionValue = filterObjectWithMethodAndPath(cloneValue?.actions);
const requestActionsRestMethod = getRequestActions(Object.keys(actionValue));
const pickedFormValues = lodash.pick(form.values, ['fields', 'name', 'title', 'filterTargetKey']);
const targetMethod = requestActionsRestMethod[0];
if (targetMethod) {
form.query(`*.actions.${targetMethod}`).take((formVal) => {
formVal.setComponentProps({
style: {
border: '1px solid #ff4d4f',
},
});
});
await form[targetMethod].submit();
} else {
await targetRepo.update({
filterByTk: keyValue,
values: {
...pickedFormValues,
actions: actionValue,
},
});
ctx.setVisible(false);
const dataSource = dm.getDataSource(name);
if (!dataSource?.reload) {
dataSource.reload.call(dataSource);
}
await form.reset();
refresh();
await refreshCM();
}
},
};
};

View File

@ -0,0 +1,7 @@
export const paramsMap = {
list: ['page', 'pageSize', 'filter', 'sort', 'appends', 'fields', 'except'],
get: ['filterByTk', 'filter', 'sort', 'appends', 'fields', 'except'],
create: ['whiteList', 'blacklist', 'body'],
update: ['filterByTk', 'filter', 'whiteList', 'blacklist', 'body'],
destroy: ['filterByTk', 'filter'],
};

View File

@ -0,0 +1,26 @@
export const requestHeaderList = [
{
name: 'X-App',
title: 'X-App',
},
{
name: 'X-Locale',
title: 'X-Locale',
},
{
name: 'X-Hostname',
title: 'X-Hostname',
},
{
name: 'X-Timezone',
title: 'X-Timezone',
},
{
name: 'X-Role',
title: 'X-Role',
},
{
name: 'X-Authenticator',
title: 'X-Authenticator',
},
];

View File

@ -0,0 +1,19 @@
import React from 'react';
// NOTE: 请求信息参数
interface IContextRequestInfo {
actionKey?: string;
form?: any;
requestActionForm?: any;
responseTransformer?: any;
setResponseTransformer?: any;
}
// TODO: 删除全部导出, 改为 provider 和 context 形式
export const ContextRequestInfo: any = React.createContext<IContextRequestInfo>({});
export const ProviderContextRequestInfo = ContextRequestInfo.Provider;
export function useContextRequestInfo(): IContextRequestInfo {
return React.useContext(ContextRequestInfo);
}

View File

@ -0,0 +1,18 @@
import React from 'react';
interface IContextResponseInfo {
rawResponse?: any;
debugResponse?: any;
responseValidationErrorMessage?: any;
setRawResponse?: any;
setDebugResponse?: any;
setResponseValidationErrorMessage?: any;
}
const ContextResponseInfo = React.createContext<IContextResponseInfo>({});
export const ProviderContextResponseInfo = ContextResponseInfo.Provider;
export function useContextResponseInfo(): IContextResponseInfo {
return React.useContext(ContextResponseInfo);
}

View File

@ -0,0 +1,188 @@
import { css } from '@tachybase/client';
import { NAMESPACE } from '../../../locale';
export const schemaDataSourceSettingsForm = {
type: 'object',
properties: {
displayName: {
type: 'string',
title: `{{t("Data source display name",{ ns: "${NAMESPACE}" })}}`,
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
key: {
type: 'string',
title: `{{t("Data source name",{ ns: "${NAMESPACE}" })}}`,
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'uid',
'x-disabled': '{{ createOnly }}',
description: `{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.',{ ns: "${NAMESPACE}" })}}`,
},
options: {
type: 'object',
properties: {
baseUrl: {
type: 'string',
title: `{{t("BaseURL",{ ns: "${NAMESPACE}" })}}`,
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input.URL',
'x-validator': 'url',
},
headers: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: `{{t("Headers", { ns: "${NAMESPACE}" })}}`,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: { flexWrap: 'nowrap', maxWidth: '100%', display: 'flex' },
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
& > .ant-space-item:first-child,
& > .ant-space-item:nth-of-type(2) {
flex: 1;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Name")}}',
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
useTypedConstant: true,
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: `{{t("Add request header", { ns: "${NAMESPACE}" })}}`,
'x-component': 'ArrayItems.Addition',
},
},
},
variables: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: `{{t("Variables", { ns: "${NAMESPACE}" })}}`,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
display: 'flex',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
& > .ant-space-item:first-child,
& > .ant-space-item:nth-of-type(2) {
flex: 1;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': { placeholder: '{{t("Name")}}' },
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': { useTypedConstant: true },
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: `{{t("Add variable", { ns: "${NAMESPACE}" })}}`,
'x-component': 'ArrayItems.Addition',
},
},
},
timeout: {
type: 'string',
title: `{{t("Timeout",{ ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {
addonAfter: 'ms',
style: {
minWidth: 200,
},
},
default: 5e3,
},
responseType: {
type: 'string',
title: `{{t("Response type",{ ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Select',
default: 'json',
enum: [
{
value: 'json',
label: `{{t("JSON",{ ns: "${NAMESPACE}" })}}`,
},
],
},
},
},
enabled: {
type: 'string',
'x-content': `{{t("Enabled the data source",{ ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
default: true,
},
},
};

View File

@ -0,0 +1,10 @@
import React from 'react';
import { SchemaComponent, Space } from '@tachybase/client';
import { useTranslation } from '../../../locale';
import { schemaDataSourceSettingsForm as schema } from './DataSourceSettingsForm.schema';
export const DataSourceSettingsForm = () => {
const { t } = useTranslation();
return <SchemaComponent schema={schema} components={{ Space }} scope={{ t }} />;
};

View File

@ -0,0 +1,26 @@
import { Plugin } from '@tachybase/client';
import PluginDataSourceManagerClient from '@tachybase/plugin-data-source-manager/client';
import { tval } from '../../locale';
import { AddCollection } from './collection-add/AddCollection';
import { DeleteCollection } from './collection-delete/DeleteCollection';
import { EditCollection } from './collection-edit/EditCollection';
import { DataSourceSettingsForm } from './form-data-source/DataSourceSettingsForm';
export class KitHttpDatasource extends Plugin {
async load() {
this.app.pm.get(PluginDataSourceManagerClient).registerType('http', {
label: tval('REST API'),
allowCollectionCreate: true,
allowCollectionDeletion: false,
disabledConfigureFields: false,
disableAddFields: true,
disableTestConnection: true,
// 组件
DataSourceSettingsForm,
AddCollection,
EditCollection,
DeleteCollection,
});
}
}

View File

@ -0,0 +1,28 @@
import React, { useState } from 'react';
import { ActionContextProvider } from '@tachybase/client';
import { ProviderContextResponseInfo } from '../../contexts/ResponseInfo.context';
export const ProviderDebug = (props) => {
const { visible, setVisible } = props;
const [rawResponse, setRawResponse] = useState(null);
const [debugResponse, setDebugResponse] = useState(null);
const [responseValidationErrorMessage, setResponseValidationErrorMessage] = useState(null);
return (
<ActionContextProvider visible={visible} setVisible={setVisible}>
<ProviderContextResponseInfo
value={{
rawResponse,
debugResponse,
responseValidationErrorMessage,
setRawResponse,
setDebugResponse,
setResponseValidationErrorMessage,
}}
>
{props.children}
</ProviderContextResponseInfo>
</ActionContextProvider>
);
};

View File

@ -0,0 +1,236 @@
import { css, cx } from '@tachybase/client';
import { onFieldChange } from '@tachybase/schema';
import { unflatten } from '@tachybase/utils/client';
import lodash from 'lodash';
import { useTranslation } from '../../../../locale';
import { AlertError } from './components/AlertError';
import { DebugResponseTabs } from './components/DebugResponseTabs';
import { ResponseTab } from './components/ResponseTab';
import { ExtractFieldMetadata } from './components/ExtractFieldMetadata';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { getSchemaParam } from './schemas/getSchemaParam';
import { MethodPathComponent } from '../method-path/MethodPathComponent';
import { RequestTab } from '../request-tab/RequestTab';
import { ResponseTransformerComponent } from '../request-transformer/ResponseTransformerComponent';
export const getSchemaDebug = () => {
const { t } = useTranslation();
const { actionKey, form, requestActionForm } = useContextRequestInfo();
const schemaChild = getSchemaParam(actionKey, form);
const handleResponseEffects = () => {
const update = async (field) => {
const { actions } = form.values || {};
const { path, value } = field.getState();
if (!path.includes('add')) {
await form.setValuesIn('actions', {
...actions,
[actionKey]: unflatten({
...actions?.[actionKey],
[path]: value,
}),
});
if (['method', 'path'].includes(path)) {
await requestActionForm.setValuesIn([path], value);
}
}
};
const debounceUpdate = lodash.debounce(update, 400);
onFieldChange('*', (field, form) => {
if (form.modified) {
debounceUpdate(field);
}
});
};
return {
type: 'object',
properties: {
modal: {
type: 'void',
'x-decorator': 'Form',
'x-component': 'Action.Modal',
'x-component-props': {
className: cx(
'nb-action-popup',
css`
.ant-modal-content {
padding: 0;
}
.ant-modal-footer {
position: absolute;
bottom: 24px;
right: 24px;
}
`,
),
width: '90%',
styles: {
body: {
padding: 0,
},
},
centered: true,
destroyOnClose: true,
},
properties: {
bodyContainer: {
type: 'void',
'x-component': 'Row',
'x-component-props': {
gutter: 1,
},
properties: {
request: {
type: 'void',
'x-decorator': 'Col',
'x-decorator-props': {
span: 8,
},
'x-component': 'Card',
'x-component-props': {
bordered: false,
title: t('TachyBase request'),
style: {
height: '80vh',
overflowY: 'auto',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
},
},
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelCol: 6,
wrapperCol: 10,
layout: 'vertical',
className: css`
.ant-formily-item-feedback-layout-loose {
margin-bottom: 5px;
}
`,
},
properties: schemaChild,
},
},
},
thirdPartyApi: {
type: 'void',
'x-decorator': 'Col',
'x-decorator-props': {
span: 8,
},
'x-component': 'Card',
'x-component-props': {
title: t('Third party API'),
bordered: false,
style: {
height: '80vh',
overflowY: 'auto',
borderRadius: 0,
},
},
properties: {
responseContainer: {
type: 'void',
'x-decorator': 'Form',
'x-decorator-props': {
effects: handleResponseEffects,
className: css`
.ant-formily-item-feedback-layout-loose {
margin-bottom: 10px;
}
`,
},
properties: {
requestAction: {
type: 'string',
'x-component': MethodPathComponent,
'x-component-props': {
actionForm: true,
},
},
requestTab: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': RequestTab,
},
responseTab: {
type: 'string',
'x-component': ResponseTab,
},
},
},
},
},
nocoBaseResponse: {
type: 'void',
'x-decorator': 'Col',
'x-decorator-props': {
span: 8,
},
'x-component': 'Card',
'x-component-props': {
bordered: false,
title: t('TachyBase response'),
style: {
height: '80vh',
overflowY: 'auto',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
},
},
properties: {
responseContainer: {
type: 'void',
'x-decorator': 'Form',
properties: {
alertError: {
type: 'string',
'x-component': AlertError,
},
responseTransformer: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': ResponseTransformerComponent,
},
debugResponse: {
type: 'string',
'x-component': DebugResponseTabs,
},
},
},
},
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Modal.Footer',
properties: {
debug: {
title: '{{t("Debug")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useDebugAction }}',
},
},
extractFieldMetadata: {
'x-component': ExtractFieldMetadata,
},
},
},
},
},
},
};
};

View File

@ -0,0 +1,42 @@
import React from 'react';
import { Action, FormItem, Input, InputNumber, SchemaComponent, Space, Variable } from '@tachybase/client';
import { Form, FormLayout } from '@tachybase/components';
import { Card, Col, Row, Select } from 'antd';
import { useTranslation } from '../../../../locale';
import { useVariableOptions } from '../../scopes/useVariableOptions';
import { getSchemaDebug } from './Debug.schema';
import { useCancelAction } from './scopes/useCancelAction';
import { useDebugAction } from './scopes/useDebugAction';
export const ViewDebug = () => {
const { t } = useTranslation();
const schema = getSchemaDebug();
return (
<SchemaComponent
schema={schema}
components={{
Form,
Input,
Action,
FormItem,
InputNumber,
Card,
Variable,
Space,
Select,
FormLayout,
Row,
Col,
}}
scope={{
t,
useDebugAction,
useCancelAction,
useVariableOptions,
}}
/>
);
};

View File

@ -0,0 +1,30 @@
import React, { useState } from 'react';
import { Button } from 'antd';
import { useTranslation } from '../../../../locale';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { ProviderDebug } from './Debug.provider';
import { ViewDebug } from './Debug.view';
export const DebugComponent = () => {
const { t } = useTranslation();
const { requestActionForm } = useContextRequestInfo();
const [visible, setVisible] = useState(false);
const handleClick = async () => {
await requestActionForm.submit();
setVisible(true);
};
return (
<>
<Button type="primary" onClick={handleClick}>
{t('Try it out')}
</Button>
<ProviderDebug visible={visible} setVisible={setVisible}>
<ViewDebug />
</ProviderDebug>
</>
);
};

View File

@ -0,0 +1,19 @@
import React from 'react';
import { Alert } from 'antd';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
export const AlertError = () => {
const { responseValidationErrorMessage: errorMessage } = useContextResponseInfo();
if (!errorMessage) {
return null;
}
return (
<>
<Alert type="error" showIcon message={errorMessage} />
<br />
</>
);
};

View File

@ -0,0 +1,33 @@
import { useRequest } from '@tachybase/client';
import { uid } from '@tachybase/schema';
export const getSchemaDebugResponse = (data) => ({
type: 'object',
'x-decorator': 'Form',
'x-decorator-props': {
useValues(params) {
const result = useRequest(
() =>
Promise.resolve({
data: {},
}),
params,
);
return result;
},
},
properties: {
[uid()]: {
type: 'string',
default: data,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
'x-component-props': {
style: {
maxHeight: '350px',
},
},
},
},
});

View File

@ -0,0 +1,21 @@
import React from 'react';
import { FormItem, Input, SchemaComponent } from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getSchemaDebugResponse } from './DebugResponse.schema';
export const ViewDebugResponse = () => {
const { debugResponse } = useContextResponseInfo();
const schema = getSchemaDebugResponse(debugResponse);
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
FormItem,
Input,
}}
/>
);
};

View File

@ -0,0 +1,14 @@
import React from 'react';
import { ViewDebugResponse } from './DebugResponse.view';
export const getItemsDebugResponse = (params) => {
const { t } = params;
return [
{
key: 'body',
label: t('Body'),
children: <ViewDebugResponse />,
},
];
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import { Tabs } from 'antd';
import { useTranslation } from '../../../../../locale';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getItemsDebugResponse } from './DebugResponseTabs.items';
export const DebugResponseTabs = () => {
const { t } = useTranslation();
const { debugResponse } = useContextResponseInfo();
const items = getItemsDebugResponse({ t });
if (!debugResponse) {
return null;
}
return <Tabs defaultActiveKey="body" items={items} />;
};

View File

@ -0,0 +1,98 @@
import React, { useContext, useState } from 'react';
import { ActionContext, useAPIClient } from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import { Button } from 'antd';
import lodash from 'lodash';
import { useParams } from 'react-router-dom';
import { useTranslation } from '../../../../../locale';
import { TooltipContainer } from './TooltipContainer';
import { useContextRequestInfo } from '../../../contexts/RequestForm.context';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
export const ExtractFieldMetadata = () => {
const form = useForm();
const apiClient = useAPIClient();
const { t } = useTranslation();
const { setVisible } = useContext(ActionContext);
const { name } = useParams();
const { rawResponse, responseValidationErrorMessage } = useContextResponseInfo();
const { form: formValue, actionKey } = useContextRequestInfo();
const { fields } = formValue.values;
const [loading, setLoading] = useState(false);
const debugVars = lodash.omit(form.values, 'responseTab');
const handleClick = async () => {
try {
setLoading(true);
const actionValue = formValue?.values?.actions?.[actionKey];
const repo = apiClient.resource('dataSources.httpCollections', name);
const {
data: { data: resData },
} = await repo.runAction({
values: {
debug: false,
inferFields: true,
actionOptions: {
...actionValue,
type: actionKey,
responseTransformer: actionValue?.responseTransformer,
},
debugVars,
},
});
setVisible(false);
setLoading(false);
const { transformedResponse, fields: fieldsList, filterTargetKey } = resData;
if (filterTargetKey) {
formValue.setValuesIn('filterTargetKey', filterTargetKey);
}
if (fieldsList) {
const fieldVal = lodash.unionBy(fields, fieldsList, 'name');
formValue.setValuesIn('fields', fieldVal);
}
if (typeof transformedResponse?.data == 'object') {
let responseData = [];
if (actionKey === 'get') {
responseData = [transformedResponse?.data];
} else {
responseData = transformedResponse?.data;
}
formValue.setValuesIn('preview', responseData);
}
} catch (error) {
setLoading(false);
console.log(error);
}
};
if (!['list', 'get'].includes(actionKey)) {
return null;
}
return (
<TooltipContainer>
<Button
type="primary"
disabled={!rawResponse || responseValidationErrorMessage}
loading={loading}
onClick={handleClick}
>
{t('Extract field metadata')}
</Button>
</TooltipContainer>
);
};

View File

@ -0,0 +1,59 @@
import { NAMESPACE } from '../../../../../locale';
export const getSchemaHeaders = ({ key, defaultValue }) => ({
type: 'object',
properties: {
[key]: {
type: 'array',
'x-decorator': 'FormItem',
'x-component': 'ArrayTable',
default: defaultValue,
'x-component-props': {
scroll: {
y: 300,
},
},
items: {
type: 'object',
properties: {
column1: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
width: 200,
title: `{{t("Name",{ ns: "${NAMESPACE}" })}}`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Name")}}',
},
},
},
},
column2: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
width: 200,
title: `{{t("Value",{ ns: "${NAMESPACE}" })}}`,
},
properties: {
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Value")}}',
},
},
},
},
},
},
},
},
});

View File

@ -0,0 +1,25 @@
import React from 'react';
import { FormItem, Input, SchemaComponent } from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getSchemaHeaders } from './Headers.schema';
export const ViewRequestHeaders = () => {
const { rawResponse } = useContextResponseInfo();
const { request } = rawResponse || {};
const requestHeaderMapList = Object.entries(request?.headers || {}).map(([name, value]) => ({ name, value }));
const schema = getSchemaHeaders({ key: 'requestHeaders', defaultValue: requestHeaderMapList });
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
FormItem,
Input,
}}
/>
);
};

View File

@ -0,0 +1,27 @@
import { useRequest } from '@tachybase/client';
import { uid } from '@tachybase/schema';
export const getSchemaResponseBody = (data) => ({
type: 'object',
'x-decorator': 'Form',
'x-decorator-props': {
useValues(params) {
const result = useRequest(() => Promise.resolve({ data: {} }), params);
return result;
},
},
properties: {
[uid()]: {
type: 'string',
default: data,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
'x-component-props': {
style: {
maxHeight: '400px',
},
},
},
},
});

View File

@ -0,0 +1,23 @@
import React from 'react';
import { FormItem, Input, SchemaComponent } from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getSchemaResponseBody } from './ResponseBody.schema';
export const ViewResponseBody = () => {
const { rawResponse } = useContextResponseInfo();
const { data } = rawResponse || {};
const schema = getSchemaResponseBody(data);
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
FormItem,
Input,
}}
/>
);
};

View File

@ -0,0 +1,23 @@
import React from 'react';
import { FormItem, Input, SchemaComponent } from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getSchemaHeaders } from './Headers.schema';
export const ViewResponseHeaders = () => {
const { rawResponse } = useContextResponseInfo();
const { headers } = rawResponse || {};
const headerMapList = Object.entries(headers || {}).map(([name, value]) => ({ name, value }));
const schema = getSchemaHeaders({ key: 'responseHeaders', defaultValue: headerMapList });
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
FormItem,
Input,
}}
/>
);
};

View File

@ -0,0 +1,26 @@
import React from 'react';
import { ViewRequestHeaders } from './RequestHeaders.view';
import { ViewResponseBody } from './ResponseBody.view';
import { ViewResponseHeaders } from './ResponseHeaders.view';
export const getItemsResponseTab = (params) => {
const { t } = params;
return [
{
key: 'body',
label: t('Body'),
children: <ViewResponseBody />,
},
{
key: 'responseHeaders',
label: t('Response headers'),
children: <ViewResponseHeaders />,
},
{
key: 'requestHeaders',
label: t('Request headers'),
children: <ViewRequestHeaders />,
},
];
};

View File

@ -0,0 +1,27 @@
import React from 'react';
import { Card, Tabs, Tag } from 'antd';
import { useTranslation } from '../../../../../locale';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getItemsResponseTab } from './ResponseTab.item';
export const ResponseTab = () => {
const { t } = useTranslation();
const { rawResponse } = useContextResponseInfo();
const { status } = rawResponse || {};
const items = getItemsResponseTab({ t });
if (!rawResponse) {
return null;
}
const color = status.toString().startsWith('2') ? 'success' : 'error';
const TitleComp = <Tag color={color}>{`HTTP Code:${status}`}</Tag>;
return (
<Card title={TitleComp}>
<Tabs defaultActiveKey="body" items={items} />
</Card>
);
};

View File

@ -0,0 +1,12 @@
import React from 'react';
import { Tooltip } from 'antd';
import { useTranslation } from '../../../../../locale';
export const TooltipContainer = (props) => {
const { t } = useTranslation();
const title = t('Extract field metadata from the response data');
return <Tooltip title={title}>{props.children}</Tooltip>;
};

View File

@ -0,0 +1,105 @@
import { getRequestValues } from '../../../utils/getRequestValues';
export const getSchemaAction = (form, actionKey) => {
const reactionsFunc = (keyName, params) => {
const result = getRequestValues(form, actionKey);
const targetRes = result.find((item) => {
const itemArr = item?.split('.') || [];
return itemArr.pop() === keyName;
});
if (targetRes) {
params.description = null;
}
};
return {
page: {
type: 'number',
title: 'Page',
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
description: "{{t('Page variable not used. Pagination based on total current request data.')}}",
'x-reactions': (params) => reactionsFunc('page', params),
},
pageSize: {
type: 'number',
title: 'PageSize',
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
description: "{{t('PageSize variable not used. Pagination based on total current request data.')}}",
'x-reactions': (params) => reactionsFunc('pageSize', params),
},
filter: {
type: 'object',
title: 'Filter',
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
description: "{{t('Filter variable not used. Filtering will apply to the current request data.')}}",
'x-reactions': (params) => reactionsFunc('filter', params),
},
sort: {
type: 'string',
title: 'Sort',
'x-decorator': 'FormItem',
'x-component': 'Input',
description: "{{t('Sort variable not used. Sorting will apply to the current request data.')}}",
'x-reactions': (params) => reactionsFunc('sort', params),
},
appends: {
type: 'string',
title: 'Appends',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
fields: {
type: 'string',
title: 'Fields',
'x-decorator': 'FormItem',
'x-component': 'Input',
description: "{{t('Fields variable not used. Filtering will be based on the current request data.')}}",
'x-reactions': (params) => reactionsFunc('fields', params),
},
except: {
type: 'string',
title: 'Except',
'x-decorator': 'FormItem',
'x-component': 'Input',
description: "{{t('Except variable not used. Filtering will be based on the current request data.')}}",
'x-reactions': (params) => reactionsFunc('except', params),
},
filterByTk: {
type: 'string',
title: 'FilterByTk',
'x-decorator': 'FormItem',
'x-component': 'Input',
description: "{{t('The current parameters are not adapted')}}",
'x-reactions': (params) => reactionsFunc('filterByTk', params),
},
whiteList: {
type: 'string',
title: 'whiteList',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
blacklist: {
type: 'string',
title: 'blacklist',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
body: {
type: 'object',
title: 'Body',
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
'x-component-props': {
autoSize: {
minRows: 5,
maxRows: 20,
},
},
},
};
};

View File

@ -0,0 +1,16 @@
import { paramsMap } from '../../../constants/mapListve';
import { getSchemaAction } from './getSchemaAction';
export function getSchemaParam(actionKey, form) {
const schemaChild = {};
const targetParams = paramsMap[actionKey] || [];
const schemaMap = getSchemaAction(form, actionKey);
for (let param of targetParams) {
schemaChild[param] = schemaMap[param];
}
return schemaChild;
}

View File

@ -0,0 +1,20 @@
import { useContext } from 'react';
import { ActionContext } from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
export const useCancelAction = () => {
const form = useForm();
const { setVisible } = useContext(ActionContext);
const { setRawResponse, setDebugResponse } = useContextResponseInfo();
return {
run() {
setVisible(false);
form.reset();
form.setValuesIn('responseTab', null);
setRawResponse(null);
setDebugResponse(null);
},
};
};

View File

@ -0,0 +1,60 @@
import { useAPIClient } from '@tachybase/client';
import { useField, useForm } from '@tachybase/schema';
import lodash from 'lodash';
import { useParams } from 'react-router-dom';
import { useContextRequestInfo } from '../../../contexts/RequestForm.context';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
export const useDebugAction = () => {
const field = useField();
const apiClient = useAPIClient();
const form = useForm();
const { form: formValue, actionKey, requestActionForm } = useContextRequestInfo();
const { name } = useParams();
const { setRawResponse, setDebugResponse, setResponseValidationErrorMessage }: any = useContextResponseInfo();
return {
async run() {
const actionValue = formValue?.values?.actions?.[actionKey];
try {
field.data = field?.data || {};
field.data.loading = true;
await requestActionForm.submit();
const repo = apiClient.resource('dataSources.httpCollections', name);
const debugVars = lodash.omit(form.values, 'responseTab');
const {
data: { data: responseData },
} = await repo.runAction({
values: {
debug: true,
inferFields: false,
actionOptions: {
...actionValue,
type: actionKey,
responseTransformer: actionValue?.responseTransformer,
},
debugVars,
},
});
field.data.loading = false;
const { rawResponse, debugBody, responseValidationErrorMessage } = responseData;
setRawResponse(rawResponse);
setDebugResponse(debugBody);
setResponseValidationErrorMessage(responseValidationErrorMessage);
} catch (error) {
field.data.loading = false;
console.log(error);
}
},
};
};

View File

@ -0,0 +1,46 @@
export const getSchemaFieldMethod = (params) => {
const { t, method, setFormValue } = params;
return {
name: 'method',
title: 'HTTP method',
default: method,
'x-decorator': 'FormItem',
'x-decorator-props': {
validator: {
required: true,
message: t('Method is required'),
},
feedbackLayout: 'popover',
},
required: true,
'x-component': 'Select',
'x-component-props': {
defaultValue: method,
onChange: (value) => {
setFormValue(value, 'method');
},
options: [
{
value: 'GET',
label: 'GET',
},
{
value: 'POST',
label: 'POST',
},
{
value: 'PUT',
label: 'PUT',
},
{
value: 'PATCH',
label: 'PATCH',
},
{
value: 'DELETE',
label: 'DELETE',
},
],
},
};
};

View File

@ -0,0 +1,17 @@
import React from 'react';
import { SchemaComponent } from '@tachybase/client';
import { useTranslation } from '../../../../locale';
import { getSchemaFieldMethod } from './FieldMethod.schema';
export const ViewFieldMethod = (props) => {
const { t } = useTranslation();
const { method, setFormValue } = props;
const schema = getSchemaFieldMethod({
t,
method,
setFormValue,
});
return <SchemaComponent schema={schema} />;
};

View File

@ -0,0 +1,67 @@
import React from 'react';
import { css, EllipsisWithTooltip, Input, Variable } from '@tachybase/client';
import { useVariableOptions } from '../../scopes/useVariableOptions';
export const getSchemaFieldPath = (params) => {
const { t, path, setFormValue, scCtx } = params;
return {
name: 'path',
title: 'URL',
required: true,
default: path,
'x-decorator': 'FormItem',
'x-decorator-props': {
feedbackLayout: 'popover',
addonBefore: (
<EllipsisWithTooltip ellipsis>
<Input.ReadPretty value={scCtx?.dataSourceData?.data?.options?.baseUrl} />
</EllipsisWithTooltip>
),
validator: {
required: true,
message: t('Path is required'),
},
className: css`
.ant-formily-item-addon-before {
border-width: 1px;
border-style: solid;
border-color: #d9d9d9;
padding: 0px 5px;
margin-right: 0px;
background: rgba(0, 0, 0, 0.02);
border-right: 0px;
margin-inline-end: 0px !important;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
.ant-description-input {
white-space: nowrap;
max-width: 230px;
overflow: hidden;
text-overflow: ellipsis;
}
}
`,
style: { width: '100%' },
},
'x-component': Variable.RawTextArea,
'x-component-props': {
defaultValue: path,
autoSize: true,
onChange: (val) => {
setFormValue(val.target.value, 'path');
},
scope: useVariableOptions,
fieldNames: {
value: 'name',
label: 'title',
},
style: {
borderTopLeftRadius: '0',
borderBottomLeftRadius: '0',
},
},
};
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import { SchemaComponent, useSchemaComponentContext } from '@tachybase/client';
import { useTranslation } from '../../../../locale';
import { getSchemaFieldPath } from './FieldPath.schema';
export const ViewFieldPath = (props) => {
const { t } = useTranslation();
const scCtx: any = useSchemaComponentContext();
const { path, setFormValue } = props;
const schema = getSchemaFieldPath({
t,
path,
setFormValue,
scCtx,
});
return <SchemaComponent schema={schema} />;
};

View File

@ -0,0 +1,47 @@
import React from 'react';
import { FormProvider, useCollectionRecord, useSchemaComponentContext } from '@tachybase/client';
import { createForm, SchemaOptionsContext } from '@tachybase/schema';
import lodash from 'lodash';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { ViewFieldMethod } from './FieldMethod.view';
import { ViewFieldPath } from './FieldPath.view';
export const MethodPathComponent = (props) => {
const { data }: any = useCollectionRecord();
const { form, actionKey, requestActionForm } = useContextRequestInfo();
const scCtx: any = useSchemaComponentContext();
const { path, method } = data?.actions?.[actionKey] || {};
const setFormValue = lodash.debounce(async (value, label) => {
const { actions } = form?.values || {};
form.setValuesIn('actions', {
...actions,
[actionKey]: {
...actions?.[actionKey],
[label]: value,
},
});
requestActionForm.setValuesIn(label, value);
}, 100);
const initialForm = React.useMemo(
() =>
createForm({
initialValues: requestActionForm.values,
}),
[],
);
return (
<FormProvider form={props?.actionForm ? initialForm : requestActionForm}>
<ViewFieldMethod method={method} setFormValue={setFormValue} />
<SchemaOptionsContext.Provider value={scCtx}>
<ViewFieldPath path={path} setFormValue={setFormValue} />
</SchemaOptionsContext.Provider>
</FormProvider>
);
};

View File

@ -0,0 +1,172 @@
import { css } from '@tachybase/client';
import { NAMESPACE } from '../../../../locale';
import { debounceClick } from './debounceClick.util';
export const getSchemaRequestBody = ({ defaultValue, actionKey, parentForm, field }) => ({
type: 'object',
properties: {
contentType: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
placeholder: `{{t("Content-Type",{ ns: "${NAMESPACE}" })}}`,
default: defaultValue?.contentType,
'x-component-props': {
onChange: (headerValue) => {
debounceClick(parentForm, actionKey, 'contentType', headerValue);
},
},
enum: [
{
value: 'application/x-www-form-urlencoded',
label: 'application/x-www-form-urlencoded ',
},
{
value: 'application/json',
label: 'application/json',
},
],
'x-reactions': (field) => {
const header = parentForm.values.actions[actionKey];
const contentType = header?.contentType;
if (field?.value !== contentType) {
field.setValue(contentType ?? 'application/json');
}
if (!field.value) {
debounceClick(parentForm, actionKey, 'contentType', 'application/json');
}
},
},
body: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
default: defaultValue.body,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
display: 'flex',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
& > .ant-space-item:first-child,
& > .ant-space-item:nth-of-type(2) {
flex: 1;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Name")}}',
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Variable.RawTextArea',
'x-component-props': {
scope: '{{useVariableOptions}}',
autoSize: true,
fieldNames: {
value: 'name',
label: 'title',
},
style: {
minWidth: '220px',
},
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
'x-component-props': {
onClick: () => {
debounceClick(parentForm, actionKey, 'body', field.form.values.body);
},
},
},
},
},
},
},
properties: {
add: {
type: 'void',
title: `{{t("Add", { ns: "${NAMESPACE}" })}}`,
'x-component': 'ArrayItems.Addition',
},
},
'x-reactions': [
{
dependencies: ['.contentType'],
fulfill: {
state: {
visible: '{{ $deps[0]==="application/x-www-form-urlencoded"}}',
},
},
},
],
},
jsonBody: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Variable.JSON',
default: defaultValue.jsonBody,
'x-component-props': {
scope: '{{useVariableOptions}}',
autoSize: true,
fieldNames: {
value: 'name',
label: 'title',
},
placeholder: '{{t("Value")}}',
style: {
minHeight: 200,
},
onChange: (value) => {
const { actions } = parentForm.values || {};
parentForm.setValuesIn('actions', {
...actions,
[actionKey]: {
...actions?.[actionKey],
body: value,
},
});
},
},
'x-reactions': [
{
dependencies: ['.contentType'],
fulfill: {
state: {
visible: '{{ $deps[0]==="application/json"}}',
},
},
},
(filed) => {
const bodyValue = parentForm?.values?.actions?.[actionKey]?.body;
if (filed?.value !== bodyValue) {
filed.setValue(parentForm.values.actions[actionKey].body);
}
},
],
},
},
});

View File

@ -0,0 +1,40 @@
import React from 'react';
import { FormItem, FormProvider, SchemaComponent } from '@tachybase/client';
import { ArrayItems } from '@tachybase/components';
import { useField } from '@tachybase/schema';
import { Input } from 'antd';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { useVariableOptions } from '../../scopes/useVariableOptions';
import { getSchemaRequestBody } from './RequestBody.schema';
export const ViewRequestBody = () => {
const field = useField();
const { form, actionKey } = useContextRequestInfo() as any;
const valueList = form?.values?.actions?.[actionKey];
const schema = getSchemaRequestBody({
parentForm: form,
actionKey: actionKey,
defaultValue: {
contentType: valueList?.contentType,
body: valueList?.body,
jsonBody: valueList?.body,
},
field: field,
});
return (
<SchemaComponent
schema={schema}
components={{
ArrayItems,
FormItem,
Input,
FormProvider,
}}
scope={{ useVariableOptions }}
/>
);
};

View File

@ -0,0 +1,98 @@
import { css } from '@tachybase/client';
import { debounceClick } from './debounceClick.util';
export const getSchemaRequestHeaders = ({ title, defaultValue, field, parentForm, actionKey }) => {
const handleReactions = (field) => {
const headerList = parentForm?.values?.actions?.[actionKey]?.headers;
if (headerList?.length > 0 && headerList !== defaultValue) {
field.setValue(headerList);
}
};
const handleClickFunc = () => {
debounceClick(parentForm, actionKey, 'headers', field.form.values.headers);
};
return {
type: 'object',
'x-decorator': 'Form',
properties: {
headers: {
type: 'array',
'x-component': 'ArrayItems',
default: defaultValue,
'x-decorator': 'FormItem',
'x-reactions': handleReactions,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
display: 'flex',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
& > .ant-space-item:first-child,
& > .ant-space-item:nth-of-type(2) {
flex: 1;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Name")}}',
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Variable.RawTextArea',
'x-component-props': {
scope: '{{useVariableOptions}}',
autoSize: true,
fieldNames: {
value: 'name',
label: 'title',
},
style: {
minWidth: '220px',
},
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
'x-component-props': {
onClick: handleClickFunc,
},
},
},
},
},
},
properties: {
add: {
title: title,
type: 'void',
'x-component': 'ArrayItems.Addition',
},
},
},
},
};
};

View File

@ -0,0 +1,38 @@
import React from 'react';
import { FormItem, SchemaComponent } from '@tachybase/client';
import { ArrayItems } from '@tachybase/components';
import { useField } from '@tachybase/schema';
import { Input } from 'antd';
import { useTranslation } from '../../../../locale';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { useVariableOptions } from '../../scopes/useVariableOptions';
import { getSchemaRequestHeaders } from './RequestHeaders.schema';
export const ViewRequestHeaders = () => {
const { t } = useTranslation();
const field = useField();
const { form, actionKey } = useContextRequestInfo();
const valueList = form?.values?.actions?.[actionKey];
const schema = getSchemaRequestHeaders({
title: t('Add header'),
defaultValue: valueList?.headers || [],
field,
parentForm: form,
actionKey,
});
return (
<SchemaComponent
schema={schema}
components={{
ArrayItems,
FormItem,
Input,
}}
scope={{ useVariableOptions }}
/>
);
};

View File

@ -0,0 +1,95 @@
import { css } from '@tachybase/client';
import { debounceClick } from './debounceClick.util';
export const getSchemaRequestParams = ({ title, defaultValue, field, parentForm, actionKey }) => ({
type: 'object',
'x-decorator': 'Form',
properties: {
params: {
type: 'array',
'x-component': 'ArrayItems',
default: defaultValue,
'x-decorator': 'FormItem',
'x-reactions': (field) => {
const params = parentForm.values?.actions?.[actionKey]?.params;
if (params?.length && params !== defaultValue) {
field.setValue(params);
}
},
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
display: 'flex',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
& > .ant-space-item:first-child,
& > .ant-space-item:nth-of-type(2) {
flex: 1;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Name")}}',
style: {
width: '100%',
},
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Variable.RawTextArea',
'x-component-props': {
scope: '{{useVariableOptions}}',
autoSize: true,
fieldNames: {
value: 'name',
label: 'title',
},
style: {
minWidth: '230px',
width: '100%',
},
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
'x-component-props': {
onClick: () => {
debounceClick(parentForm, actionKey, 'params', field.form.values?.params);
},
},
},
},
},
},
},
properties: {
add: {
type: 'void',
title: title,
'x-component': 'ArrayItems.Addition',
},
},
},
},
});

View File

@ -0,0 +1,38 @@
import React from 'react';
import { FormItem, SchemaComponent } from '@tachybase/client';
import { ArrayItems } from '@tachybase/components';
import { useField } from '@tachybase/schema';
import { Input } from 'antd';
import { useTranslation } from '../../../../locale';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { useVariableOptions } from '../../scopes/useVariableOptions';
import { getSchemaRequestParams } from './RequestParams.schema';
export const ViewRequestParams = () => {
const { t } = useTranslation();
const field = useField();
const { form, actionKey } = useContextRequestInfo() as any;
const valueList = form?.values?.actions?.[actionKey];
const schema = getSchemaRequestParams({
title: t('Add parameter'),
defaultValue: valueList?.params,
field,
parentForm: form,
actionKey,
});
return (
<SchemaComponent
schema={schema}
components={{
ArrayItems,
FormItem,
Input,
}}
scope={{ useVariableOptions }}
/>
);
};

View File

@ -0,0 +1,27 @@
import React from 'react';
import { ViewRequestBody } from './RequestBody.view';
import { ViewRequestHeaders } from './RequestHeaders.view';
import { ViewRequestParams } from './RequestParams.view';
export const getItemsRequestTab = (params: any) => {
const { t } = params;
return [
{
key: 'parameters',
label: t('Parameters'),
children: <ViewRequestParams />,
},
{
key: 'body',
label: t('Body'),
children: <ViewRequestBody />,
},
{
key: 'headers',
label: t('Headers'),
children: <ViewRequestHeaders />,
},
];
};

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Tabs } from 'antd';
import { useTranslation } from '../../../../locale';
import { getItemsRequestTab } from './RequestTab.items';
export const RequestTab = () => {
const { t } = useTranslation();
const items = getItemsRequestTab({
t,
});
return <Tabs items={items} />;
};

View File

@ -0,0 +1,13 @@
import { lodash } from '@tachybase/utils/client';
export const debounceClick = lodash.debounce((parentForm, actionKey, keyName, headerValue) => {
const { actions } = parentForm.values || {};
parentForm.setValuesIn('actions', {
...actions,
[actionKey]: {
...actions?.[actionKey],
[keyName]: headerValue,
},
});
}, 400);

View File

@ -0,0 +1,45 @@
import React, { useEffect } from 'react';
import { useCollectionRecord, Variable } from '@tachybase/client';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { useVariableOptions } from '../../scopes/useVariableOptions';
import { setFormValue } from '../../utils/setFormValue';
import { responseTransformerAe } from '../../utils/responseTransformerAe';
export const ResponseTransformerComponent = () => {
const { form, actionKey, responseTransformer, setResponseTransformer } = useContextRequestInfo();
const transformer = form?.values?.actions?.[actionKey]?.responseTransformer;
const transformerValue = responseTransformer ?? transformer;
const { data } = useCollectionRecord() as any;
useEffect(() => {
if (!data?.actions?.[actionKey]?.responseTransformer && !transformer) {
const { actions } = form.values || {};
form.setValuesIn('actions', {
...actions,
[actionKey]: {
...actions?.[actionKey],
responseTransformer: responseTransformerAe,
},
});
}
}, []);
return (
<Variable.JSON
value={transformerValue}
onChange={(value) => {
setResponseTransformer(value);
setFormValue(form, actionKey, value);
}}
autoSize={{
minRows: 5,
}}
fieldNames={{
value: 'name',
label: 'title',
}}
scope={() => useVariableOptions(true)}
/>
);
};

View File

@ -0,0 +1,79 @@
import React, { useContext } from 'react';
import { SchemaComponentContext, useCompile } from '@tachybase/client';
import { useTranslation } from '../../../locale';
import { paramsMap } from '../constants/mapListve';
import { requestHeaderList } from '../constants/requestHeaderList';
import { useContextRequestInfo } from '../contexts/RequestForm.context';
export const useVariableOptions = (showResponse) => {
const compile = useCompile();
const { t } = useTranslation();
const ctx: any = useContext(SchemaComponentContext);
const { actionKey } = useContextRequestInfo();
const { variables } = ctx.dataSourceData?.data?.options || {};
const options = React.useMemo(() => getOptions({ t, variables, compile, actionKey, showResponse }), [actionKey]);
return options;
};
const getOptions = (params) => {
const { t, variables, compile, actionKey, showResponse } = params;
const options = [];
if (variables) {
options.push({
name: 'dataSourceVariables',
title: t('Custom variables'),
children: variables?.map((val) => ({
name: compile(val.name),
title: val.name,
})),
});
}
options.push({
name: 'request',
title: t('TachyBase request'),
children: [
{
name: 'params',
title: 'params',
children: (paramsMap[actionKey] || []).map((value) => ({
name: value,
title: value,
})),
},
{
name: 'header',
title: 'headers',
children: requestHeaderList,
},
{
name: 'body',
title: 'Body',
},
{
name: 'token',
title: 'Token',
},
],
});
if (showResponse) {
options.push({
name: 'rawResponse',
title: t('Third party response'),
children: [
{
name: 'body',
title: 'body',
},
],
});
}
return options;
};

View File

@ -0,0 +1,21 @@
// 这个函数的目的是筛选出参数对象中的属性,这些属性必须满足两个条件:有一个 method 属性和一个 path 属性。然后,它将这些属性的值作为新对象的键值对
export function filterObjectWithMethodAndPath(obj: Record<string, any>): Record<string, any> {
const keys = Object.keys(obj);
const filteredKeys = keys.filter((key) => {
const value = obj[key];
return value.method && value.path;
});
const resultObj = filteredKeys.reduce(
(filteredObj, key) => ({
...filteredObj,
[key]: obj[key],
}),
{},
);
return resultObj;
}

View File

@ -0,0 +1,17 @@
// 过滤一个树状结构的数据,只保留那些子项的 availableTypes 包含 type 的项
export const filterTree = (items, type) => {
const filteredItems = items
.filter((item) => {
const matchingChildren = item.children.filter(
(child) => child.availableTypes && child.availableTypes.includes(type),
);
return matchingChildren.length > 0;
})
.map((item) => ({
label: item.label,
key: item.key,
children: item.children.filter((child) => child.availableTypes && child.availableTypes.includes(type)),
}));
return filteredItems;
};

View File

@ -0,0 +1,3 @@
export function getRequestActions(sourceMethod) {
return ['list', 'get'].filter((method) => !sourceMethod.includes(method));
}

View File

@ -0,0 +1,23 @@
export function getRequestValues(form, actionKey) {
const actionValue = form?.values?.actions?.[actionKey];
let result = [];
const { params = [], headers = [], body = [] } = actionValue || {};
result = [...params, ...headers].map((key) => {
return key?.value?.replace(/{{|}}/g, '');
});
if (typeof body == 'string') {
result.concat(body?.replace(/{{|}}/g, ''));
} else {
result.concat(
body?.map((item) => {
return item?.value.replace(/{{|}}/g, '');
}),
);
}
return result.filter(Boolean);
}

View File

@ -0,0 +1,4 @@
export const responseTransformerAe = {
data: '{{rawResponse.body}}',
meta: {},
};

View File

@ -0,0 +1,10 @@
export const setFormValue = async (form, action, value) => {
const { actions } = form.values || {};
form.setValuesIn('actions', {
...actions,
[action]: {
...actions?.[action],
responseTransformer: value,
},
});
};

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { SchemaComponent } from '@tachybase/client'; import { SchemaComponent } from '@tachybase/client';
import { tval, usePluginTranslation } from '../locale'; import { tval, useTranslation } from '../locale';
export const MysqlDataSourceSettingsForm = () => { export const MysqlDataSourceSettingsForm = () => {
const { t } = usePluginTranslation(); const { t } = useTranslation();
return ( return (
<SchemaComponent <SchemaComponent
scope={{ t }} scope={{ t }}

View File

@ -3,10 +3,10 @@ import { SchemaComponent } from '@tachybase/client';
import { Space } from 'antd'; import { Space } from 'antd';
import { tval, usePluginTranslation } from '../locale'; import { tval, useTranslation } from '../locale';
export const PgDataSourceSettingsForm = () => { export const PgDataSourceSettingsForm = () => {
const { t } = usePluginTranslation(); const { t } = useTranslation();
return ( return (
<SchemaComponent <SchemaComponent
scope={{ t }} scope={{ t }}

View File

@ -1,19 +1,28 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '@tachybase/client';
import { PluginDataSourceManagerClient } from '@tachybase/plugin-data-source-manager/client'; import { PluginDataSourceManagerClient } from '@tachybase/plugin-data-source-manager/client';
import { KitHttpDatasource } from './features/rest-api/kit';
import { MysqlDataSourceSettingsForm } from './forms/msql'; import { MysqlDataSourceSettingsForm } from './forms/msql';
import { PgDataSourceSettingsForm } from './forms/pg'; import { PgDataSourceSettingsForm } from './forms/pg';
import { tval } from './locale'; import { tval } from './locale';
export class PluginExternalDataSourceClient extends Plugin { export class PluginExternalDataSourceClient extends Plugin {
async afterAdd() {
// 注册 REST API 数据源
this.app.pm.add(KitHttpDatasource);
}
async load() { async load() {
this.app.pm // 注册 PostgreSQL 数据源
.get(PluginDataSourceManagerClient) this.app.pm.get(PluginDataSourceManagerClient).registerType('postgres', {
.registerType('postgres', { DataSourceSettingsForm: PgDataSourceSettingsForm, label: tval('PostgreSQL') }); DataSourceSettingsForm: PgDataSourceSettingsForm,
label: tval('PostgreSQL'),
});
this.app.pm // 注册 MySQL 数据源
.get(PluginDataSourceManagerClient) this.app.pm.get(PluginDataSourceManagerClient).registerType('mysql', {
.registerType('mysql', { DataSourceSettingsForm: MysqlDataSourceSettingsForm, label: tval('MySQL') }); DataSourceSettingsForm: MysqlDataSourceSettingsForm,
label: tval('MySQL'),
});
} }
} }

View File

@ -1,15 +1,20 @@
import { i18n } from '@tachybase/client'; import { i18n, tval as nTval, useApp } from '@tachybase/client';
import { useTranslation } from 'react-i18next';
export const NAMESPACE = '@hera/plugin-external-data-source'; export const NAMESPACE = '@hera/plugin-external-data-source';
// NOTE: 保持翻译统一经由这里处理, 所有本插件内的翻译方法从这里统一导出
export const tval = (key: string, useCore = false) => nTval(key, { ns: useCore ? undefined : NAMESPACE });
export function lang(key: string, options = {}) { export function lang(key: string, options = {}) {
return i18n.t(key, { ...options, ns: NAMESPACE }); return i18n.t(key, { ...options, ns: NAMESPACE });
} }
export function tval(key: string) {
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`; export const useTranslation = (useCore = false): any => {
} const { i18n } = useApp();
export function usePluginTranslation() { const t = (key: string, props = {}) =>
return useTranslation(NAMESPACE); i18n.t(key, {
} ns: useCore ? undefined : NAMESPACE,
...props,
});
return { t };
};

View File

@ -1,12 +1,16 @@
{ {
"Data source name": "Data source name", "Data source name": "Data source name",
"Data source display name": "Data source display name", "Data source display name": "Data source display name",
"Get":"Get(required)",
"Host": "Host", "Host": "Host",
"If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.": "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.",
"List":"List(required)",
"Port": "Port", "Port": "Port",
"Database": "Database", "Database": "Database",
"Database connections": "Database connections", "Database connections": "Database connections",
"Test Connection": "Test Connection", "Test Connection": "Test Connection",
"Connection successful'": "Connection successful'", "Connection successful'": "Connection successful'",
"Create collection": "Create collection",
"Display name": "Display name", "Display name": "Display name",
"Username": "Username", "Username": "Username",
"Password": "Password", "Password": "Password",
@ -22,5 +26,7 @@
"Postgres": "Postgres", "Postgres": "Postgres",
"Enabled the data source": "Enabled the data source", "Enabled the data source": "Enabled the data source",
"Table prefix": "Table prefix", "Table prefix": "Table prefix",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter." "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.",
"Record unique key": "Record unique key",
"Request actions": "Request actions"
} }

View File

@ -1,12 +1,16 @@
{ {
"Data source name": "数据源标识", "Data source name": "数据源标识",
"Data source display name": "数据源名称", "Data source display name": "数据源名称",
"Get":"Get(必填)",
"Host": "服务器地址", "Host": "服务器地址",
"If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.": "当数据表没有主键时,你需要配置记录唯一标识符,用于在区块中定位行记录,不配置将无法创建该表的数据区块。",
"List":"List(必填)",
"Port": "端口", "Port": "端口",
"Database": "数据库", "Database": "数据库",
"Database connections": "连接第三方数据库", "Database connections": "连接第三方数据库",
"Test Connection": "测试连接", "Test Connection": "测试连接",
"Connection successful'": "连接成功", "Connection successful'": "连接成功",
"Create collection": "创建数据表",
"Display name": "名称", "Display name": "名称",
"Username": "用户名", "Username": "用户名",
"Password": "密码", "Password": "密码",
@ -22,5 +26,7 @@
"Postgres":"PostgreSQL", "Postgres":"PostgreSQL",
"Enabled the data source":"启用数据源", "Enabled the data source":"启用数据源",
"Table prefix":"表前缀", "Table prefix":"表前缀",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。" "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
"Record unique key": "记录唯一标识符",
"Request actions": "请求操作"
} }

View File

@ -0,0 +1,39 @@
import { Plugin } from '@tachybase/server';
import _ from 'lodash';
import { HttpCollection } from './services/http-collection';
import { HttpDataSource } from './services/http-data-source';
export class PluginHttpDatasource extends Plugin {
async afterAdd() {}
async beforeLoad() {
this.app.dataSourceManager.factory.register('http', HttpDataSource);
this.app.resourcer.define({
name: 'dataSources.httpCollections',
actions: {
async runAction(ctx, next) {
const { sourceId } = ctx.action;
const { actionOptions, inferFields, debugVars, debug } = ctx.action.params.values;
if (!actionOptions.type) {
_.set(actionOptions, 'type', 'list');
}
const dataSource = ctx.app.dataSourceManager.dataSources.get(sourceId);
ctx.body = await HttpCollection.runAction({
dataSource,
actionOptions,
parseField: inferFields,
runAsDebug: debug,
debugVars,
});
await next();
},
},
});
}
async load() {}
async install() {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}

View File

@ -0,0 +1,90 @@
import _ from 'lodash';
import { HttpCollection } from './http-collection';
export class HttpApiRepository {
constructor(public collection: HttpCollection) {}
async count(options) {
const { transformedResponse } = await this.collection.runAction('list');
return _.get(transformedResponse, 'meta.count', transformedResponse.data.length);
}
async find(options) {
const { transformedResponse } = await this.collection.runAction('list');
return transformedResponse.data;
}
async findAndCount(options = {}) {
const templateContext = this.buildTemplateContextFromRequestContext(options.context, {
filter: options.filter,
sort: options.sort,
});
const { transformedResponse } = await this.collection.runAction('list', templateContext);
return [transformedResponse.data, _.get(transformedResponse, 'meta.count', transformedResponse.length)];
}
async findOne(options) {
const templateContext = this.buildTemplateContextFromRequestContext(options.context, {
filter: options.filter,
sort: options.sort,
filterByTk: options.filterByTk,
});
const { transformedResponse } = await this.collection.runAction('get', templateContext);
return transformedResponse.data;
}
async create(options) {
options.values = this.handleValuesWithWhiteListAndBlackList(options);
const templateContext = this.buildTemplateContextFromRequestContext(options.context, {
values: options.values,
});
const { transformedResponse } = await this.collection.runAction('create', templateContext);
return transformedResponse.data;
}
async update(options) {
options.values = this.handleValuesWithWhiteListAndBlackList(options);
const templateContext = this.buildTemplateContextFromRequestContext(options.context, {
filter: options.filter,
sort: options.sort,
filterByTk: options.filterByTk,
values: options.values,
});
const { transformedResponse } = await this.collection.runAction('update', templateContext);
return transformedResponse.data;
}
async destroy(options) {
const templateContext = this.buildTemplateContextFromRequestContext(options.context, {
filter: options.filter,
sort: options.sort,
filterByTk: options.filterByTk,
});
const { transformedResponse } = await this.collection.runAction('destroy', templateContext);
return transformedResponse.data;
}
buildTemplateContextFromRequestContext(ctx, others = {}) {
const templateContext = { ...others };
if (_.get(ctx, 'action.params')) {
_.set(templateContext, 'request.params', ctx.action.params);
if (ctx.action.params.page) {
_.set(templateContext, 'page', ctx.action.params.page);
}
if (ctx.action.params.pageSize) {
_.set(templateContext, 'pageSize', ctx.action.params.pageSize);
}
}
if (_.get(ctx, 'request.headers')) {
_.set(templateContext, 'request.headers', ctx.request.headers);
}
return templateContext;
}
handleValuesWithWhiteListAndBlackList(options) {
const { whiteList, blackList } = options;
let { values } = options;
if (!values) {
return values;
}
if (whiteList && whiteList.length) {
values = _.pick(values, whiteList);
}
if (blackList && blackList.length) {
values = _.omit(values, blackList);
}
return values;
}
}

View File

@ -0,0 +1,12 @@
import { CollectionManager, CollectionOptions } from '@tachybase/data-source-manager';
import { HttpCollection } from './http-collection';
export class HttpCollectionManager extends CollectionManager {
constructor(options = {}) {
super(options);
}
newCollection(options: CollectionOptions) {
return new HttpCollection(options, this);
}
}

View File

@ -0,0 +1,236 @@
import { Collection } from '@tachybase/data-source-manager';
import { parse } from '@tachybase/utils';
import axios from 'axios';
import _ from 'lodash';
import { transformResponseThroughMiddleware } from './transform-response';
import { typeInterfaceMap } from './type-interface-map';
import { normalizeRequestOptions, normalizeRequestOptionsKey } from './utils';
function compileTemplate(template, context) {
return parse(template)(context);
}
function mergeRequestOptions(options) {
const { baseRequestConfig, actionOptions, templateContext = {} } = options;
const rawConfig = {
method: actionOptions.method,
url: actionOptions.path,
baseURL: baseRequestConfig.baseUrl,
headers: {
...baseRequestConfig.headers,
...actionOptions.headers,
},
params: actionOptions.params,
};
if (actionOptions.body) {
rawConfig.data = actionOptions.body;
}
if (actionOptions.contentType) {
rawConfig.headers['Content-Type'] = actionOptions.contentType;
}
if (actionOptions.contentType === 'application/x-www-form-urlencoded') {
rawConfig.data = normalizeRequestOptionsKey(rawConfig.data);
}
const config = compileTemplate(rawConfig, templateContext);
if (templateContext.values && !rawConfig.data) {
config.data = templateContext.values;
}
if (actionOptions.contentType === 'application/x-www-form-urlencoded') {
config.data = new URLSearchParams(config.data).toString();
}
return config;
}
function buildTemplateContext(options) {
const { dataSourceRequestConfig, templateContext, debugVars = {} } = options;
const variables = {
dataSourceVariables: dataSourceRequestConfig.variables || {},
};
for (const variableKey of Object.keys(variables)) {
templateContext[variableKey] = variables[variableKey];
}
for (const variableKey of Object.keys(debugVars)) {
if (variableKey == 'body') {
_.set(templateContext, ['request', 'body'], debugVars[variableKey]);
} else {
_.set(templateContext, ['request', 'params', variableKey], debugVars[variableKey]);
}
}
if (templateContext.values && !_.get(templateContext, 'request.body')) {
templateContext.body = templateContext.values;
_.set(templateContext, 'request.body', templateContext.values);
}
}
function rawTypeToFieldType(rawType, exampleValue) {
const typeInfers = {
string: 'string',
number: () => {
if (Number.isInteger(exampleValue)) {
return 'integer';
}
return 'float';
},
boolean: 'boolean',
object: () => {
if (_.isNull(exampleValue)) {
return ['string', 'integer', 'float', 'boolean', 'json'];
}
return 'json';
},
};
const inferType = typeInfers[rawType];
if (typeof inferType === 'function') {
return inferType();
}
return inferType;
}
function getInterfaceOptionsByType(type) {
const interfaceConfig = typeInterfaceMap[type];
if (typeof interfaceConfig === 'function') {
return interfaceConfig();
} else {
return interfaceConfig;
}
}
function parseResponseToFieldsOptions(responseData) {
let objectItem = {};
if (Array.isArray(responseData)) {
objectItem = responseData[0];
}
if (_.isPlainObject(responseData)) {
objectItem = responseData;
}
return Object.keys(objectItem).map((key) => {
const rawType = typeof objectItem[key];
const inferredFieldType = rawTypeToFieldType(rawType, objectItem[key]);
let fieldOptions = {
name: key,
rawType,
title: key,
field: key,
};
let fieldType = inferredFieldType;
if (Array.isArray(inferredFieldType)) {
fieldType = inferredFieldType[0];
fieldOptions['possibleTypes'] = inferredFieldType;
}
fieldOptions = {
...fieldOptions,
type: fieldType,
...getInterfaceOptionsByType(fieldType),
};
_.set(fieldOptions, 'uiSchema.title', key);
return fieldOptions;
});
}
function guessFilterTargetKeyName(fields) {
const keyNames = ['id', 'nodeId', 'node_id'];
for (const keyName of keyNames) {
if (fields.find((field) => field.field === keyName)) {
return keyName;
}
}
return void 0;
}
export class HttpCollection extends Collection {
availableActions() {
const allActionOptions = this.options.actions || {};
const actions = ['list', 'get', 'create', 'update', 'destroy'];
return actions.filter((action) => allActionOptions[action]);
}
// send http request to external server
static async runAction(options) {
const { dataSource, actionOptions, templateContext = {}, parseField, debugVars, runAsDebug } = options;
normalizeRequestOptions(actionOptions);
const dataSourceRequestConfig = dataSource.requestConfig();
buildTemplateContext({
dataSourceRequestConfig,
templateContext,
debugVars,
});
const requestConfig = mergeRequestOptions({
baseRequestConfig: dataSourceRequestConfig,
actionOptions,
templateContext,
});
if (runAsDebug) {
requestConfig.validateStatus = () => true;
}
const handleResponse = (response2) => {
if (parseField && response2.transformedResponse.data) {
response2.fields = parseResponseToFieldsOptions(response2.transformedResponse.data);
const filterTargetKeyName = guessFilterTargetKeyName(response2.fields);
if (filterTargetKeyName) {
response2.filterTargetKey = filterTargetKeyName;
}
}
if (options.runAsDebug && !response2.responseValidationErrorMessage) {
response2.debugBody = response2.transformedResponse;
}
return response2;
};
const response = await axios.request(requestConfig).catch((error) => {
throw new Error(`Failed to request external http datasource`, { cause: error });
});
const clientRequest = response.request;
const baseResponse = {
rawResponse: {
status: response.status,
headers: response.headers,
data: response.data,
body: response.data,
request: {
url: axios.getUri(requestConfig),
method: clientRequest.method,
headers: response.request.getHeaders(),
},
},
data: null,
};
const transformResponse = (baseResponse2, actionOptions2) => {
const transformer = actionOptions2.responseTransformer;
baseResponse2.transformedResponse = compileTemplate(transformer, {
rawResponse: baseResponse2.rawResponse,
});
try {
transformResponseThroughMiddleware({
transformedResponse: baseResponse2.transformedResponse,
actionOptions: actionOptions2,
templateContext,
});
} catch (e) {
if (options.runAsDebug) {
baseResponse2.responseValidationErrorMessage = e.message;
} else {
throw e;
}
}
};
transformResponse(baseResponse, actionOptions);
dataSource.logger.debug({
actionOptions,
requestConfig,
baseResponse,
});
return handleResponse(baseResponse);
}
getActionOptions(action) {
const allActionOptions = this.options.actions || {};
const actionOption = allActionOptions[action];
if (!actionOption) {
throw new Error(`request config of action "${action}" is not set`);
}
return actionOption;
}
runAction(action: string, templateContext?: any) {
const actionOptions = this.getActionOptions(action);
return HttpCollection.runAction({
actionOptions: {
...actionOptions,
type: action,
},
dataSource: this.collectionManager.dataSource,
templateContext,
});
}
}

View File

@ -0,0 +1,34 @@
import { DataSource } from '@tachybase/data-source-manager';
import _ from 'lodash';
import { HttpApiRepository } from './http-api-repository';
import { HttpCollectionManager } from './http-collection-manager';
import { normalizeRequestOptions } from './utils';
export class HttpDataSource extends DataSource {
async load(options = {}) {
const { localData } = options;
for (const collectionName of Object.keys(localData)) {
this.collectionManager.defineCollection(localData[collectionName]);
}
}
createCollectionManager(options) {
const collectionManager = new HttpCollectionManager({
dataSource: this,
});
collectionManager.registerRepositories({
Repository: HttpApiRepository,
});
return collectionManager;
}
requestConfig() {
const configKeys = ['baseUrl', 'headers', 'variables', 'timeout', 'responseType'];
const config = _.pick(this.options, configKeys);
normalizeRequestOptions(config);
return config;
}
publicOptions() {
return _.pick(this.options, ['baseUrl', 'variables']);
}
}

View File

@ -0,0 +1,3 @@
export class HttpRequestBuilder {
constructor(dataSource) {}
}

View File

@ -0,0 +1,151 @@
import { filterMatch } from '@tachybase/database';
import _ from 'lodash';
const middlewares = {
list: [
function validateListActionTransformedResponse(options) {
const { transformedResponse } = options;
const dataItem = _.get(transformedResponse, 'data');
if (!Array.isArray(dataItem)) {
throw new Error(
`The transformed response for the list action should be an array. got ${getValueType(dataItem)}`,
);
}
if (!dataItem.every((item) => _.isPlainObject(item))) {
throw new Error('Every item in the transformed response should be an object');
}
},
function handleFilter(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.filter'])) {
return;
}
const filter = _.get(options.templateContext, 'request.params.filter') || {};
const { data } = options.transformedResponse;
options.transformedResponse.data = data.filter((item) => {
return filterMatch(item, filter);
});
},
function handleSort(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.sort'])) {
return;
}
const sort = _.get(options.templateContext, 'request.params.sort') || [];
if (sort.length === 0) {
return;
}
const { data } = options.transformedResponse;
options.transformedResponse.data = data.sort((a, b) => {
for (const sortItem of sort) {
const isDesc = sortItem.startsWith('-');
const field = isDesc ? sortItem.slice(1) : sortItem;
if (a[field] > b[field]) {
return isDesc ? -1 : 1;
}
if (a[field] < b[field]) {
return isDesc ? 1 : -1;
}
}
return 0;
});
},
function handleFullList(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.page'])) {
return;
}
const page = parseInt(_.get(options.templateContext, 'request.params.page', 1));
const pageSize = parseInt(_.get(options.templateContext, 'request.params.pageSize', 20));
const { data } = options.transformedResponse;
const start = (page - 1) * pageSize;
const end = start + pageSize;
options.transformedResponse.data = data.slice(start, end);
_.set(options.transformedResponse, 'meta.count', data.length);
_.set(options.transformedResponse, 'meta.page', page);
_.set(options.transformedResponse, 'meta.pageSize', pageSize);
_.set(options.transformedResponse, 'meta.totalPage', Math.ceil(data.length / pageSize));
},
function handleFields(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.fields'])) {
return;
}
const fields = _.get(options.templateContext, 'request.params.fields') || [];
if (fields.length === 0) {
return;
}
const { data } = options.transformedResponse;
options.transformedResponse.data = data.map((item) => {
return _.pick(item, fields);
});
},
function handleExpect(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.expect'])) {
return;
}
const expect = _.get(options.templateContext, 'request.params.expect') || [];
if (expect.length === 0) {
return;
}
const { data } = options.transformedResponse;
options.transformedResponse.data = data.map((item) => {
return _.omit(item, expect);
});
},
],
get: [
function validateGetActionTransformedResponse(options) {
const { transformedResponse } = options;
const dataItem = _.get(transformedResponse, 'data');
if (!_.isPlainObject(dataItem)) {
throw new Error(
`The transformed response for the get action should be an object. got ${getValueType(dataItem)}`,
);
}
},
function handleFields2(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.fields'])) {
return;
}
const fields = _.get(options.templateContext, 'request.params.fields') || [];
if (fields.length === 0) {
return;
}
const { data } = options.transformedResponse;
options.transformedResponse.data = _.pick(data, fields);
},
function handleExpect2(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.expect'])) {
return;
}
const expect = _.get(options.templateContext, 'request.params.expect') || [];
if (expect.length === 0) {
return;
}
const { data } = options.transformedResponse;
options.transformedResponse.data = _.omit(data, expect);
},
],
create: [],
update: [],
destroy: [],
};
export function transformResponseThroughMiddleware(options) {
const { actionOptions } = options;
const { type } = actionOptions;
const actionMiddlewares = middlewares[type] || [];
actionMiddlewares.forEach((middleware) => {
middleware(options);
});
}
function checkVariablesAreUsed(options, variables) {
const template = JSON.stringify(options);
return variables.every((variable) => template.includes(`{{${variable}}}`));
}
function getValueType(value) {
if (Array.isArray(value)) {
return 'array';
}
if (_.isPlainObject(value)) {
return 'object';
}
return typeof value;
}

View File

@ -0,0 +1,220 @@
export const typeInterfaceMap = {
array: '',
belongsTo: '',
belongsToMany: '',
boolean: () => {
return {
interface: 'checkbox',
uiSchema: {
type: 'boolean',
'x-component': 'Checkbox',
},
};
},
context: '',
date: () => {
return {
interface: 'datetime',
uiSchema: {
'x-component': 'DatePicker',
'x-component-props': {
dateFormat: 'YYYY-MM-DD',
showTime: false,
},
},
};
},
hasMany: '',
hasOne: '',
json: () => {
return {
interface: 'json',
uiSchema: {
'x-component': 'Input.JSON',
'x-component-props': {
autoSize: {
minRows: 5,
// maxRows: 20,
},
},
default: null,
},
};
},
jsonb: () => {
return {
interface: 'json',
uiSchema: {
'x-component': 'Input.JSON',
'x-component-props': {
autoSize: {
minRows: 5,
// maxRows: 20,
},
},
default: null,
},
};
},
integer: () => ({
interface: 'integer',
// name,
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '1',
},
'x-validator': 'integer',
},
}),
bigInt: (columnInfo) => {
return {
interface: 'integer',
uiSchema: {
'x-component': 'InputNumber',
'x-component-props': {
style: {
width: '100%',
},
},
},
};
},
float: () => {
return {
interface: 'number',
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '1',
},
},
};
},
double: () => {
return {
interface: 'number',
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '1',
},
},
};
},
real: () => {
return {
interface: 'number',
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '1',
},
},
};
},
decimal: () => {
return {
interface: 'number',
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '1',
},
},
};
},
password: () => ({
interface: 'password',
hidden: true,
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Password',
},
}),
radio: '',
set: '',
sort: '',
string: () => {
return {
interface: 'input',
uiSchema: {
'x-component': 'Input',
'x-component-props': {
style: {
width: '100%',
},
},
},
};
},
text: () => {
return {
interface: 'textarea',
// name,
uiSchema: {
type: 'string',
'x-component': 'Input.TextArea',
},
};
},
time: () => ({
interface: 'time',
// name,
uiSchema: {
type: 'string',
'x-component': 'TimePicker',
'x-component-props': {
format: 'HH:mm:ss',
},
},
}),
uid: () => {
return {
interface: 'input',
uiSchema: {
'x-component': 'Input',
'x-component-props': {
style: {
width: '100%',
},
},
},
};
},
uuid: () => {
return {
interface: 'uuid',
uiSchema: {
'x-component': 'Input',
'x-component-props': {
style: {
width: '100%',
},
},
},
};
},
virtual: '',
point: '',
polygon: '',
lineString: '',
circle: '',
};

View File

@ -0,0 +1,21 @@
export function normalizeRequestOptionsKey(value) {
if (Array.isArray(value)) {
return Object.fromEntries(
value.map((item) => {
const key = item.name;
const value2 = item.value;
return [key, value2];
}),
);
}
return value;
}
export function normalizeRequestOptions(actionOptions) {
const arrayKeys = ['headers', 'variables', 'params'];
for (const key of arrayKeys) {
if (actionOptions[key]) {
actionOptions[key] = normalizeRequestOptionsKey(actionOptions[key]);
}
}
return actionOptions;
}

View File

@ -1,9 +1,14 @@
import { Plugin } from '@tachybase/server'; import Application, { Plugin, PluginOptions } from '@tachybase/server';
import { PluginHttpDatasource } from './http/plugin';
import { MySQLDataSource } from './mysql/mysql-data-source'; import { MySQLDataSource } from './mysql/mysql-data-source';
import { PostgresDataSource } from './pg/postgres-data-source'; import { PostgresDataSource } from './pg/postgres-data-source';
export class PluginExternalDataSourceServer extends Plugin { export class PluginExternalDataSourceServer extends Plugin {
constructor(app: Application, options?: PluginOptions) {
super(app, options);
this.addFeature(PluginHttpDatasource);
}
async afterAdd() {} async afterAdd() {}
async beforeLoad() { async beforeLoad() {

View File

@ -133,10 +133,10 @@ importers:
version: 8.4.0(eslint@9.10.0)(typescript@5.4.5) version: 8.4.0(eslint@9.10.0)(typescript@5.4.5)
umi: umi:
specifier: ^4.3.3 specifier: ^4.3.3
version: 4.3.3(@babel/core@7.25.2)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@9.10.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.5))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0) version: 4.3.3(@babel/core@7.22.10)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@9.10.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.5))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0)
vitest: vitest:
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.6.0(@types/node@20.14.2)(jsdom@24.1.1(canvas@2.11.2(encoding@0.1.13)))(less@4.2.0)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6) version: 1.6.0(@types/node@20.14.2)(jsdom@24.1.1(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6)
packages/core/acl: packages/core/acl:
dependencies: dependencies:
@ -1010,7 +1010,7 @@ importers:
version: 5.4.4 version: 5.4.4
umi: umi:
specifier: ^4.3.3 specifier: ^4.3.3
version: 4.3.3(@babel/core@7.22.10)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@8.55.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.4))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0) version: 4.3.3(@babel/core@7.25.2)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@8.55.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.4))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0)
packages/core/evaluators: packages/core/evaluators:
dependencies: dependencies:
@ -2783,6 +2783,9 @@ importers:
packages/plugins/@tachybase/plugin-external-data-source: packages/plugins/@tachybase/plugin-external-data-source:
dependencies: dependencies:
'@ant-design/icons':
specifier: ^5.4.0
version: 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tachybase/client': '@tachybase/client':
specifier: workspace:* specifier: workspace:*
version: link:../../../core/client version: link:../../../core/client
@ -2813,6 +2816,9 @@ importers:
react-i18next: react-i18next:
specifier: ^14.1.2 specifier: ^14.1.2
version: 14.1.2(i18next@23.13.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 14.1.2(i18next@23.13.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom:
specifier: ^6.25.1
version: 6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies: devDependencies:
'@types/lodash': '@types/lodash':
specifier: ^4.17.5 specifier: ^4.17.5
@ -20462,7 +20468,7 @@ snapshots:
'@ant-design/pro-layout@7.17.16(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@ant-design/pro-layout@7.17.16(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@ant-design/icons': 5.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/icons': 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/pro-provider': 2.13.5(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/pro-provider': 2.13.5(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/pro-utils': 2.15.2(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/pro-utils': 2.15.2(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@babel/runtime': 7.25.0 '@babel/runtime': 7.25.0
@ -20493,7 +20499,7 @@ snapshots:
'@ant-design/pro-utils@2.15.2(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@ant-design/pro-utils@2.15.2(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@ant-design/icons': 5.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/icons': 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/pro-provider': 2.13.5(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/pro-provider': 2.13.5(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@babel/runtime': 7.25.0 '@babel/runtime': 7.25.0
antd: 5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) antd: 5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -38420,19 +38426,19 @@ snapshots:
uglify-to-browserify@1.0.2: uglify-to-browserify@1.0.2:
optional: true optional: true
umi@4.3.3(@babel/core@7.22.10)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@8.55.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.4))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0): umi@4.3.3(@babel/core@7.22.10)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@9.10.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.5))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0):
dependencies: dependencies:
'@babel/runtime': 7.23.6 '@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.3.3 '@umijs/bundler-utils': 4.3.3
'@umijs/bundler-webpack': 4.3.3(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0) '@umijs/bundler-webpack': 4.3.3(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0)
'@umijs/core': 4.3.3 '@umijs/core': 4.3.3
'@umijs/lint': 4.3.3(eslint@8.55.0)(stylelint@16.8.2(typescript@5.4.4))(typescript@5.4.4) '@umijs/lint': 4.3.3(eslint@9.10.0)(stylelint@16.8.2(typescript@5.4.5))(typescript@5.4.5)
'@umijs/preset-umi': 4.3.3(@types/node@20.14.2)(@types/react@18.3.3)(lightningcss@1.26.0)(rollup@3.29.4)(sass@1.77.8)(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0) '@umijs/preset-umi': 4.3.3(@types/node@20.14.2)(@types/react@18.3.3)(lightningcss@1.26.0)(rollup@3.29.4)(sass@1.77.8)(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0)
'@umijs/renderer-react': 4.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@umijs/renderer-react': 4.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/server': 4.3.3 '@umijs/server': 4.3.3
'@umijs/test': 4.3.3(@babel/core@7.22.10) '@umijs/test': 4.3.3(@babel/core@7.22.10)
'@umijs/utils': 4.3.3 '@umijs/utils': 4.3.3
prettier-plugin-organize-imports: 3.2.4(prettier@3.2.5)(typescript@5.4.4) prettier-plugin-organize-imports: 3.2.4(prettier@3.2.5)(typescript@5.4.5)
prettier-plugin-packagejson: 2.4.3(prettier@3.2.5) prettier-plugin-packagejson: 2.4.3(prettier@3.2.5)
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@ -38467,19 +38473,19 @@ snapshots:
- webpack-hot-middleware - webpack-hot-middleware
- webpack-plugin-serve - webpack-plugin-serve
umi@4.3.3(@babel/core@7.25.2)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@9.10.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.5))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0): umi@4.3.3(@babel/core@7.25.2)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@8.55.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.4))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0):
dependencies: dependencies:
'@babel/runtime': 7.23.6 '@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.3.3 '@umijs/bundler-utils': 4.3.3
'@umijs/bundler-webpack': 4.3.3(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0) '@umijs/bundler-webpack': 4.3.3(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0)
'@umijs/core': 4.3.3 '@umijs/core': 4.3.3
'@umijs/lint': 4.3.3(eslint@9.10.0)(stylelint@16.8.2(typescript@5.4.5))(typescript@5.4.5) '@umijs/lint': 4.3.3(eslint@8.55.0)(stylelint@16.8.2(typescript@5.4.4))(typescript@5.4.4)
'@umijs/preset-umi': 4.3.3(@types/node@20.14.2)(@types/react@18.3.3)(lightningcss@1.26.0)(rollup@3.29.4)(sass@1.77.8)(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0) '@umijs/preset-umi': 4.3.3(@types/node@20.14.2)(@types/react@18.3.3)(lightningcss@1.26.0)(rollup@3.29.4)(sass@1.77.8)(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0)
'@umijs/renderer-react': 4.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@umijs/renderer-react': 4.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/server': 4.3.3 '@umijs/server': 4.3.3
'@umijs/test': 4.3.3(@babel/core@7.25.2) '@umijs/test': 4.3.3(@babel/core@7.25.2)
'@umijs/utils': 4.3.3 '@umijs/utils': 4.3.3
prettier-plugin-organize-imports: 3.2.4(prettier@3.2.5)(typescript@5.4.5) prettier-plugin-organize-imports: 3.2.4(prettier@3.2.5)(typescript@5.4.4)
prettier-plugin-packagejson: 2.4.3(prettier@3.2.5) prettier-plugin-packagejson: 2.4.3(prettier@3.2.5)
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@ -38835,6 +38841,23 @@ snapshots:
string_decoder: 1.3.0 string_decoder: 1.3.0
util-deprecate: 1.0.2 util-deprecate: 1.0.2
vite-node@1.6.0(@types/node@20.14.2)(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6):
dependencies:
cac: 6.7.14
debug: 4.3.6(supports-color@8.1.1)
pathe: 1.1.2
picocolors: 1.0.1
vite: 5.2.13(@types/node@20.14.2)(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6)
transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
vite-node@1.6.0(@types/node@20.14.2)(less@4.2.0)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6): vite-node@1.6.0(@types/node@20.14.2)(less@4.2.0)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6):
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14
@ -38875,6 +38898,19 @@ snapshots:
sass: 1.77.8 sass: 1.77.8
terser: 5.31.6 terser: 5.31.6
vite@5.2.13(@types/node@20.14.2)(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6):
dependencies:
esbuild: 0.20.2
postcss: 8.4.39
rollup: 4.14.1
optionalDependencies:
'@types/node': 20.14.2
fsevents: 2.3.3
less: 4.1.3
lightningcss: 1.26.0
sass: 1.77.8
terser: 5.31.6
vite@5.2.13(@types/node@20.14.2)(less@4.2.0)(lightningcss@1.26.0)(sass@1.75.0)(terser@5.31.6): vite@5.2.13(@types/node@20.14.2)(less@4.2.0)(lightningcss@1.26.0)(sass@1.75.0)(terser@5.31.6):
dependencies: dependencies:
esbuild: 0.20.2 esbuild: 0.20.2
@ -38935,6 +38971,40 @@ snapshots:
- supports-color - supports-color
- terser - terser
vitest@1.6.0(@types/node@20.14.2)(jsdom@24.1.1(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6):
dependencies:
'@vitest/expect': 1.6.0
'@vitest/runner': 1.6.0
'@vitest/snapshot': 1.6.0
'@vitest/spy': 1.6.0
'@vitest/utils': 1.6.0
acorn-walk: 8.3.2
chai: 4.3.10
debug: 4.3.5(supports-color@5.5.0)
execa: 8.0.1
local-pkg: 0.5.0
magic-string: 0.30.8
pathe: 1.1.2
picocolors: 1.0.1
std-env: 3.7.0
strip-literal: 2.0.0
tinybench: 2.6.0
tinypool: 0.8.3
vite: 5.2.13(@types/node@20.14.2)(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6)
vite-node: 1.6.0(@types/node@20.14.2)(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6)
why-is-node-running: 2.2.2
optionalDependencies:
'@types/node': 20.14.2
jsdom: 24.1.1(canvas@2.11.2(encoding@0.1.13))
transitivePeerDependencies:
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
vitest@1.6.0(@types/node@20.14.2)(jsdom@24.1.1(canvas@2.11.2(encoding@0.1.13)))(less@4.2.0)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6): vitest@1.6.0(@types/node@20.14.2)(jsdom@24.1.1(canvas@2.11.2(encoding@0.1.13)))(less@4.2.0)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6):
dependencies: dependencies:
'@vitest/expect': 1.6.0 '@vitest/expect': 1.6.0

View File

@ -26,7 +26,7 @@
"module": "commonjs" "module": "commonjs"
} }
}, },
"include": ["packages/**/*", "playwright.config.ts", "vitest.config.mts"], "include": ["packages/**/*", "playwright.config.ts", "vitest.config.mts", "packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/kit.tsx"],
"exclude": [ "exclude": [
"packages/**/node_modules", "packages/**/node_modules",
"packages/**/dist", "packages/**/dist",