diff --git a/packages/core/data-source-manager/src/index.ts b/packages/core/data-source-manager/src/index.ts index a7aec23a1..730a1c735 100644 --- a/packages/core/data-source-manager/src/index.ts +++ b/packages/core/data-source-manager/src/index.ts @@ -10,3 +10,4 @@ export * from './types'; export * from './data-source-with-database'; export * from './utils'; +export * from './collection'; diff --git a/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/CollectionsManager/index.tsx b/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/CollectionsManager/index.tsx index ca24901c7..3555407a6 100644 --- a/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/CollectionsManager/index.tsx +++ b/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/CollectionsManager/index.tsx @@ -17,11 +17,15 @@ import { SyncFieldsAction, SyncFieldsActionCom, SyncSQLFieldsAction, + usePlugin, ViewCollectionField, ViewFieldAction, } from '@tachybase/client'; import { ISchema, uid } from '@tachybase/schema'; +import { useLocation } from 'react-router-dom'; + +import PluginDatabaseConnectionsClient from '../../'; import { ConfigurationTable } from './ConfigurationTable'; import { ConfigurationTabs } from './ConfigurationTabs'; @@ -35,6 +39,11 @@ const schema2: ISchema = { }; 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 ( { ConfigurationTabs, AddFieldAction, AddCollectionField, - AddCollection, + AddCollection: type?.AddCollection || AddCollection, AddCollectionAction, - EditCollection, + EditCollection: type?.EditCollection || EditCollection, EditCollectionAction, - DeleteCollection, + DeleteCollection: type?.DeleteCollection || DeleteCollection, DeleteCollectionAction, EditFieldAction, EditCollectionField, @@ -60,6 +69,12 @@ export const CollectionManagerPage = () => { SyncFieldsActionCom, SyncSQLFieldsAction, }} + scope={{ + allowCollectionDeletion: !!type?.allowCollectionDeletion, + disabledConfigureFields: type?.disabledConfigureFields, + disableAddFields: type?.disableAddFields, + allowCollectionCreate: !!type?.allowCollectionCreate, + }} /> ); }; diff --git a/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collections.ts b/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collections.ts index 39e339e82..95891ebe9 100644 --- a/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collections.ts +++ b/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/CollectionsManager/schema/collections.ts @@ -168,7 +168,7 @@ export const collectionTableSchema: ISchema = { 'x-component-props': { type: 'primary', }, - 'x-visible': false, + 'x-visible': '{{allowCollectionCreate}}', }, }, }, diff --git a/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/ViewDatabaseConnectionAction.tsx b/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/ViewDatabaseConnectionAction.tsx index 62e0d7323..fa99cf84c 100644 --- a/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/ViewDatabaseConnectionAction.tsx +++ b/packages/plugins/@tachybase/plugin-data-source-manager/src/client/component/ViewDatabaseConnectionAction.tsx @@ -30,7 +30,7 @@ export const ViewDatabaseConnectionAction = () => { style={{ padding: '0px' }} disabled={!record.enabled} onClick={() => { - navigate(getConnectionCollectionPath(record.key)); + navigate(getConnectionCollectionPath(record)); }} role="button" aria-label={`${record?.key}-Configure`} diff --git a/packages/plugins/@tachybase/plugin-data-source-manager/src/client/constant.ts b/packages/plugins/@tachybase/plugin-data-source-manager/src/client/constant.ts index 8504fa741..f1b2f6cd0 100644 --- a/packages/plugins/@tachybase/plugin-data-source-manager/src/client/constant.ts +++ b/packages/plugins/@tachybase/plugin-data-source-manager/src/client/constant.ts @@ -1,2 +1,2 @@ -export const getConnectionCollectionPath = (name: string | number) => - `/admin/settings/data-source-manager/${name}/collections`; +export const getConnectionCollectionPath = ({ key, type }: { key: string | number; type: string }) => + `/admin/settings/data-source-manager/${key}/collections?type=${type}`; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/package.json b/packages/plugins/@tachybase/plugin-external-data-source/package.json index c92569dac..f6924c9c4 100644 --- a/packages/plugins/@tachybase/plugin-external-data-source/package.json +++ b/packages/plugins/@tachybase/plugin-external-data-source/package.json @@ -3,11 +3,13 @@ "version": "0.22.5", "main": "dist/server/index.js", "dependencies": { + "@ant-design/icons": "^5.4.0", "antd": "5.19.4", "lodash": "^4.17.21", "mysql2": "^3.9.1", "pg": "^8.11.3", - "react-i18next": "^14.1.2" + "react-i18next": "^14.1.2", + "react-router-dom": "^6.25.1" }, "devDependencies": { "@types/lodash": "^4.17.5" diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/AddCollection.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/AddCollection.tsx new file mode 100644 index 000000000..25636d975 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/AddCollection.tsx @@ -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 ; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/AddCollectionAction.component.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/AddCollectionAction.component.tsx new file mode 100644 index 000000000..70a676718 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/AddCollectionAction.component.tsx @@ -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 ( + + + + + + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/CreateCollection.schema.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/CreateCollection.schema.tsx new file mode 100644 index 000000000..a1d7309ca --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/CreateCollection.schema.tsx @@ -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 = {}) { + const cloneItem = lodash.cloneDeep(item); + + const data: Record = { + 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, + }, + }, + }, + }, + }, + }, + }, + }; +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/CreateCollection.view.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/CreateCollection.view.tsx new file mode 100644 index 000000000..7ba90faf1 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/CreateCollection.view.tsx @@ -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 ( + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/PreviewComponent.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/PreviewComponent.tsx new file mode 100644 index 000000000..ff832203f --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/PreviewComponent.tsx @@ -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 ( + + + + ); + }, + }; + }); + + return columnsConfig; + }; + + useEffect(() => { + setDataSource(preview); + }, [preview, fields]); + + useEffect(() => { + setColumns([]); + const schemaColumns = filterAndMapSchemas(fields); + setColumns(schemaColumns); + }, [form.values.fields]); + + return ( +
+ {dataSource?.length > 0 && ( + <> +
+
+ + + +
+ : +
+ + + )} + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/PreviewFields.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/PreviewFields.tsx new file mode 100644 index 000000000..a7a12a370 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/PreviewFields.tsx @@ -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 ? ( + {text} + ) : ( + + {treeChild?.map((child) => ( + + {child.children.map(({ label, value, name }) => ( + + {compile(label)} + + ))} + + ))} + + ); + }, + }, + { + title: t('Actions'), + dataIndex: 'actions', + fixed: 'right', + key: 'actions', + align: 'center', + width: 100, + render: (text, record, index) => deleteDataSoureItem(index)}> {t('Delete')}, + }, + ], + [dataSource], + ); + + useEffect(() => { + setDataSource(field.value); + }, [field.value]); + + return ( + <> + {dataSource.length ? ( + + ) : null} +
+ + ); +}; +PreviewFields.displayName = 'PreviewFields'; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/RequestActionItems.provider.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/RequestActionItems.provider.tsx new file mode 100644 index 000000000..33571b45f --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/RequestActionItems.provider.tsx @@ -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 ( + + {props.children} + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/getSchemaRequestAction.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/getSchemaRequestAction.ts new file mode 100644 index 000000000..6c05caa9d --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/getSchemaRequestAction.ts @@ -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, + }, + }, + }, + }, + }, + }, + }; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/useActionCreateCollection.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/useActionCreateCollection.ts new file mode 100644 index 000000000..89170d595 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-add/useActionCreateCollection.ts @@ -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; + } + }, + }; +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-delete/DeleteCollection.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-delete/DeleteCollection.tsx new file mode 100644 index 000000000..72800e402 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-delete/DeleteCollection.tsx @@ -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 ; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-delete/DeleteCollectionAction.component.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-delete/DeleteCollectionAction.component.tsx new file mode 100644 index 000000000..a2b3dd68a --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/collection-delete/DeleteCollectionAction.component.tsx @@ -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 ( + + + {isBulk ? ( + + + + + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/AlertError.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/AlertError.tsx new file mode 100644 index 000000000..c8cf598a1 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/AlertError.tsx @@ -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 ( + <> + +
+ + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponse.schema.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponse.schema.tsx new file mode 100644 index 000000000..7d54165ff --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponse.schema.tsx @@ -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', + }, + }, + }, + }, +}); diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponse.view.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponse.view.tsx new file mode 100644 index 000000000..e663acfb3 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponse.view.tsx @@ -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 ( + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponseTabs.items.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponseTabs.items.tsx new file mode 100644 index 000000000..8bad837c5 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponseTabs.items.tsx @@ -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: , + }, + ]; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponseTabs.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponseTabs.tsx new file mode 100644 index 000000000..db0d05c57 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/DebugResponseTabs.tsx @@ -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 ; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ExtractFieldMetadata.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ExtractFieldMetadata.tsx new file mode 100644 index 000000000..b757e64bc --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ExtractFieldMetadata.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/Headers.schema.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/Headers.schema.tsx new file mode 100644 index 000000000..3989d1e20 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/Headers.schema.tsx @@ -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")}}', + }, + }, + }, + }, + }, + }, + }, + }, +}); diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/RequestHeaders.view.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/RequestHeaders.view.tsx new file mode 100644 index 000000000..eefcfcc9f --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/RequestHeaders.view.tsx @@ -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 ( + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseBody.schema.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseBody.schema.tsx new file mode 100644 index 000000000..c31f692c1 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseBody.schema.tsx @@ -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', + }, + }, + }, + }, +}); diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseBody.view.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseBody.view.tsx new file mode 100644 index 000000000..cae051385 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseBody.view.tsx @@ -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 ( + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseHeaders.view.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseHeaders.view.tsx new file mode 100644 index 000000000..1d1e8a35a --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseHeaders.view.tsx @@ -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 ( + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseTab.item.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseTab.item.tsx new file mode 100644 index 000000000..6891b64e9 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseTab.item.tsx @@ -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: , + }, + { + key: 'responseHeaders', + label: t('Response headers'), + children: , + }, + { + key: 'requestHeaders', + label: t('Request headers'), + children: , + }, + ]; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseTab.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseTab.tsx new file mode 100644 index 000000000..9bab99ea2 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/ResponseTab.tsx @@ -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 = {`HTTP Code:${status}`}; + return ( + + + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/TooltipContainer.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/TooltipContainer.tsx new file mode 100644 index 000000000..55d9bc1c1 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/components/TooltipContainer.tsx @@ -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 {props.children}; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/schemas/getSchemaAction.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/schemas/getSchemaAction.tsx new file mode 100644 index 000000000..fade0b05f --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/schemas/getSchemaAction.tsx @@ -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, + }, + }, + }, + }; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/schemas/getSchemaParam.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/schemas/getSchemaParam.tsx new file mode 100644 index 000000000..9bd6a727b --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/schemas/getSchemaParam.tsx @@ -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; +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/scopes/useCancelAction.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/scopes/useCancelAction.ts new file mode 100644 index 000000000..401e8bfda --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/scopes/useCancelAction.ts @@ -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); + }, + }; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/scopes/useDebugAction.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/scopes/useDebugAction.ts new file mode 100644 index 000000000..06f7f42a8 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/debug-area/scopes/useDebugAction.ts @@ -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); + } + }, + }; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldMethod.schema.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldMethod.schema.tsx new file mode 100644 index 000000000..1000e430f --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldMethod.schema.tsx @@ -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', + }, + ], + }, + }; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldMethod.view.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldMethod.view.tsx new file mode 100644 index 000000000..073dc5f1c --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldMethod.view.tsx @@ -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 ; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldPath.schema.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldPath.schema.tsx new file mode 100644 index 000000000..4c72c74c0 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldPath.schema.tsx @@ -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: ( + + + + ), + 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', + }, + }, + }; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldPath.view.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldPath.view.tsx new file mode 100644 index 000000000..710fe8b30 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/FieldPath.view.tsx @@ -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 ; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/MethodPathComponent.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/MethodPathComponent.tsx new file mode 100644 index 000000000..3864ccd9f --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/method-path/MethodPathComponent.tsx @@ -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 ( + + + + + + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestBody.schema.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestBody.schema.tsx new file mode 100644 index 000000000..37535e78d --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestBody.schema.tsx @@ -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); + } + }, + ], + }, + }, +}); diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestBody.view.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestBody.view.tsx new file mode 100644 index 000000000..a1976e2f3 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestBody.view.tsx @@ -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 ( + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestHeaders.schema.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestHeaders.schema.tsx new file mode 100644 index 000000000..34ee85403 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestHeaders.schema.tsx @@ -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', + }, + }, + }, + }, + }; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestHeaders.view.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestHeaders.view.tsx new file mode 100644 index 000000000..760741063 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestHeaders.view.tsx @@ -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 ( + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestParams.schema.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestParams.schema.tsx new file mode 100644 index 000000000..56bb6d2a4 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestParams.schema.tsx @@ -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', + }, + }, + }, + }, +}); diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestParams.view.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestParams.view.tsx new file mode 100644 index 000000000..5c26a0500 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestParams.view.tsx @@ -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 ( + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestTab.items.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestTab.items.tsx new file mode 100644 index 000000000..6e9d1c3bf --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestTab.items.tsx @@ -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: , + }, + { + key: 'body', + label: t('Body'), + children: , + }, + { + key: 'headers', + label: t('Headers'), + children: , + }, + ]; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestTab.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestTab.tsx new file mode 100644 index 000000000..3da3ba940 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/RequestTab.tsx @@ -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 ; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/debounceClick.util.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/debounceClick.util.ts new file mode 100644 index 000000000..2ae2c5489 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-tab/debounceClick.util.ts @@ -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); diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-transformer/ResponseTransformerComponent.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-transformer/ResponseTransformerComponent.tsx new file mode 100644 index 000000000..3f8823376 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/request-configs/request-transformer/ResponseTransformerComponent.tsx @@ -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 ( + { + setResponseTransformer(value); + setFormValue(form, actionKey, value); + }} + autoSize={{ + minRows: 5, + }} + fieldNames={{ + value: 'name', + label: 'title', + }} + scope={() => useVariableOptions(true)} + /> + ); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/scopes/useVariableOptions.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/scopes/useVariableOptions.ts new file mode 100644 index 000000000..08c1ed830 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/scopes/useVariableOptions.ts @@ -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; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/filterObjectWithMethodAndPath.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/filterObjectWithMethodAndPath.tsx new file mode 100644 index 000000000..535d51cdc --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/filterObjectWithMethodAndPath.tsx @@ -0,0 +1,21 @@ +// 这个函数的目的是筛选出参数对象中的属性,这些属性必须满足两个条件:有一个 method 属性和一个 path 属性。然后,它将这些属性的值作为新对象的键值对 + +export function filterObjectWithMethodAndPath(obj: Record): Record { + 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; +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/filterTree.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/filterTree.tsx new file mode 100644 index 000000000..0a3591ab6 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/filterTree.tsx @@ -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; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/getRequestActions.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/getRequestActions.tsx new file mode 100644 index 000000000..b3e10f1cd --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/getRequestActions.tsx @@ -0,0 +1,3 @@ +export function getRequestActions(sourceMethod) { + return ['list', 'get'].filter((method) => !sourceMethod.includes(method)); +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/getRequestValues.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/getRequestValues.tsx new file mode 100644 index 000000000..c9902837d --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/getRequestValues.tsx @@ -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); +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/responseTransformerAe.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/responseTransformerAe.tsx new file mode 100644 index 000000000..4642e944c --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/responseTransformerAe.tsx @@ -0,0 +1,4 @@ +export const responseTransformerAe = { + data: '{{rawResponse.body}}', + meta: {}, +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/setFormValue.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/setFormValue.tsx new file mode 100644 index 000000000..9570a49b0 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/utils/setFormValue.tsx @@ -0,0 +1,10 @@ +export const setFormValue = async (form, action, value) => { + const { actions } = form.values || {}; + form.setValuesIn('actions', { + ...actions, + [action]: { + ...actions?.[action], + responseTransformer: value, + }, + }); +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/client/forms/msql.tsx b/packages/plugins/@tachybase/plugin-external-data-source/src/client/forms/msql.tsx index d8afc5b36..38695e1bf 100644 --- a/packages/plugins/@tachybase/plugin-external-data-source/src/client/forms/msql.tsx +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/client/forms/msql.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { SchemaComponent } from '@tachybase/client'; -import { tval, usePluginTranslation } from '../locale'; +import { tval, useTranslation } from '../locale'; export const MysqlDataSourceSettingsForm = () => { - const { t } = usePluginTranslation(); + const { t } = useTranslation(); return ( { - const { t } = usePluginTranslation(); + const { t } = useTranslation(); return ( nTval(key, { ns: useCore ? undefined : NAMESPACE }); + export function lang(key: string, options = {}) { return i18n.t(key, { ...options, ns: NAMESPACE }); } -export function tval(key: string) { - return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`; -} -export function usePluginTranslation() { - return useTranslation(NAMESPACE); -} + +export const useTranslation = (useCore = false): any => { + const { i18n } = useApp(); + const t = (key: string, props = {}) => + i18n.t(key, { + ns: useCore ? undefined : NAMESPACE, + ...props, + }); + return { t }; +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/locale/en-US.json b/packages/plugins/@tachybase/plugin-external-data-source/src/locale/en-US.json index fd64840be..b5698062d 100644 --- a/packages/plugins/@tachybase/plugin-external-data-source/src/locale/en-US.json +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/locale/en-US.json @@ -1,12 +1,16 @@ { "Data source name": "Data source name", "Data source display name": "Data source display name", + "Get":"Get(required)", "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", "Database": "Database", "Database connections": "Database connections", "Test Connection": "Test Connection", "Connection successful'": "Connection successful'", + "Create collection": "Create collection", "Display name": "Display name", "Username": "Username", "Password": "Password", @@ -22,5 +26,7 @@ "Postgres": "Postgres", "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." -} \ No newline at end of file + "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" +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/locale/zh-CN.json b/packages/plugins/@tachybase/plugin-external-data-source/src/locale/zh-CN.json index 0e531da37..1410cf334 100644 --- a/packages/plugins/@tachybase/plugin-external-data-source/src/locale/zh-CN.json +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/locale/zh-CN.json @@ -1,12 +1,16 @@ { "Data source name": "数据源标识", "Data source display name": "数据源名称", + "Get":"Get(必填)", "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": "端口", "Database": "数据库", "Database connections": "连接第三方数据库", "Test Connection": "测试连接", "Connection successful'": "连接成功", + "Create collection": "创建数据表", "Display name": "名称", "Username": "用户名", "Password": "密码", @@ -22,5 +26,7 @@ "Postgres":"PostgreSQL", "Enabled the data source":"启用数据源", "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": "请求操作" } diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/.gitkeep b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/plugin.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/plugin.ts new file mode 100644 index 000000000..26c614442 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/plugin.ts @@ -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() {} +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-api-repository.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-api-repository.ts new file mode 100644 index 000000000..f3be12f8b --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-api-repository.ts @@ -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; + } +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-collection-manager.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-collection-manager.ts new file mode 100644 index 000000000..d5e4748b5 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-collection-manager.ts @@ -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); + } +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-collection.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-collection.ts new file mode 100644 index 000000000..99226ecf4 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-collection.ts @@ -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, + }); + } +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-data-source.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-data-source.ts new file mode 100644 index 000000000..430870509 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-data-source.ts @@ -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']); + } +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-request-builder.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-request-builder.ts new file mode 100644 index 000000000..d7a86006c --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/http-request-builder.ts @@ -0,0 +1,3 @@ +export class HttpRequestBuilder { + constructor(dataSource) {} +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/transform-response.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/transform-response.ts new file mode 100644 index 000000000..2efc7b528 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/transform-response.ts @@ -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; +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/type-interface-map.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/type-interface-map.ts new file mode 100644 index 000000000..aa8ee586f --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/type-interface-map.ts @@ -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: '', +}; diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/utils.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/utils.ts new file mode 100644 index 000000000..731f4edf5 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/http/services/utils.ts @@ -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; +} diff --git a/packages/plugins/@tachybase/plugin-external-data-source/src/server/plugin.ts b/packages/plugins/@tachybase/plugin-external-data-source/src/server/plugin.ts index 5289e0315..dd982cadf 100644 --- a/packages/plugins/@tachybase/plugin-external-data-source/src/server/plugin.ts +++ b/packages/plugins/@tachybase/plugin-external-data-source/src/server/plugin.ts @@ -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 { PostgresDataSource } from './pg/postgres-data-source'; export class PluginExternalDataSourceServer extends Plugin { + constructor(app: Application, options?: PluginOptions) { + super(app, options); + this.addFeature(PluginHttpDatasource); + } async afterAdd() {} async beforeLoad() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f88fb37f7..f5f9966e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,10 +133,10 @@ importers: version: 8.4.0(eslint@9.10.0)(typescript@5.4.5) umi: 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: 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: dependencies: @@ -1010,7 +1010,7 @@ importers: version: 5.4.4 umi: 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: dependencies: @@ -2783,6 +2783,9 @@ importers: packages/plugins/@tachybase/plugin-external-data-source: 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': specifier: workspace:* version: link:../../../core/client @@ -2813,6 +2816,9 @@ importers: react-i18next: 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) + 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: '@types/lodash': 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)': 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-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 @@ -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)': 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) '@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) @@ -38420,19 +38426,19 @@ snapshots: uglify-to-browserify@1.0.2: 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: '@babel/runtime': 7.23.6 '@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/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.4)(webpack@5.93.0) + '@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.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/server': 4.3.3 '@umijs/test': 4.3.3(@babel/core@7.22.10) '@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) transitivePeerDependencies: - '@babel/core' @@ -38467,19 +38473,19 @@ snapshots: - webpack-hot-middleware - 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: '@babel/runtime': 7.23.6 '@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/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.5)(webpack@5.93.0) + '@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.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/server': 4.3.3 '@umijs/test': 4.3.3(@babel/core@7.25.2) '@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) transitivePeerDependencies: - '@babel/core' @@ -38835,6 +38841,23 @@ snapshots: string_decoder: 1.3.0 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): dependencies: cac: 6.7.14 @@ -38875,6 +38898,19 @@ snapshots: sass: 1.77.8 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): dependencies: esbuild: 0.20.2 @@ -38935,6 +38971,40 @@ snapshots: - 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.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): dependencies: '@vitest/expect': 1.6.0 diff --git a/tsconfig.json b/tsconfig.json index c4b5c0bbf..64d21ae9a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,7 @@ "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": [ "packages/**/node_modules", "packages/**/dist",