From b275d00c5a1df8c5a96b21b6940f2f6556c59307 Mon Sep 17 00:00:00 2001 From: Katherine Date: Mon, 1 Jun 2026 10:18:27 +0800 Subject: [PATCH 1/3] docs(plugin-data-source-manager): add v2 extension guide (#9634) * docs(plugin-data-source-manager): add v2 extension guide * docs: data source v2 * refactor(plugin-data-source-manager): split v2 collection templates * fix(plugin-data-source-manager): align v2 settings routes --- .../plugin-collection-sql/client-v2.d.ts | 2 + .../plugin-collection-sql/client-v2.js | 3 + .../plugin-collection-sql/package.json | 3 + .../src/client-v2}/SqlCollectionConfigure.tsx | 134 +++- .../src/client-v2/index.tsx | 12 + .../src/client-v2/plugin.tsx | 72 +++ .../plugin-collection-tree/client-v2.d.ts | 2 + .../plugin-collection-tree/client-v2.js | 3 + .../plugin-collection-tree/package.json | 2 + .../src/client-v2/index.tsx | 11 + .../src/client-v2/plugin.tsx | 112 ++++ .../src/client-v2/README.md | 583 ++++++++++++++++++ .../src/client-v2/locale.ts | 1 + .../pages/DataSourceCollectionsPage.tsx | 8 +- .../src/client-v2/pages/DataSourcesPage.tsx | 12 +- .../client-v2/pages/components/FieldsPage.tsx | 182 ++---- .../src/client-v2/plugin.tsx | 166 +---- 17 files changed, 1005 insertions(+), 303 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-collection-sql/client-v2.d.ts create mode 100644 packages/plugins/@nocobase/plugin-collection-sql/client-v2.js rename packages/plugins/@nocobase/{plugin-data-source-manager/src/client-v2/pages/components => plugin-collection-sql/src/client-v2}/SqlCollectionConfigure.tsx (86%) create mode 100644 packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/index.tsx create mode 100644 packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/plugin.tsx create mode 100644 packages/plugins/@nocobase/plugin-collection-tree/client-v2.d.ts create mode 100644 packages/plugins/@nocobase/plugin-collection-tree/client-v2.js create mode 100644 packages/plugins/@nocobase/plugin-collection-tree/src/client-v2/index.tsx create mode 100644 packages/plugins/@nocobase/plugin-collection-tree/src/client-v2/plugin.tsx create mode 100644 packages/plugins/@nocobase/plugin-data-source-manager/src/client-v2/README.md diff --git a/packages/plugins/@nocobase/plugin-collection-sql/client-v2.d.ts b/packages/plugins/@nocobase/plugin-collection-sql/client-v2.d.ts new file mode 100644 index 00000000000..f12b5649835 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-collection-sql/client-v2.d.ts @@ -0,0 +1,2 @@ +export * from './dist/client-v2'; +export { default } from './dist/client-v2'; diff --git a/packages/plugins/@nocobase/plugin-collection-sql/client-v2.js b/packages/plugins/@nocobase/plugin-collection-sql/client-v2.js new file mode 100644 index 00000000000..3d4c34c3f79 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-collection-sql/client-v2.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./dist/client-v2/index.js'); diff --git a/packages/plugins/@nocobase/plugin-collection-sql/package.json b/packages/plugins/@nocobase/plugin-collection-sql/package.json index e9babd525f5..c98ab54d058 100644 --- a/packages/plugins/@nocobase/plugin-collection-sql/package.json +++ b/packages/plugins/@nocobase/plugin-collection-sql/package.json @@ -14,6 +14,9 @@ "license": "Apache-2.0", "peerDependencies": { "@nocobase/client": "2.x", + "@nocobase/client-v2": "2.x", + "@nocobase/flow-engine": "2.x", + "@nocobase/plugin-data-source-manager": "2.x", "@nocobase/server": "2.x", "@nocobase/test": "2.x" }, diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client-v2/pages/components/SqlCollectionConfigure.tsx b/packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/SqlCollectionConfigure.tsx similarity index 86% rename from packages/plugins/@nocobase/plugin-data-source-manager/src/client-v2/pages/components/SqlCollectionConfigure.tsx rename to packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/SqlCollectionConfigure.tsx index 387946d464a..ee5babc1c7c 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client-v2/pages/components/SqlCollectionConfigure.tsx +++ b/packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/SqlCollectionConfigure.tsx @@ -8,15 +8,19 @@ */ import { EditOutlined, RightSquareOutlined } from '@ant-design/icons'; +import { DrawerFormLayout } from '@nocobase/client-v2'; import { randomId, useFlowContext } from '@nocobase/flow-engine'; +import type { + CollectionTemplateConfigureItemProps, + CollectionTemplateSyncFieldsProps, +} from '@nocobase/plugin-data-source-manager/client-v2'; import { useRequest } from 'ahooks'; -import { Alert, Button, Cascader, Form, Input, Select, Spin, Table, Tag } from 'antd'; +import { Alert, App, Button, Cascader, Form, Input, Select, Space, Spin, Table, Tag } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; import { get } from 'lodash'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import type { CollectionTemplateConfigureItemProps } from '../../plugin'; -import { compileLegacyTemplate } from '../../utils/compileLegacyTemplate'; +import { compileLegacyTemplate } from '@nocobase/plugin-data-source-manager/client-v2'; export type SqlPreviewField = { collection?: string; @@ -247,6 +251,27 @@ function getSqlFieldInterfaceOptions(groups: ReturnType group.options.length); } +function getSqlSyncErrorMessage(error: unknown, fallback: string) { + const typedError = error as + | { + message?: string; + response?: { + data?: { + errors?: Array<{ message?: string }>; + }; + }; + } + | undefined; + return ( + typedError?.response?.data?.errors + ?.map((item) => item.message) + .filter(Boolean) + .join('\n') || + typedError?.message || + fallback + ); +} + export function buildSqlFieldsFromPreview(options: { currentFields?: SqlFieldRecord[]; manager?: FieldInterfaceManager; @@ -339,7 +364,7 @@ function SqlStatementControl(props: { onChange?.(event.target.value); }} /> - + - + ); } @@ -791,3 +816,102 @@ export function normalizeSqlCollectionSubmitValues(values: Record { + form.setFieldsValue(props.collection); + }, [form, props.collection]); + + useEffect(() => { + let ignore = false; + + const loadPreview = async () => { + const sql = props.collection.sql; + if (!sql) { + return; + } + + try { + const response = await ctx.api.resource('sqlCollection').execute({ + values: { + sql, + }, + }); + if (ignore) { + return; + } + const preview = normalizeSqlPreviewResult(response); + const currentSources = props.collection.sources || []; + const currentFields = props.collection.fields || []; + form.setFieldsValue({ + sources: Array.from( + new Set([ + ...(Array.isArray(currentSources) ? currentSources : []), + ...(Array.isArray(preview.sources) ? preview.sources : []), + ]), + ), + fields: buildSqlFieldsFromPreview({ + currentFields: Array.isArray(currentFields) ? currentFields : [], + manager: ctx.dataSourceManager.collectionFieldInterfaceManager, + preview, + }), + [previewName]: preview, + }); + } catch (error) { + if (!ignore) { + form.setFieldValue(previewName, { + error: getSqlSyncErrorMessage(error, t('SQL error')), + }); + } + } + }; + + loadPreview(); + + return () => { + ignore = true; + }; + }, [ctx.api, ctx.dataSourceManager.collectionFieldInterfaceManager, form, previewName, props.collection, t]); + + const handleSubmit = useCallback(async () => { + const values = await form.validateFields(); + setSubmitting(true); + try { + await ctx.api.resource('sqlCollection').setFields({ + filterByTk: props.collection.name, + values: { + fields: values.fields, + sources: values.sources, + }, + }); + await ctx.dataSourceManager.getDataSource(props.dataSourceKey)?.reload(); + props.onSubmitted(); + message.success(t('Sync successfully')); + } finally { + setSubmitting(false); + } + }, [ctx.api, ctx.dataSourceManager, form, message, props, t]); + + return ( + +
+ + + + +
+ ); +} diff --git a/packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/index.tsx b/packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/index.tsx new file mode 100644 index 00000000000..2c296e99d0f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/index.tsx @@ -0,0 +1,12 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export { default } from './plugin'; +export * from './plugin'; +export * from './SqlCollectionConfigure'; diff --git a/packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/plugin.tsx b/packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/plugin.tsx new file mode 100644 index 00000000000..6847f198e49 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-collection-sql/src/client-v2/plugin.tsx @@ -0,0 +1,72 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Application, Plugin } from '@nocobase/client-v2'; +import type { PluginDataSourceManagerClientV2 } from '@nocobase/plugin-data-source-manager/client-v2'; +import { + normalizeSqlCollectionSubmitValues, + SqlFieldsConfigureItem, + SqlPreviewConfigureItem, + SqlSourceCollectionsConfigureItem, + SqlStatementConfigureItem, + SqlSyncFieldsDrawer, +} from './SqlCollectionConfigure'; + +export class PluginCollectionSqlClientV2 extends Plugin { + async load() { + const dataSourceManager = (this.app.pm.get('@nocobase/plugin-data-source-manager') || + this.app.pm.get('data-source-manager')) as PluginDataSourceManagerClientV2 | undefined; + + dataSourceManager?.registerCollectionTemplate?.({ + name: 'sql', + title: '{{t("SQL collection")}}', + order: 40, + color: 'yellow', + divider: true, + collection: { + options: { + template: 'sql', + }, + fields: [], + }, + capabilities: { + recordUniqueKey: true, + }, + configure: { + items: [ + { + name: 'sql', + Component: SqlStatementConfigureItem, + required: true, + }, + { + name: 'sources', + Component: SqlSourceCollectionsConfigureItem, + }, + { + name: 'fields', + Component: SqlFieldsConfigureItem, + required: true, + }, + { + name: 'preview', + Component: SqlPreviewConfigureItem, + }, + ], + syncFields: { + visible: true, + Component: SqlSyncFieldsDrawer, + }, + transformSubmitValues: normalizeSqlCollectionSubmitValues, + }, + }); + } +} + +export default PluginCollectionSqlClientV2; diff --git a/packages/plugins/@nocobase/plugin-collection-tree/client-v2.d.ts b/packages/plugins/@nocobase/plugin-collection-tree/client-v2.d.ts new file mode 100644 index 00000000000..f12b5649835 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-collection-tree/client-v2.d.ts @@ -0,0 +1,2 @@ +export * from './dist/client-v2'; +export { default } from './dist/client-v2'; diff --git a/packages/plugins/@nocobase/plugin-collection-tree/client-v2.js b/packages/plugins/@nocobase/plugin-collection-tree/client-v2.js new file mode 100644 index 00000000000..3d4c34c3f79 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-collection-tree/client-v2.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./dist/client-v2/index.js'); diff --git a/packages/plugins/@nocobase/plugin-collection-tree/package.json b/packages/plugins/@nocobase/plugin-collection-tree/package.json index 4f2bd470e55..c7864cb54ad 100644 --- a/packages/plugins/@nocobase/plugin-collection-tree/package.json +++ b/packages/plugins/@nocobase/plugin-collection-tree/package.json @@ -13,6 +13,8 @@ "main": "dist/server/index.js", "peerDependencies": { "@nocobase/client": "2.x", + "@nocobase/client-v2": "2.x", + "@nocobase/plugin-data-source-manager": "2.x", "@nocobase/server": "2.x", "@nocobase/test": "2.x" }, diff --git a/packages/plugins/@nocobase/plugin-collection-tree/src/client-v2/index.tsx b/packages/plugins/@nocobase/plugin-collection-tree/src/client-v2/index.tsx new file mode 100644 index 00000000000..0c0477d9a5b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-collection-tree/src/client-v2/index.tsx @@ -0,0 +1,11 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export { default } from './plugin'; +export * from './plugin'; diff --git a/packages/plugins/@nocobase/plugin-collection-tree/src/client-v2/plugin.tsx b/packages/plugins/@nocobase/plugin-collection-tree/src/client-v2/plugin.tsx new file mode 100644 index 00000000000..00bb241770a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-collection-tree/src/client-v2/plugin.tsx @@ -0,0 +1,112 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Application, Plugin } from '@nocobase/client-v2'; +import type { CollectionTemplateField } from '@nocobase/client-v2'; +import type { PluginDataSourceManagerClientV2 } from '@nocobase/plugin-data-source-manager/client-v2'; + +function getTreeCollectionFields(): CollectionTemplateField[] { + return [ + { + interface: 'integer', + name: 'parentId', + type: 'bigInt', + title: '{{t("Parent ID")}}', + isForeignKey: true, + uiSchema: { + 'x-read-pretty': true, + }, + }, + { + interface: 'm2o', + type: 'belongsTo', + name: 'parent', + title: '{{t("Parent")}}', + foreignKey: 'parentId', + treeParent: true, + onDelete: 'CASCADE', + componentProps: { + multiple: false, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + { + interface: 'o2m', + type: 'hasMany', + name: 'children', + title: '{{t("Children")}}', + foreignKey: 'parentId', + treeChildren: true, + onDelete: 'CASCADE', + componentProps: { + multiple: true, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + ]; +} + +function normalizeTreeCollectionSubmitValues(values: Record) { + if (!Array.isArray(values.fields)) { + return; + } + + values.fields = values.fields.map((field) => { + if (!field.target && ['belongsToMany', 'belongsTo', 'hasMany', 'hasOne'].includes(field.type)) { + return { ...field, target: values.name }; + } + return field; + }); + + const primaryKey = values.fields.find((field) => field.primaryKey); + const parentId = values.fields.find((field) => field.name === 'parentId' && field.isForeignKey); + if (primaryKey && parentId) { + const parentIdTitle = parentId.uiSchema?.title; + parentId.interface = primaryKey.interface; + parentId.type = primaryKey.type; + parentId.uiSchema = structuredClone(primaryKey.uiSchema || {}); + parentId.uiSchema.title = parentIdTitle; + parentId.autoFill = false; + } +} + +export class PluginCollectionTreeClientV2 extends Plugin { + async load() { + const dataSourceManager = (this.app.pm.get('@nocobase/plugin-data-source-manager') || + this.app.pm.get('data-source-manager')) as PluginDataSourceManagerClientV2 | undefined; + + dataSourceManager?.registerCollectionTemplate?.({ + name: 'tree', + title: '{{t("Tree collection")}}', + order: 30, + color: 'blue', + collection: { + options: { + template: 'tree', + tree: 'adjacencyList', + }, + fields: getTreeCollectionFields, + }, + presetFields: { + disabledIncludes: ['id'], + }, + configure: { + transformSubmitValues: normalizeTreeCollectionSubmitValues, + }, + }); + } +} + +export default PluginCollectionTreeClientV2; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client-v2/README.md b/packages/plugins/@nocobase/plugin-data-source-manager/src/client-v2/README.md new file mode 100644 index 00000000000..688cf77c49f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client-v2/README.md @@ -0,0 +1,583 @@ +# v2 Data Source Manager 扩展说明 + +本文档说明 `plugin-data-source-manager/src/client-v2` 当前提供的扩展点,主要面向需要扩展外部数据源、字段 Interface、数据表模板的插件开发者。 + +v2 Data Source Manager 的实现原则是: + +- 管理页使用 client-v2 / Ant Design 组件实现,不再依赖 v1 SchemaComponent / Formily 页面搭建方式。 +- 页面跳转和抽屉统一使用 v2 的 `ctx.viewer` / `ctx.router`。 +- 字段 Interface 和数据表模板由各自插件注册,Data Source Manager 只负责读取注册信息并渲染通用管理界面。 +- v1 兼容入口仍存在,但新增 v2 扩展应优先使用本文档中的 v2 注册结构。 + +## 关键入口 + +- 插件入口:`plugin.tsx` +- 外部数据源表单:`components/DataSourceForm.tsx` +- 数据源列表页:`pages/DataSourcesPage.tsx` +- 数据表列表和创建/编辑:`pages/components/CollectionsPage.tsx` +- 字段列表和创建/编辑:`pages/components/FieldsPage.tsx`、`pages/components/FieldForm.tsx` +- 字段 Interface 配置类型:`field-interfaces/index.ts` +- 数据表模板字段归一化:`packages/core/client-v2/src/collection-manager/template-fields.ts` +- 字段配置项通用工具:`packages/core/client-v2/src/collection-manager/field-configure.ts` + +## 1. 扩展外部数据源 + +外部数据源类型由数据源插件在 client-v2 中注册: + +```tsx +import React from 'react'; +import { Form, Input, Select } from 'antd'; +import { Plugin } from '@nocobase/client-v2'; +import PluginDataSourceManagerClientV2 from '@nocobase/plugin-data-source-manager/client-v2'; + +function MyDataSourceSettingsForm(props) { + return ( + <> + + + + + + + + onChange?.(ev.target.value)} + disabled={disabled} + placeholder={placeholder} + {...typedProps} + /> + ); + case 'number': + return ( + onChange?.(next)} + disabled={disabled} + placeholder={placeholder} + style={{ width: '100%' }} + {...typedProps} + /> + ); + case 'boolean': + return ( + `} readOnly disabled={disabled} style={{ width: '100%' }} /> + ) : ( + renderConstantEditor(constantTypeForRendering, value, onChange, { + typedProps, + disabled, + placeholder, + t, + }) + )} + + + options={switcherOptions} + onChange={onSwitcherChange} + disabled={disabled} + changeOnSelect + > + + + + ); +} + +export default TypedVariableInput; diff --git a/packages/core/client-v2/src/components/form/__tests__/TypedVariableInput.test.tsx b/packages/core/client-v2/src/components/form/__tests__/TypedVariableInput.test.tsx new file mode 100644 index 00000000000..3f318c7f2fe --- /dev/null +++ b/packages/core/client-v2/src/components/form/__tests__/TypedVariableInput.test.tsx @@ -0,0 +1,152 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { FlowContext, FlowContextProvider } from '@nocobase/flow-engine'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { TypedVariableInput } from '../TypedVariableInput'; + +function createContextWithEnv() { + const ctx = new FlowContext(); + (ctx as any).t = (key: string) => key; + + ctx.defineProperty('$env', { + value: { SMTP_PORT: 465, SECURE_FLAG: true }, + meta: { + title: 'Env', + type: 'object', + properties: { + SMTP_PORT: { title: 'SMTP Port', type: 'number' }, + SECURE_FLAG: { title: 'Secure flag', type: 'boolean' }, + }, + }, + }); + + return ctx; +} + +function renderWithCtx(ctx: FlowContext, node: React.ReactNode) { + return render({node}); +} + +describe('TypedVariableInput - constant rendering', () => { + it('renders an InputNumber for a numeric value when types=[number]', async () => { + const ctx = createContextWithEnv(); + renderWithCtx( + ctx, + undefined} + />, + ); + const numberInput = await screen.findByDisplayValue('465'); + expect(numberInput).toBeInTheDocument(); + expect(numberInput.getAttribute('role')).toBe('spinbutton'); + }); + + it('renders a boolean Select when value is true and types=[boolean]', async () => { + const ctx = createContextWithEnv(); + renderWithCtx( + ctx, + undefined} />, + ); + await waitFor(() => { + // antd Select renders the chosen item label via a selection-item span + const item = screen.getByText('True'); + expect(item).toBeInTheDocument(); + }); + }); + + it('renders the Null placeholder when value=null and nullable=true', async () => { + const ctx = createContextWithEnv(); + renderWithCtx( + ctx, + undefined} />, + ); + // Uses placeholder slot (not value) so antd's grey placeholder colour + // applies — see comment in TypedVariableInput.tsx. + const nullInput = await screen.findByPlaceholderText(''); + expect(nullInput).toBeInTheDocument(); + expect(nullInput.getAttribute('readonly')).not.toBeNull(); + }); +}); + +describe('TypedVariableInput - variable rendering', () => { + it('renders a labelled pill when value is a {{ $env.X }} expression', async () => { + const ctx = createContextWithEnv(); + renderWithCtx( + ctx, + undefined} + />, + ); + await waitFor(() => { + const tag = screen.getByRole('button', { name: 'variable-tag' }); + expect(tag.textContent).toContain('Env'); + expect(tag.textContent).toContain('SMTP Port'); + }); + }); + + it('clears back to null when the close button is clicked (nullable=true)', async () => { + const ctx = createContextWithEnv(); + const handleChange = vi.fn(); + renderWithCtx( + ctx, + , + ); + const clear = await screen.findByRole('button', { name: 'icon-close' }); + fireEvent.click(clear); + expect(handleChange).toHaveBeenCalledWith(null); + }); + + it('clears back to default-of-first-type when nullable=false', async () => { + const ctx = createContextWithEnv(); + const handleChange = vi.fn(); + renderWithCtx( + ctx, + , + ); + const clear = await screen.findByRole('button', { name: 'icon-close' }); + fireEvent.click(clear); + expect(handleChange).toHaveBeenCalledWith(0); + }); +}); + +describe('TypedVariableInput - editor onChange propagation', () => { + it('forwards numeric edits via onChange', async () => { + const ctx = createContextWithEnv(); + const handleChange = vi.fn(); + renderWithCtx(ctx, ); + const numberInput = await screen.findByDisplayValue('465'); + fireEvent.change(numberInput, { target: { value: '587' } }); + // antd InputNumber may emit on blur; force blur to flush + fireEvent.blur(numberInput); + expect(handleChange).toHaveBeenCalled(); + const lastCall = handleChange.mock.calls[handleChange.mock.calls.length - 1][0]; + expect(lastCall).toBe(587); + }); +}); diff --git a/packages/core/client-v2/src/components/form/index.tsx b/packages/core/client-v2/src/components/form/index.tsx index 00b5f5a3dc4..1be2a9152ae 100644 --- a/packages/core/client-v2/src/components/form/index.tsx +++ b/packages/core/client-v2/src/components/form/index.tsx @@ -16,4 +16,5 @@ export * from './FileSizeInput'; export * from './JsonTextArea'; export * from './PasswordInput'; export * from './RemoteSelect'; +export * from './TypedVariableInput'; export * from './VariableInput'; diff --git a/packages/plugins/@nocobase/plugin-notification-email/src/client-v2/forms/ChannelConfigForm.tsx b/packages/plugins/@nocobase/plugin-notification-email/src/client-v2/forms/ChannelConfigForm.tsx index 3aa52e8065a..d024a7b4c38 100644 --- a/packages/plugins/@nocobase/plugin-notification-email/src/client-v2/forms/ChannelConfigForm.tsx +++ b/packages/plugins/@nocobase/plugin-notification-email/src/client-v2/forms/ChannelConfigForm.tsx @@ -7,8 +7,8 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { EnvVariableInput } from '@nocobase/client-v2'; -import { Col, Form, InputNumber, Row, Select } from 'antd'; +import { EnvVariableInput, TypedVariableInput } from '@nocobase/client-v2'; +import { Col, Form, Row, Select } from 'antd'; import React from 'react'; import { useNotificationEmailTranslation } from '../locale'; @@ -27,7 +27,7 @@ export function ChannelConfigForm() { - + - + - + @@ -54,12 +54,7 @@ export function ChannelConfigForm() { initialValue={true} extra={t('In most cases, if using port 465, set it to true; otherwise, set it to false.')} > -