(name: string, of?: any): R;
+}
+```
+
+示例
+
+```ts
+import axios from 'axios';
+
+// 不传参时,内部直接创建 axios 实例
+const apiClient = new APIClient();
+
+// 提供 AxiosRequestConfig 配置参数
+const apiClient = new APIClient({
+ baseURL: '',
+});
+
+// 提供 AxiosInstance
+const instance = axios.create({
+ baseURL: '',
+});
+const apiClient = new APIClient(instance);
+
+// 常规请求
+const response = await apiClient.request({ url });
+
+// NocoBase 特有的资源操作
+const response = await apiClient.resource('posts').list();
+
+// 请求共享
+const { data, loading, run } = apiClient.service('uid');
+```
+
+`api.service(uid)` 的例子,ComponentB 里刷新 ComponentA 的请求数据
+
+
+
+## APIClientProvider
+
+提供 APIClient 实例的上下文。
+
+```tsx | pure
+const apiClient = new APIClient();
+
+
+```
+
+## useAPIClient
+
+获取当前上下文的 APIClient 实例。
+
+```ts
+const apiClient = useAPIClient();
+```
+
+## useRequest
+
+```ts
+function useRequest(
+ service: AxiosRequestConfig
| ResourceActionOptions
| FunctionService,
+ options?: Options,
+);
+```
+
+支持 `axios.request(config)`,config 详情查看 [axios](https://github.com/axios/axios#request-config)
+
+```ts
+const { data, loading, refresh, run, params } = useRequest({ url: '/users' });
+
+// useRequest 里传的是 AxiosRequestConfig,所以 run 里传的也是 AxiosRequestConfig
+run({
+ params: {
+ pageSize: 20,
+ }
+});
+```
+
+例子如下:
+
+
+
+或者是 NocoBase 的 resource & action 请求:
+
+```ts
+const { data, run } = useRequest({
+ resource: 'users',
+ action: 'list',
+ params: {
+ pageSize: 20,
+ },
+});
+
+// useRequest 传的是 ResourceActionOptions,所以 run 直接传 action params 就可以了。
+run({
+ pageSize: 50,
+});
+```
+
+例子如下:
+
+
+
+也可以是自定义的异步函数:
+
+```ts
+const { data, loading, run, refresh, params } = useRequest((...params) => Promise.resolve({}));
+
+run(...params);
+```
+
+更多用法查看 ahooks 的 [useRequest()](https://ahooks.js.org/hooks/use-request/index)
+
+## useResource
+
+```ts
+function useResource(name: string, of?: string | number): IResource;
+```
+
+资源是 NocoBase 的核心概念,包括:
+
+- 独立资源,如 `posts`
+- 关系资源,如 `posts.tags` `posts.user` `posts.comments`
+
+资源 URI
+
+```bash
+# 独立资源,文章
+/api/posts
+# 关系资源,文章 ID=1 的评论
+/api/posts/1/comments
+```
+
+通过 APIClient 获取资源
+
+```ts
+const api = new APIClient();
+
+api.resource('posts');
+api.resource('posts.comments', 1);
+```
+
+useResource 用法:
+
+```ts
+const resource = useResource('posts');
+const resource = useResource('posts.comments', 1);
+```
+
+resource 的实际场景用例参见:
+
+- [useCollection()](collection-manager#usecollection)
+- [useCollectionField()](collection-manager#usecollectionfield)
diff --git a/packages/core/client/docs/zh-CN/core/data-block/data-block-provider.md b/packages/core/client/docs/zh-CN/core/data-block/data-block-provider.md
index 6aeaec0492..1ffc718afd 100644
--- a/packages/core/client/docs/zh-CN/core/data-block/data-block-provider.md
+++ b/packages/core/client/docs/zh-CN/core/data-block/data-block-provider.md
@@ -37,7 +37,7 @@ Table 中的字段信息及列表数据,都是存储在数据库中的。
- `DataBlockProvider`:封装了下面的所有组件,并提供了区块属性
- [CollectionProvider](/core/data-source/collection-provider) / [AssociationProvider](/core/data-source/association-provider): 根据 `DataBlockProvider` 提供的上下文信息,查询对应数据表数据及关系字段信息并传递
- - [BlockResourceProvider](/core/data-block/data-block-resource-provider): 根据 `DataBlockProvider` 提供的上下文信息,构建区块 [Resource](https://docs.nocobase.com/api/sdk#resource-action) API,用于区块数据的增删改查
+ - [BlockResourceProvider](/core/data-block/data-block-resource-provider): 根据 `DataBlockProvider` 提供的上下文信息,构建区块 [Resource](/core/request) API,用于区块数据的增删改查
- [BlockRequestProvider](/core/data-block/data-block-request-provider): 根据 `DataBlockProvider` 提供的上下文信息,自动调用 `BlockResourceProvider` 提供的 `resource.get()` 或 `resource.list()` 发起请求,得到区块数据,并传递
- [CollectionRecordProvider](/core/data-source/record-provider): 对于 `resource.get()` 场景,会自动嵌套 `CollectionRecordProvider` 并将 `resource.get()` 请求结果传递下去,`resource.list()` 场景则需要自行使用 `CollectionRecordProvider` 提供数据记录
diff --git a/packages/core/client/docs/zh-CN/core/data-block/data-block-resource-provider.md b/packages/core/client/docs/zh-CN/core/data-block/data-block-resource-provider.md
index e94246b6c7..50178dc86f 100644
--- a/packages/core/client/docs/zh-CN/core/data-block/data-block-resource-provider.md
+++ b/packages/core/client/docs/zh-CN/core/data-block/data-block-resource-provider.md
@@ -1,6 +1,6 @@
# DataBlockResourceProvider
-根据 `DataBlockProvider` 中的 `collection`、`association`、`sourceId` 等属性,构建好 [resource](https://docs.nocobase.com/api/sdk#resource-action) 对象,方便子组件对区块数据的增删改查操作,其内置在 [DataBlockProvider](/core/data-block/data-block-provider) 中
+根据 `DataBlockProvider` 中的 `collection`、`association`、`sourceId` 等属性,构建好 [resource](/core/request) 对象,方便子组件对区块数据的增删改查操作,其内置在 [DataBlockProvider](/core/data-block/data-block-provider) 中
## useDataBlockResource
diff --git a/packages/core/client/docs/zh-CN/core/request/demos/demo1.tsx b/packages/core/client/docs/zh-CN/core/request/demos/demo1.tsx
new file mode 100644
index 0000000000..dbc18123d2
--- /dev/null
+++ b/packages/core/client/docs/zh-CN/core/request/demos/demo1.tsx
@@ -0,0 +1,24 @@
+import { APIClient, APIClientProvider, compose, useRequest } from '@nocobase/client';
+import MockAdapter from 'axios-mock-adapter';
+import React from 'react';
+
+const apiClient = new APIClient();
+
+const mock = new MockAdapter(apiClient.axios);
+
+mock.onGet('/users:get').reply(200, {
+ data: { id: 1, name: 'John Smith' },
+});
+
+const providers = [[APIClientProvider, { apiClient }]];
+
+export default compose(...providers)(() => {
+ const { data } = useRequest<{
+ data: any;
+ }>({
+ resource: 'users',
+ action: 'get',
+ params: {},
+ });
+ return {data?.data?.name}
;
+});
diff --git a/packages/core/client/docs/zh-CN/core/request/demos/demo2.tsx b/packages/core/client/docs/zh-CN/core/request/demos/demo2.tsx
new file mode 100644
index 0000000000..823a5dbb41
--- /dev/null
+++ b/packages/core/client/docs/zh-CN/core/request/demos/demo2.tsx
@@ -0,0 +1,23 @@
+import { APIClient, APIClientProvider, compose, useRequest } from '@nocobase/client';
+import MockAdapter from 'axios-mock-adapter';
+import React from 'react';
+
+const apiClient = new APIClient();
+
+const mock = new MockAdapter(apiClient.axios);
+
+mock.onGet('/users:get').reply(200, {
+ data: { id: 1, name: 'John Smith' },
+});
+
+const providers = [[APIClientProvider, { apiClient }]];
+
+export default compose(...providers)(() => {
+ const { data } = useRequest<{
+ data: any;
+ }>({
+ url: 'users:get',
+ method: 'get',
+ });
+ return {data?.data?.name}
;
+});
diff --git a/packages/core/client/docs/zh-CN/core/request/demos/demo3.tsx b/packages/core/client/docs/zh-CN/core/request/demos/demo3.tsx
new file mode 100644
index 0000000000..d27bf0470f
--- /dev/null
+++ b/packages/core/client/docs/zh-CN/core/request/demos/demo3.tsx
@@ -0,0 +1,69 @@
+import { uid } from '@formily/shared';
+import { APIClient, APIClientProvider, useAPIClient, useRequest } from '@nocobase/client';
+import { Button, Input, Space, Table } from 'antd';
+import MockAdapter from 'axios-mock-adapter';
+import React from 'react';
+
+const apiClient = new APIClient();
+
+const mock = new MockAdapter(apiClient.axios);
+const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
+
+mock.onGet('/users:list').reply(async () => {
+ await sleep(1000);
+ return [
+ 200,
+ {
+ data: [
+ { id: 1, name: uid() },
+ { id: 2, name: uid() },
+ ],
+ },
+ ];
+});
+
+const ComponentA = () => {
+ console.log('ComponentA');
+ const { data, loading } = useRequest<{
+ data: any;
+ }>(
+ {
+ url: 'users:list',
+ method: 'get',
+ },
+ {
+ uid: 'test', // 当指定了 uid 的 useRequest 的结果,可以通过 api.service(uid) 获取
+ },
+ );
+ return (
+
+ );
+};
+
+const ComponentB = () => {
+ console.log('ComponentB');
+ const apiClient = useAPIClient();
+ return (
+
+
+
+
+ );
+};
+
+export default () => {
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/core/client/docs/zh-CN/core/request/index.md b/packages/core/client/docs/zh-CN/core/request/index.md
new file mode 100644
index 0000000000..d442a20194
--- /dev/null
+++ b/packages/core/client/docs/zh-CN/core/request/index.md
@@ -0,0 +1,167 @@
+# APIClient
+
+## APIClient
+
+```ts
+class APIClient {
+ // axios 实例
+ axios: AxiosInstance;
+ // 缓存带 uid 的 useRequest({}, {uid}) 的结果,可供其他组件调用
+ services: Record>;
+ // 构造器
+ constructor(instance?: AxiosInstance | AxiosRequestConfig);
+ // 客户端请求,支持 AxiosRequestConfig 和 ResourceActionOptions
+ request, D = any>(config: AxiosRequestConfig | ResourceActionOptions): Promise;
+ // 获取资源
+ resource(name: string, of?: any): R;
+}
+```
+
+示例
+
+```ts
+import axios from 'axios';
+
+// 不传参时,内部直接创建 axios 实例
+const apiClient = new APIClient();
+
+// 提供 AxiosRequestConfig 配置参数
+const apiClient = new APIClient({
+ baseURL: '',
+});
+
+// 提供 AxiosInstance
+const instance = axios.create({
+ baseURL: '',
+});
+const apiClient = new APIClient(instance);
+
+// 常规请求
+const response = await apiClient.request({ url });
+
+// NocoBase 特有的资源操作
+const response = await apiClient.resource('posts').list();
+
+// 请求共享
+const { data, loading, run } = apiClient.service('uid');
+```
+
+`api.service(uid)` 的例子,ComponentB 里刷新 ComponentA 的请求数据
+
+
+
+## APIClientProvider
+
+提供 APIClient 实例的上下文。
+
+```tsx | pure
+const apiClient = new APIClient();
+
+
+```
+
+## useAPIClient
+
+获取当前上下文的 APIClient 实例。
+
+```ts
+const apiClient = useAPIClient();
+```
+
+## useRequest
+
+```ts
+function useRequest(
+ service: AxiosRequestConfig
| ResourceActionOptions
| FunctionService,
+ options?: Options,
+);
+```
+
+支持 `axios.request(config)`,config 详情查看 [axios](https://github.com/axios/axios#request-config)
+
+```ts
+const { data, loading, refresh, run, params } = useRequest({ url: '/users' });
+
+// useRequest 里传的是 AxiosRequestConfig,所以 run 里传的也是 AxiosRequestConfig
+run({
+ params: {
+ pageSize: 20,
+ }
+});
+```
+
+例子如下:
+
+
+
+或者是 NocoBase 的 resource & action 请求:
+
+```ts
+const { data, run } = useRequest({
+ resource: 'users',
+ action: 'list',
+ params: {
+ pageSize: 20,
+ },
+});
+
+// useRequest 传的是 ResourceActionOptions,所以 run 直接传 action params 就可以了。
+run({
+ pageSize: 50,
+});
+```
+
+例子如下:
+
+
+
+也可以是自定义的异步函数:
+
+```ts
+const { data, loading, run, refresh, params } = useRequest((...params) => Promise.resolve({}));
+
+run(...params);
+```
+
+更多用法查看 ahooks 的 [useRequest()](https://ahooks.js.org/hooks/use-request/index)
+
+## useResource
+
+```ts
+function useResource(name: string, of?: string | number): IResource;
+```
+
+资源是 NocoBase 的核心概念,包括:
+
+- 独立资源,如 `posts`
+- 关系资源,如 `posts.tags` `posts.user` `posts.comments`
+
+资源 URI
+
+```bash
+# 独立资源,文章
+/api/posts
+# 关系资源,文章 ID=1 的评论
+/api/posts/1/comments
+```
+
+通过 APIClient 获取资源
+
+```ts
+const api = new APIClient();
+
+api.resource('posts');
+api.resource('posts.comments', 1);
+```
+
+useResource 用法:
+
+```ts
+const resource = useResource('posts');
+const resource = useResource('posts.comments', 1);
+```
+
+resource 的实际场景用例参见:
+
+- [useCollection()](collection-manager#usecollection)
+- [useCollectionField()](collection-manager#usecollectionfield)
diff --git a/packages/core/client/src/api-client/index.md b/packages/core/client/src/api-client/index.md
index e4c67d46d3..1804df06b2 100644
--- a/packages/core/client/src/api-client/index.md
+++ b/packages/core/client/src/api-client/index.md
@@ -166,8 +166,3 @@ useResource 用法:
const resource = useResource('posts');
const resource = useResource('posts.comments', 1);
```
-
-resource 的实际场景用例参见:
-
-- [useCollection()](collection-manager#usecollection)
-- [useCollectionField()](collection-manager#usecollectionfield)
diff --git a/packages/core/client/src/application/__tests__/Application.test.tsx b/packages/core/client/src/application/__tests__/Application.test.tsx
index e44217888e..a45689fe44 100644
--- a/packages/core/client/src/application/__tests__/Application.test.tsx
+++ b/packages/core/client/src/application/__tests__/Application.test.tsx
@@ -386,7 +386,7 @@ describe('Application', () => {
render();
await sleep(10);
- expect(screen.getByText('Load Plugin Error')).toBeInTheDocument();
+ expect(screen.getByText('App Error')).toBeInTheDocument();
});
it('replace Component', async () => {
diff --git a/packages/core/client/src/application/components/defaultComponents.tsx b/packages/core/client/src/application/components/defaultComponents.tsx
index f011954c89..aeb2c475b9 100644
--- a/packages/core/client/src/application/components/defaultComponents.tsx
+++ b/packages/core/client/src/application/components/defaultComponents.tsx
@@ -14,7 +14,7 @@ const Loading: FC = () => Loading...
;
const AppError: FC<{ error: Error }> = ({ error }) => {
return (
-
Load Plugin Error
+
App Error
{error?.message}
{process.env.__TEST__ && error?.stack}
diff --git a/packages/core/client/src/application/hoc/withDynamicSchemaProps.tsx b/packages/core/client/src/application/hoc/withDynamicSchemaProps.tsx
index 6c653e8c88..df68c317b7 100644
--- a/packages/core/client/src/application/hoc/withDynamicSchemaProps.tsx
+++ b/packages/core/client/src/application/hoc/withDynamicSchemaProps.tsx
@@ -18,7 +18,10 @@ interface WithSchemaHookOptions {
displayName?: string;
}
-export function withDynamicSchemaProps(Component: any, options: WithSchemaHookOptions = {}) {
+export function withDynamicSchemaProps(
+ Component: React.ComponentType,
+ options: WithSchemaHookOptions = {},
+) {
const displayName = options.displayName || Component.displayName || Component.name;
const ComponentWithProps: ComponentType = (props) => {
const { dn, findComponent } = useDesignable();
diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts
index e38c4978c2..b3c6b0c668 100644
--- a/packages/core/client/src/index.ts
+++ b/packages/core/client/src/index.ts
@@ -63,10 +63,10 @@ export * from './variables';
export { withDynamicSchemaProps } from './application/hoc/withDynamicSchemaProps';
export * from './modules/blocks/BlockSchemaToolbar';
-export * from './modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
export * from './modules/blocks/data-blocks/form';
export * from './modules/blocks/data-blocks/table';
export * from './modules/blocks/data-blocks/table-selector';
export * from './modules/blocks/useParentRecordCommon';
+export * from './modules/blocks/index';
export { DeclareVariable } from './modules/variable/DeclareVariable';
diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/hooks/index.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/hooks/index.ts
new file mode 100644
index 0000000000..8e61dbd187
--- /dev/null
+++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/hooks/index.ts
@@ -0,0 +1,3 @@
+export * from './useDetailsWithPaginationBlockParams';
+export * from './useDetailsWithPaginationProps';
+export * from './useDetailsWithPaginationDecoratorProps';
diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/index.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/index.ts
new file mode 100644
index 0000000000..8f95a33778
--- /dev/null
+++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/index.ts
@@ -0,0 +1,2 @@
+export * from './hooks';
+export * from './setDataLoadingModeSettingsItem';
diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/hooks/index.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/hooks/index.ts
new file mode 100644
index 0000000000..85a5c57245
--- /dev/null
+++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from './useDetailsDecoratorProps';
+export * from './useDetailsProps';
diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/index.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/index.ts
new file mode 100644
index 0000000000..4cc90d02bd
--- /dev/null
+++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/index.ts
@@ -0,0 +1 @@
+export * from './hooks';
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/index.ts b/packages/core/client/src/modules/blocks/data-blocks/table/index.ts
index 646b14e223..f0f4eb2f72 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/index.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/index.ts
@@ -16,3 +16,4 @@ export * from './tableColumnSettings';
export * from './TableColumnInitializers';
export * from './createTableBlockUISchema';
export * from './hooks/useTableBlockDecoratorProps';
+export * from './hooks/useTableBlockProps';
diff --git a/packages/core/client/src/modules/blocks/index.ts b/packages/core/client/src/modules/blocks/index.ts
new file mode 100644
index 0000000000..dece9dd13b
--- /dev/null
+++ b/packages/core/client/src/modules/blocks/index.ts
@@ -0,0 +1,2 @@
+export * from './data-blocks/details-multi';
+export * from './data-blocks/details-single';
diff --git a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx
index 50c851a633..c420af24d2 100644
--- a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx
+++ b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx
@@ -11,7 +11,7 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
import { Drawer } from 'antd';
import classNames from 'classnames';
import React from 'react';
-import { OpenSize } from './';
+import { OpenSize } from './types';
import { useStyles } from './Action.Drawer.style';
import { useActionContext } from './hooks';
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
diff --git a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx
index ac4ceb4575..14f2738062 100644
--- a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx
+++ b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx
@@ -12,10 +12,10 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
import { Modal, ModalProps } from 'antd';
import classNames from 'classnames';
import React from 'react';
-import { OpenSize, useActionContext } from '.';
import { useToken } from '../../../style';
import { useSetAriaLabelForModal } from './hooks/useSetAriaLabelForModal';
-import { ComposedActionDrawer } from './types';
+import { ComposedActionDrawer, OpenSize } from './types';
+import { useActionContext } from './hooks';
const openSizeWidthMap = new Map([
['small', '40%'],
diff --git a/packages/core/client/src/schema-component/antd/action/Action.Popover.tsx b/packages/core/client/src/schema-component/antd/action/Action.Popover.tsx
deleted file mode 100644
index d3436f53a5..0000000000
--- a/packages/core/client/src/schema-component/antd/action/Action.Popover.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * 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.
- */
-
diff --git a/packages/core/client/src/schema-component/antd/action/Action.tsx b/packages/core/client/src/schema-component/antd/action/Action.tsx
index 52ff8ae02c..ff9768fac1 100644
--- a/packages/core/client/src/schema-component/antd/action/Action.tsx
+++ b/packages/core/client/src/schema-component/antd/action/Action.tsx
@@ -42,11 +42,11 @@ import useStyles from './Action.style';
import { ActionContextProvider } from './context';
import { useA } from './hooks';
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
-import { ComposedAction } from './types';
+import { ActionProps, ComposedAction } from './types';
import { linkageAction, setInitialActionState } from './utils';
export const Action: ComposedAction = withDynamicSchemaProps(
- observer((props: any) => {
+ observer((props: ActionProps) => {
const {
popover,
confirm,
@@ -59,6 +59,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
title,
onClick,
style,
+ loading,
openSize: os,
disabled: propsDisabled,
actionCallback,
@@ -180,14 +181,14 @@ export const Action: ComposedAction = withDynamicSchemaProps(
aria-label={getAriaLabel()}
{...others}
onMouseEnter={handleMouseEnter}
- loading={field?.data?.loading}
+ loading={field?.data?.loading || loading}
icon={icon ? : null}
disabled={disabled}
style={buttonStyle}
onClick={handleButtonClick}
component={tarComponent || Button}
className={classnames(componentCls, hashId, className, 'nb-action')}
- type={props.type === 'danger' ? undefined : props.type}
+ type={(props as any).type === 'danger' ? undefined : props.type}
>
{actionTitle}
diff --git a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx
index 610a7dca79..bf06ff2ba4 100644
--- a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx
+++ b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx
@@ -9,7 +9,7 @@
import { cx } from '@emotion/css';
import { RecursionField, observer, useFieldSchema } from '@formily/react';
-import { Space } from 'antd';
+import { Space, SpaceProps } from 'antd';
import React, { CSSProperties, useContext } from 'react';
import { createPortal } from 'react-dom';
import { DndContext } from '../../common';
@@ -17,10 +17,11 @@ import { useDesignable, useProps } from '../../hooks';
import { useSchemaInitializerRender } from '../../../application';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
-interface ActionBarContextForceProps {
- layout?: 'one-column' | 'tow-columns';
+export interface ActionBarProps {
+ layout?: 'one-column' | 'two-columns';
style?: CSSProperties;
className?: string;
+ spaceProps?: SpaceProps;
}
export interface ActionBarContextValue {
@@ -28,7 +29,7 @@ export interface ActionBarContextValue {
/**
* override props
*/
- forceProps?: ActionBarContextForceProps;
+ forceProps?: ActionBarProps;
parentComponents?: string[];
}
@@ -61,7 +62,7 @@ export const ActionBar = withDynamicSchemaProps(
const { forceProps = {} } = useActionBarContext();
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
- const { layout = 'tow-columns', style, spaceProps, ...others } = { ...useProps(props), ...forceProps } as any;
+ const { layout = 'two-columns', style, spaceProps, ...others } = { ...useProps(props), ...forceProps } as any;
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
diff --git a/packages/core/client/src/schema-component/antd/action/context.tsx b/packages/core/client/src/schema-component/antd/action/context.tsx
index be51c5877f..cc7067521d 100644
--- a/packages/core/client/src/schema-component/antd/action/context.tsx
+++ b/packages/core/client/src/schema-component/antd/action/context.tsx
@@ -7,11 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { Schema } from '@formily/react';
-import { DrawerProps, ModalProps } from 'antd';
import React, { createContext, useEffect, useRef, useState } from 'react';
import { useActionContext } from './hooks';
import { useDataBlockRequest } from '../../../data-source';
+import { ActionContextProps } from './types';
export const ActionContext = createContext({});
ActionContext.displayName = 'ActionContext';
@@ -46,21 +45,3 @@ export const ActionContextProvider: React.FC
);
};
-
-export type OpenSize = 'small' | 'middle' | 'large';
-export interface ActionContextProps {
- button?: any;
- visible?: boolean;
- setVisible?: (v: boolean) => void;
- openMode?: 'drawer' | 'modal' | 'page';
- snapshot?: boolean;
- openSize?: OpenSize;
- containerRefKey?: string;
- formValueChanged?: boolean;
- setFormValueChanged?: (v: boolean) => void;
- fieldSchema?: Schema;
- drawerProps?: DrawerProps;
- modalProps?: ModalProps;
- submitted?: boolean;
- setSubmitted?: (v: boolean) => void;
-}
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-container.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-container.tsx
new file mode 100644
index 0000000000..71dc9d9279
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-container.tsx
@@ -0,0 +1,49 @@
+import { useActionContext } from '@nocobase/client';
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: 'Open',
+ 'x-component-props': {
+ openMode: 'drawer',
+ },
+ properties: {
+ drawer: {
+ type: 'void',
+ 'x-component': 'Action.Container',
+ title: 'Title',
+ properties: {
+ footer: {
+ type: 'void',
+ 'x-component': 'Action.Container.Footer',
+ properties: {
+ close: {
+ title: 'Close',
+ 'x-component': 'Action',
+ 'x-use-component-props': function useActionProps() {
+ const { setVisible } = useActionContext();
+ return {
+ type: 'default',
+ onClick() {
+ setVisible(false);
+ },
+ };
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-context.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-context.tsx
new file mode 100644
index 0000000000..bec1655bbe
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-context.tsx
@@ -0,0 +1,59 @@
+import { ISchema, observer } from '@formily/react';
+import {
+ Action,
+ ActionContextProvider,
+ Form,
+ FormItem,
+ Input,
+ SchemaComponent,
+ SchemaComponentProvider,
+ useActionContext,
+} from '@nocobase/client';
+import React, { useState } from 'react';
+
+const schema: ISchema = {
+ type: 'object',
+ properties: {
+ drawer1: {
+ 'x-component': 'Action.Drawer',
+ type: 'void',
+ title: 'Drawer Title',
+ properties: {
+ hello1: {
+ 'x-content': 'Hello',
+ title: 'T1',
+ },
+ footer1: {
+ 'x-component': 'Action.Drawer.Footer',
+ type: 'void',
+ properties: {
+ close1: {
+ title: 'Close',
+ 'x-component': 'Action',
+ 'x-use-component-props': function useActionProps() {
+ const { setVisible } = useActionContext();
+ return {
+ onClick() {
+ setVisible(false);
+ },
+ };
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
+
+export default observer(() => {
+ const [visible, setVisible] = useState(false);
+ return (
+
+
+ setVisible(true)}>Open
+
+
+
+ );
+});
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-link.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-link.tsx
new file mode 100644
index 0000000000..83ce03763c
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-link.tsx
@@ -0,0 +1,17 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'Action.Link',
+ title: 'Edit',
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-modal.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-modal.tsx
new file mode 100644
index 0000000000..8aede324d1
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-modal.tsx
@@ -0,0 +1,103 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { Space, App as AntdApp } from 'antd';
+import { useAPIClient, useActionContext } from '@nocobase/client';
+import { useForm } from '@formily/react';
+
+const useCloseActionProps = () => {
+ const { setVisible } = useActionContext();
+ return {
+ type: 'default',
+ onClick() {
+ setVisible(false);
+ },
+ };
+};
+
+const useSubmitActionProps = () => {
+ const { setVisible } = useActionContext();
+ const api = useAPIClient();
+ const { message } = AntdApp.useApp();
+ const form = useForm();
+
+ return {
+ type: 'primary',
+ async onClick() {
+ // Submit the form
+ await form.submit();
+ const values = form.values;
+
+ console.log('values:', values);
+ const { data } = await api.request({ url: 'test', data: values, method: 'POST' });
+ if (data.data === 'ok') {
+ message.success('Submit success');
+ setVisible(false);
+ form.reset(); // 提交成功后重置表单
+ }
+ },
+ };
+};
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': Space,
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: 'Open Modal',
+ 'x-component-props': {
+ openSize: 'small',
+ },
+ properties: {
+ drawer: {
+ type: 'void',
+ 'x-component': 'Action.Modal',
+ title: 'Modal Title',
+ 'x-decorator': 'FormV2',
+ properties: {
+ username: {
+ type: 'string',
+ title: `Username`,
+ required: true,
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input',
+ },
+ footer: {
+ type: 'void',
+ 'x-component': 'Action.Modal.Footer',
+ properties: {
+ close: {
+ title: 'Close',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ type: 'default',
+ },
+ 'x-use-component-props': 'useCloseActionProps',
+ },
+ submit: {
+ title: 'Submit',
+ 'x-component': 'Action',
+ 'x-use-component-props': 'useSubmitActionProps',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ appOptions: {
+ scopes: {
+ useSubmitActionProps,
+ useCloseActionProps,
+ },
+ },
+ apis: {
+ test: { data: 'ok' },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-popover.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-popover.tsx
new file mode 100644
index 0000000000..89832901d7
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/action-popover.tsx
@@ -0,0 +1,33 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ properties: {
+ test: {
+ 'x-component': 'Action',
+ 'x-component-props': {
+ type: 'primary',
+ popover: true,
+ },
+ type: 'void',
+ title: 'Open',
+ properties: {
+ popover: {
+ type: 'void',
+ 'x-component': 'Action.Popover',
+ properties: {
+ hello: {
+ type: 'void',
+ 'x-content': 'Hello',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/actionbar-one-column.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/actionbar-one-column.tsx
new file mode 100644
index 0000000000..89cb6e106d
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/actionbar-one-column.tsx
@@ -0,0 +1,77 @@
+import { ActionInitializer, SchemaInitializer } from '@nocobase/client';
+import { getAppComponent } from '@nocobase/test/web';
+
+const addActionButton = new SchemaInitializer({
+ name: 'addActionButton',
+ designable: true,
+ title: 'Configure actions',
+ style: {
+ marginLeft: 8,
+ },
+ items: [
+ {
+ type: 'itemGroup',
+ title: 'Enable actions',
+ name: 'enableActions',
+ children: [
+ {
+ name: 'action1',
+ title: '{{t("Action 1")}}',
+ Component: 'ActionInitializer',
+ schema: {
+ title: 'Action 1',
+ 'x-component': 'Action',
+ 'x-action': 'a1', // unique identifier
+ },
+ },
+ {
+ name: 'action2',
+ title: '{{t("Action 2")}}',
+ Component: 'ActionInitializer',
+ schema: {
+ title: 'Action 2',
+ 'x-component': 'Action',
+ 'x-action': 'a2', // unique identifier
+ },
+ },
+ ],
+ },
+ ],
+});
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'ActionBar',
+ 'x-initializer': 'addActionButton',
+ 'x-component-props': {
+ layout: 'one-column',
+ },
+ properties: {
+ a1: {
+ title: 'Action 1',
+ 'x-component': 'Action',
+ 'x-action': 'a1',
+ },
+ a2: {
+ title: 'Action 2',
+ 'x-component': 'Action',
+ 'x-action': 'a2',
+ },
+ },
+ },
+ },
+ },
+ appOptions: {
+ schemaInitializers: [addActionButton],
+ components: {
+ ActionInitializer,
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/actionbar-two-columns.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/actionbar-two-columns.tsx
new file mode 100644
index 0000000000..4dce2ac3a9
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/actionbar-two-columns.tsx
@@ -0,0 +1,81 @@
+import { ActionInitializer, SchemaInitializer } from '@nocobase/client';
+import { getAppComponent } from '@nocobase/test/web';
+
+const addActionButton = new SchemaInitializer({
+ name: 'addActionButton',
+ designable: true,
+ title: 'Configure actions',
+ style: {
+ marginLeft: 8,
+ },
+ items: [
+ {
+ type: 'itemGroup',
+ title: 'Enable actions',
+ name: 'enableActions',
+ children: [
+ {
+ name: 'action1',
+ title: '{{t("Action 1")}}',
+ Component: 'ActionInitializer',
+ schema: {
+ title: 'Action 1',
+ 'x-component': 'Action',
+ 'x-action': 'a1',
+ 'x-align': 'left',
+ },
+ },
+ {
+ name: 'action2',
+ title: '{{t("Action 2")}}',
+ Component: 'ActionInitializer',
+ schema: {
+ title: 'Action 2',
+ 'x-component': 'Action',
+ 'x-action': 'a2',
+ 'x-align': 'right',
+ },
+ },
+ ],
+ },
+ ],
+});
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'ActionBar',
+ 'x-initializer': 'addActionButton',
+ 'x-component-props': {
+ layout: 'two-columns',
+ },
+ properties: {
+ a1: {
+ title: 'Action 1',
+ 'x-component': 'Action',
+ 'x-action': 'a1',
+ 'x-align': 'left',
+ },
+ a2: {
+ title: 'Action 2',
+ 'x-component': 'Action',
+ 'x-action': 'a2',
+ 'x-align': 'right',
+ },
+ },
+ },
+ },
+ },
+ appOptions: {
+ schemaInitializers: [addActionButton],
+ components: {
+ ActionInitializer,
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/basic.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/basic.tsx
new file mode 100644
index 0000000000..e2025dbfb5
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/basic.tsx
@@ -0,0 +1,23 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ ghost: true, // ButtonProps
+ type: 'dashed', // ButtonProps
+ danger: true, // ButtonProps
+ title: 'Open', // title
+ },
+ // title: 'Open', // It's also possible here
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/confirm.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/confirm.tsx
new file mode 100644
index 0000000000..fb357752e4
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/confirm.tsx
@@ -0,0 +1,35 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { Space, App as AntdApp } from 'antd';
+import { ActionProps } from '@nocobase/client';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': Space,
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: 'Delete',
+ 'x-use-component-props': function useActionProps(): ActionProps {
+ const { message } = AntdApp.useApp();
+
+ return {
+ confirm: {
+ // confirm props
+ title: 'Delete',
+ content: 'Are you sure you want to delete it?',
+ },
+ onClick() {
+ // after confirm ok
+ message.success('Deleted!');
+ },
+ };
+ },
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/custom-component.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/custom-component.tsx
new file mode 100644
index 0000000000..44576f6d20
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/custom-component.tsx
@@ -0,0 +1,38 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { Button, Space } from 'antd';
+import React from 'react';
+
+const ComponentButton = (props) => {
+ return ;
+};
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': Space,
+ properties: {
+ test1: {
+ type: 'void',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ component: 'ComponentButton', // string type
+ },
+ },
+ test2: {
+ type: 'void',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ component: ComponentButton, // ComponentType type
+ },
+ },
+ },
+ },
+ appOptions: {
+ components: {
+ ComponentButton, // register custom component
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-basic.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-basic.tsx
new file mode 100644
index 0000000000..f56e96e94e
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-basic.tsx
@@ -0,0 +1,26 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { Space } from 'antd';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': Space,
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: 'Open Drawer',
+ properties: {
+ drawer: {
+ type: 'void',
+ 'x-component': 'Action.Drawer',
+ title: 'Drawer Title',
+ },
+ },
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-footer.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-footer.tsx
new file mode 100644
index 0000000000..ec41e92455
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-footer.tsx
@@ -0,0 +1,79 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { Space } from 'antd';
+import { useActionContext } from '@nocobase/client';
+
+const useCloseActionProps = () => {
+ const { setVisible } = useActionContext();
+ return {
+ type: 'default',
+ onClick() {
+ setVisible(false);
+ },
+ };
+};
+
+const useSubmitActionProps = () => {
+ const { setVisible } = useActionContext();
+ return {
+ type: 'primary',
+ onClick() {
+ console.log('submit');
+ setVisible(false);
+ },
+ };
+};
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': Space,
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: 'Open Drawer',
+ properties: {
+ drawer: {
+ type: 'void',
+ 'x-component': 'Action.Drawer',
+ title: 'Drawer Title',
+ properties: {
+ content: {
+ type: 'void',
+ 'x-content': 'Hello',
+ },
+ footer: {
+ type: 'void',
+ 'x-component': 'Action.Drawer.Footer', // must be `Action.Drawer.Footer`
+ properties: {
+ close: {
+ title: 'Close',
+ 'x-component': 'Action',
+ 'x-use-component-props': 'useCloseActionProps',
+ },
+ submit: {
+ title: 'Submit',
+ 'x-component': 'Action',
+ 'x-use-component-props': 'useSubmitActionProps',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ appOptions: {
+ scopes: {
+ useCloseActionProps,
+ useSubmitActionProps,
+ },
+ },
+ apis: {
+ test: { data: 'ok' },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-openSize.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-openSize.tsx
new file mode 100644
index 0000000000..7a1be043ab
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-openSize.tsx
@@ -0,0 +1,36 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { Space } from 'antd';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': Space,
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: 'Open Drawer',
+ 'x-component-props': {
+ openSize: 'large', // open drawer size
+ },
+ properties: {
+ drawer: {
+ type: 'void',
+ title: 'Drawer Title',
+ 'x-component': 'Action.Drawer',
+ properties: {
+ // Drawer content
+ hello: {
+ type: 'void',
+ 'x-content': 'Hello',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-with-form.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-with-form.tsx
new file mode 100644
index 0000000000..aec8eade74
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/drawer-with-form.tsx
@@ -0,0 +1,101 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { Space, App as AntdApp } from 'antd';
+import { useAPIClient, useActionContext } from '@nocobase/client';
+import { useForm } from '@formily/react';
+
+const useCloseActionProps = () => {
+ const { setVisible } = useActionContext();
+ return {
+ type: 'default',
+ onClick() {
+ setVisible(false);
+ },
+ };
+};
+
+const useSubmitActionProps = () => {
+ const { setVisible } = useActionContext();
+ const api = useAPIClient();
+ const { message } = AntdApp.useApp();
+ const form = useForm();
+
+ return {
+ type: 'primary',
+ async onClick() {
+ // Submit the form
+ await form.submit();
+ const values = form.values;
+
+ console.log('values:', values);
+ const { data } = await api.request({ url: 'test', data: values, method: 'POST' });
+ if (data.data === 'ok') {
+ message.success('Submit success');
+ setVisible(false);
+ form.reset(); // 提交成功后重置表单
+ }
+ },
+ };
+};
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': Space,
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: 'Open Drawer',
+ properties: {
+ drawer: {
+ type: 'void',
+ 'x-component': 'Action.Drawer',
+ title: 'Drawer Title',
+ 'x-decorator': 'FormV2', // This uses the `FormV2` component.
+ properties: {
+ username: {
+ // This is a form field.
+ type: 'string',
+ title: `Username`,
+ required: true,
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Input',
+ },
+ footer: {
+ type: 'void',
+ 'x-component': 'Action.Drawer.Footer',
+ properties: {
+ close: {
+ title: 'Close',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ type: 'default',
+ },
+ 'x-use-component-props': 'useCloseActionProps',
+ },
+ submit: {
+ title: 'Submit',
+ 'x-component': 'Action',
+ 'x-use-component-props': 'useSubmitActionProps',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ appOptions: {
+ scopes: {
+ useSubmitActionProps,
+ useCloseActionProps,
+ },
+ },
+ apis: {
+ test: { data: 'ok' },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/dynamic-props.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/dynamic-props.tsx
new file mode 100644
index 0000000000..441fa8b77e
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/dynamic-props.tsx
@@ -0,0 +1,68 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { Space, App as AntdApp } from 'antd';
+import { useAPIClient, ActionProps } from '@nocobase/client';
+
+const useCustomActionProps = (): ActionProps => {
+ const api = useAPIClient();
+ const { message } = AntdApp.useApp();
+
+ return {
+ onClick: async () => {
+ const { data } = await api.request({ url: 'test' });
+ if (data.data.result === 'ok') {
+ message.success('Success!');
+ }
+ },
+ };
+};
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': Space,
+ properties: {
+ test1: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: 'test1',
+ 'x-use-component-props': useCustomActionProps, // function type
+ },
+ test2: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: 'test2',
+ 'x-use-component-props': function useCustomActionProps(): ActionProps {
+ // inline function type
+ const api = useAPIClient();
+ const { message } = AntdApp.useApp();
+
+ return {
+ onClick: async () => {
+ const { data } = await api.request({ url: 'test' });
+ if (data.data.result === 'ok') {
+ message.success('Success!');
+ }
+ },
+ };
+ },
+ },
+ test3: {
+ type: 'void',
+ 'x-component': 'Action',
+ title: 'test2',
+ 'x-use-component-props': 'useCustomActionProps', // string type
+ },
+ },
+ },
+ appOptions: {
+ scopes: {
+ useCustomActionProps,
+ },
+ },
+ apis: {
+ test: { data: { result: 'ok' } },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/demos/new-demos/schema-toolbar.tsx b/packages/core/client/src/schema-component/antd/action/demos/new-demos/schema-toolbar.tsx
new file mode 100644
index 0000000000..17d65644d9
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/demos/new-demos/schema-toolbar.tsx
@@ -0,0 +1,64 @@
+import { SchemaSettings } from '@nocobase/client';
+import { getAppComponent } from '@nocobase/test/web';
+
+const myActionSettings = new SchemaSettings({
+ name: 'myActionSettings',
+ items: [
+ {
+ name: 'delete',
+ type: 'remove',
+ },
+ ],
+});
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ properties: {
+ test: {
+ type: 'void',
+ 'x-component': 'ActionBar',
+ 'x-component-props': {
+ layout: 'two-columns',
+ },
+ properties: {
+ a1: {
+ title: 'Action 1',
+ 'x-component': 'Action',
+ 'x-action': 'a1',
+ 'x-align': 'right',
+ 'x-settings': 'myActionSettings',
+ },
+ a2: {
+ title: 'Action 2',
+ 'x-component': 'Action',
+ 'x-action': 'a2',
+ 'x-align': 'right',
+ 'x-settings': 'myActionSettings',
+ },
+ a3: {
+ title: 'Action 3',
+ 'x-component': 'Action',
+ 'x-action': 'a1',
+ 'x-align': 'left',
+ 'x-settings': 'myActionSettings',
+ },
+ a4: {
+ title: 'Action 4',
+ 'x-component': 'Action',
+ 'x-action': 'a2',
+ 'x-align': 'left',
+ 'x-settings': 'myActionSettings',
+ },
+ },
+ },
+ },
+ },
+ appOptions: {
+ schemaSettings: [myActionSettings],
+ },
+ designable: true,
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/action/index.md b/packages/core/client/src/schema-component/antd/action/index.md
index 490c1339b2..d1c99547ed 100644
--- a/packages/core/client/src/schema-component/antd/action/index.md
+++ b/packages/core/client/src/schema-component/antd/action/index.md
@@ -1,46 +1,206 @@
----
-group:
- title: Schema Components
- order: 3
----
-
# Action
-## Nodes
+将按钮和各种操作结合在一起,提供了一种统一的方式来处理操作。
-- Action
-- Action.Drawer
-- Action.Drawer.Footer
-- Action.Modal
-- Action.Modal.Footer
-- Action.Drawer
-- Action.Drawer.Footer
-- Action.Container
-- Action.Container.Footer
-- ActionBar
+## Action
-## Examples
+操作的触发器,默认是 ant-design 的 `Button` 组件,并为后续的弹窗或者抽屉提供上下文。
-### Action + Action.Drawer
+```ts
+export interface ActionProps extends ButtonProps {
+ /**
+ * button title
+ */
+ title?: string;
-
+ /**
+ * custom component, replace the default Button component
+ */
+ component?: string | ComponentType;
-### ActionContext + Action.Drawer
+ /**
+ * Dynamic rendering of the opened content in conjunction with `Action.Container`.
+ */
+ openMode?: 'drawer' | 'modal' | 'page';
+ /**
+ * The size of the pop-up window, only valid when `openMode: 'modal'`
+ */
+ openSize?: 'small' | 'middle' | 'large';
+ /**
+ * Customize the position of the pop-up window
+ */
+ containerRefKey?: string;
-只配置 Action.Drawer,而不需要 Action,结合 ActionContext 自定义按钮。
+ /**
+ * Whether to display the popover, only valid when `openMode: 'popover'`
+ */
+ popover?: boolean;
-
+ /**
+ * When the button is clicked, whether a pop-up confirmation is required
+ */
+ confirm?: false | {
+ title: string;
+ content: string;
+ };
+}
+```
-### 不同的打开方式
+### Basic Usage
-
+- `ButtonProp`
+- title
-### Action + Action.Popover
+
-
+### Custom Component
-
+- component
-### ActionBar
+
-
+### Dynamic Props
+
+这里使用了 `x-use-component-props` 的能力,具体可以查看 [x-use-component-props](https://docs.nocobase.com/development/client/ui-schema/what-is-ui-schema#x-component-props-%E5%92%8C-x-use-component-props)。
+
+
+
+### Confirm
+
+- confirm
+
+
+
+## Action.Link
+
+将 `Button` 组件替换为 `a` 标签。
+
+
+
+## Action.Drawer
+
+主要用于在右侧弹出一个抽屉。
+
+```ts
+interface ActionDrawer extends DrawerProps {}
+```
+
+### Basic Usage
+
+- `DrawerProps`
+
+
+
+### openSize
+
+
+
+### Footer
+
+Footer 可以放一些按钮,比如取消或者提交等。
+
+其 Schema `x-component` 必须为 `Action.Drawer.Footer` 组件。
+
+
+
+### With Form
+
+
+
+## Action.Modal
+
+```ts
+interface ActionModal extends ModalProps {}
+```
+
+其用法和 `Action.Drawer` 类似,这里只举一个例子。
+
+
+
+## Action.Popover
+
+注意,此时 Action 的 `popover` 属性必须为 `true`。
+
+
+
+## Action.Container
+
+当根据需要动态渲染内容时,可以使用 `Action.Container` + Action `openMode` 属性动态决定。
+
+
+
+## ActionBar
+
+一般用于区块的顶部的操作按钮,其内部会自动处理布局和渲染 [schema-initializer](/core/ui-schema/schema-initializer)。
+
+```ts
+import { SpaceProps } from 'antd'
+
+interface ActionBarProps {
+ layout?: 'one-column' | 'two-columns';
+ style?: CSSProperties;
+ className?: string;
+ spaceProps?: SpaceProps;
+}
+```
+
+### one-column
+
+一列布局,全部靠左。
+
+Schema 中的 `x-action` 是按钮的唯一标识,不能和已有的重复,用于 `ActionInitializer` 的查找和删除。
+
+
+
+### two-columns
+
+左右布局,通过 `x-align` 控制。
+
+
+
+## ActionContext
+
+封装在 `Action` 组件内部,用于传递上下文。
+
+```ts
+export type OpenSize = 'small' | 'middle' | 'large';
+export interface ActionContextProps {
+ button?: React.JSX.Element;
+ visible?: boolean;
+ setVisible?: (v: boolean) => void;
+ openMode?: 'drawer' | 'modal' | 'page';
+ snapshot?: boolean;
+ openSize?: OpenSize;
+ /**
+ * Customize the position of the pop-up window
+ */
+ containerRefKey?: string;
+ formValueChanged?: boolean;
+ setFormValueChanged?: (v: boolean) => void;
+ fieldSchema?: Schema;
+ drawerProps?: DrawerProps;
+ modalProps?: ModalProps;
+ submitted?: boolean;
+ setSubmitted?: (v: boolean) => void;
+}
+```
+
+假设 Action 组件无法满足需求,我们可以直接使用 ActionContext 组件进行自定义。
+
+
+
+## ActionSchemaToolbar
+
+用于单个按钮渲染 [SchemaToolbar](/core/ui-schema/schema-toolbar) 和 [SchemaSettings](/core/ui-schema/schema-settings)。
+
+
+
+## Hooks
+
+### useActionContext()
+
+获取 `ActionContext` 上下文。
+
+```ts
+const { visible, setVisible, fieldSchema } = useActionContext();
+```
diff --git a/packages/core/client/src/schema-component/antd/action/index.tsx b/packages/core/client/src/schema-component/antd/action/index.tsx
index b9f6ad3745..22db0e3b38 100644
--- a/packages/core/client/src/schema-component/antd/action/index.tsx
+++ b/packages/core/client/src/schema-component/antd/action/index.tsx
@@ -16,3 +16,4 @@ export * from './hooks/useGetAriaLabelOfDrawer';
export * from './hooks/useGetAriaLabelOfModal';
export * from './hooks/useGetAriaLabelOfPopover';
export * from './Action.Designer';
+export * from './types';
diff --git a/packages/core/client/src/schema-component/antd/action/types.ts b/packages/core/client/src/schema-component/antd/action/types.ts
index 733026bf97..a03db6377a 100644
--- a/packages/core/client/src/schema-component/antd/action/types.ts
+++ b/packages/core/client/src/schema-component/antd/action/types.ts
@@ -8,14 +8,79 @@
*/
import { ButtonProps, DrawerProps, ModalProps } from 'antd';
+import { ComponentType } from 'react';
+import { Schema } from '@formily/react';
-export type ActionProps = ButtonProps & {
- component?: any;
- useAction?: () => {
- run(): Promise;
- };
+export type OpenSize = 'small' | 'middle' | 'large';
+export interface ActionContextProps {
+ button?: React.JSX.Element;
+ visible?: boolean;
+ setVisible?: (v: boolean) => void;
+ openMode?: 'drawer' | 'modal' | 'page';
+ snapshot?: boolean;
+ openSize?: OpenSize;
+ /**
+ * Customize the position of the pop-up window
+ */
+ containerRefKey?: string;
+ formValueChanged?: boolean;
+ setFormValueChanged?: (v: boolean) => void;
+ fieldSchema?: Schema;
+ drawerProps?: DrawerProps;
+ modalProps?: ModalProps;
+ submitted?: boolean;
+ setSubmitted?: (v: boolean) => void;
+}
+
+export type UseActionType = (callback?: () => void) => {
+ run: () => void | Promise;
};
+export interface ActionProps extends ButtonProps {
+ /**
+ * button title
+ */
+ title?: string;
+
+ /**
+ * custom component, replace the default Button component
+ */
+ component?: string | ComponentType;
+
+ openMode?: ActionContextProps['openMode'];
+ openSize?: ActionContextProps['openSize'];
+ containerRefKey?: ActionContextProps['containerRefKey'];
+
+ /**
+ * Whether to display the popover, only valid when `openMode: 'popover'`
+ */
+ popover?: boolean;
+
+ /**
+ * When the button is clicked, whether a pop-up confirmation is required
+ */
+ confirm?:
+ | false
+ | {
+ title: string;
+ content: string;
+ };
+
+ /**
+ * @deprecated
+ */
+ useAction?: string | UseActionType;
+ /**
+ * @deprecated
+ */
+ actionCallback?: () => void;
+
+ /**
+ * @internal
+ */
+ addChild?: boolean;
+}
+
export type ComposedAction = React.FC & {
Drawer?: ComposedActionDrawer;
[key: string]: any;
diff --git a/packages/core/client/src/schema-component/antd/association-select/demos/new-demos/basic.tsx b/packages/core/client/src/schema-component/antd/association-select/demos/new-demos/basic.tsx
new file mode 100644
index 0000000000..bae26987f2
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/association-select/demos/new-demos/basic.tsx
@@ -0,0 +1,31 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'string',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'AssociationSelect',
+ 'x-component-props': {
+ service: {
+ resource: 'roles', // roles 表
+ action: 'list', // 列表接口
+ },
+ fieldNames: {
+ label: 'title', // 显示的字段
+ value: 'name', // 值字段
+ },
+ },
+ },
+ },
+ },
+ delayResponse: 500,
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/association-select/demos/new-demos/multiple.tsx b/packages/core/client/src/schema-component/antd/association-select/demos/new-demos/multiple.tsx
new file mode 100644
index 0000000000..da678a00b7
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/association-select/demos/new-demos/multiple.tsx
@@ -0,0 +1,32 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'array', // 数组类型
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'AssociationSelect',
+ 'x-component-props': {
+ multiple: true, // 多选
+ service: {
+ resource: 'roles',
+ action: 'list',
+ },
+ fieldNames: {
+ label: 'title',
+ value: 'name',
+ },
+ },
+ },
+ },
+ },
+ delayResponse: 500,
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/association-select/demos/new-demos/read-pretty.tsx b/packages/core/client/src/schema-component/antd/association-select/demos/new-demos/read-pretty.tsx
new file mode 100644
index 0000000000..39fedfdb6e
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/association-select/demos/new-demos/read-pretty.tsx
@@ -0,0 +1,33 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'string',
+ title: 'Test',
+ default: 'admin',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'AssociationSelect',
+ 'x-pattern': 'readPretty',
+ 'x-component-props': {
+ service: {
+ resource: 'roles',
+ action: 'list',
+ },
+ fieldNames: {
+ label: 'title',
+ value: 'name',
+ },
+ },
+ },
+ },
+ },
+ delayResponse: 500,
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/association-select/index.md b/packages/core/client/src/schema-component/antd/association-select/index.md
index 3b63d043c3..6b425a6a9c 100644
--- a/packages/core/client/src/schema-component/antd/association-select/index.md
+++ b/packages/core/client/src/schema-component/antd/association-select/index.md
@@ -1,27 +1,24 @@
----
-group:
- title: Schema Components
- order: 3
----
-
# AssociationSelect
-## Examples
-
-
-
-## API
-
-基于 Ant Design 的 [Select](https://ant.design/components/select/#API),相关扩展属性有:
-
-- `objectValue` 值为 object 类型
-- `fieldNames` 默认值有区别
+通过指定数据表和字段,获取数据表的数据。
```ts
-export const defaultFieldNames = {
- label: 'label',
- value: 'value',
- color: 'color',
- options: 'children',
+type AssociationSelectProps = RemoteSelectProps
& {
+ action?: string;
+ multiple?: boolean;
};
```
+
+## Basic Usage
+
+
+
+## Multiple Selection
+
+`type` 需要改为 `array`,并且属性需要增加 `multiple: true`。
+
+
+
+## Read Pretty
+
+
diff --git a/packages/core/client/src/schema-component/antd/auto-complete/AutoComplete.tsx b/packages/core/client/src/schema-component/antd/auto-complete/AutoComplete.tsx
index 282febaf58..f3737ae320 100644
--- a/packages/core/client/src/schema-component/antd/auto-complete/AutoComplete.tsx
+++ b/packages/core/client/src/schema-component/antd/auto-complete/AutoComplete.tsx
@@ -10,11 +10,14 @@
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { AutoComplete as AntdAutoComplete } from 'antd';
import { ReadPretty } from '../input';
+import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
-export const AutoComplete = connect(
- AntdAutoComplete,
- mapProps({
- dataSource: 'options',
- }),
- mapReadPretty(ReadPretty.Input),
+export const AutoComplete = withDynamicSchemaProps(
+ connect(
+ AntdAutoComplete,
+ mapProps({
+ dataSource: 'options',
+ }),
+ mapReadPretty(ReadPretty.Input),
+ ),
);
diff --git a/packages/core/client/src/schema-component/antd/auto-complete/demos/basic.tsx b/packages/core/client/src/schema-component/antd/auto-complete/demos/basic.tsx
new file mode 100644
index 0000000000..db3c0c89b0
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/auto-complete/demos/basic.tsx
@@ -0,0 +1,38 @@
+import { useField } from '@formily/react';
+import { getAppComponent } from '@nocobase/test/web';
+
+const mockVal = (str: string, repeat = 1) => ({
+ value: str.repeat(repeat),
+});
+const getPanelValue = (searchText: string) =>
+ !searchText ? [] : [mockVal(searchText), mockVal(searchText, 2), mockVal(searchText, 3)];
+
+function useAutoCompleteProps() {
+ const field = useField();
+
+ return {
+ onSearch(text: string) {
+ field.dataSource = getPanelValue(text);
+ },
+ };
+}
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'boolean',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'AutoComplete',
+ 'x-use-component-props': useAutoCompleteProps,
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/auto-complete/demos/read-pretty.tsx b/packages/core/client/src/schema-component/antd/auto-complete/demos/read-pretty.tsx
new file mode 100644
index 0000000000..758676a558
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/auto-complete/demos/read-pretty.tsx
@@ -0,0 +1,22 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ 'x-pattern': 'readPretty',
+ properties: {
+ test: {
+ type: 'boolean',
+ title: 'Test',
+ default: 'aaa',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'AutoComplete',
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/auto-complete/index.md b/packages/core/client/src/schema-component/antd/auto-complete/index.md
new file mode 100644
index 0000000000..ecf72084b0
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/auto-complete/index.md
@@ -0,0 +1,20 @@
+
+# AutoComplete
+
+自动完成输出框。其基于 ant-design [AutoComplete](https://ant.design/components/auto-complete) 组件封装。
+
+## Basic Usage
+
+```ts
+type AutoCompleteProps = AntdAutoCompleteProps;
+```
+
+
+
+## Read Pretty
+
+```ts
+type AutoCompleteReadPrettyProps = InputReadPrettyProps;
+```
+
+
diff --git a/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx b/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx
index 9c7bb0b18d..5c4c67b6e9 100644
--- a/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx
+++ b/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx
@@ -67,7 +67,13 @@ const useStyles = createStyles(({ css, token }: CustomCreateStylesUtils) => {
`;
});
-export const BlockItem: React.FC = withDynamicSchemaProps(
+export interface BlockItemProps {
+ name?: string;
+ className?: string;
+ children?: React.ReactNode;
+}
+
+export const BlockItem: React.FC = withDynamicSchemaProps(
(props) => {
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
const { className, children } = useProps(props);
diff --git a/packages/core/client/src/schema-component/antd/block-item/demos/new-demos/basic.tsx b/packages/core/client/src/schema-component/antd/block-item/demos/new-demos/basic.tsx
new file mode 100644
index 0000000000..55a8937cf7
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/block-item/demos/new-demos/basic.tsx
@@ -0,0 +1,68 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { DragHandler, SchemaSettings } from '@nocobase/client';
+import { useFieldSchema } from '@formily/react';
+import { observer } from '@formily/reactive-react';
+import React from 'react';
+
+const simpleSettings = new SchemaSettings({
+ name: 'simpleSettings',
+ items: [
+ {
+ name: 'delete',
+ type: 'remove',
+ },
+ ],
+});
+
+const MyBlock = observer(
+ () => {
+ const fieldSchema = useFieldSchema();
+ return (
+
+ {fieldSchema.name}
+
+
+ );
+ },
+ { displayName: 'MyBlock' },
+);
+
+const App = getAppComponent({
+ designable: true,
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': 'DndContext',
+ properties: {
+ block1: {
+ type: 'void',
+ 'x-decorator': 'BlockItem',
+ 'x-component': 'MyBlock',
+ 'x-settings': 'simpleSettings',
+ },
+ block2: {
+ type: 'void',
+ 'x-decorator': 'BlockItem',
+ 'x-component': 'MyBlock',
+ 'x-settings': 'simpleSettings',
+ },
+ block3: {
+ type: 'void',
+ 'x-decorator': 'BlockItem',
+ 'x-component': 'MyBlock',
+ 'x-settings': 'simpleSettings',
+ },
+ },
+ },
+ appOptions: {
+ schemaSettings: [simpleSettings],
+ components: {
+ MyBlock,
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/block-item/index.md b/packages/core/client/src/schema-component/antd/block-item/index.md
index c2f97b4ffe..842def429d 100644
--- a/packages/core/client/src/schema-component/antd/block-item/index.md
+++ b/packages/core/client/src/schema-component/antd/block-item/index.md
@@ -1,11 +1,22 @@
----
-group:
- title: Schema Components
- order: 3
----
-
# BlockItem
-普通的装饰器(Decorator)组件,无特殊 UI 效果,一般用在 x-decorator 中。用于提供区块的管理,如拖拽功能、当前节点的 SettingsForm。CardItem 和 FormItem 组件都是基于 BlockItem 实现,也具备以上相同功能。
+普通的装饰器(Decorator)组件,无 UI 效果,一般用在 `x-decorator` 中。
-
+主要提供了以下 2 个能力:
+
+- 拖拽功能
+- [SchemaToolbar](/core/ui-schema/schema-toolbar) 和 [SchemaSettings](/core/ui-schema/schema-settings) 的渲染
+
+[CardItem](/components/card-item) 和 [FormItem](/components/form-item) 组件都是基于 BlockItem 实现,也具备以上相同功能。
+
+```ts
+interface BlockItemProps {
+ name?: string;
+ className?: string;
+ children?: React.ReactNode;
+}
+```
+
+注意拖拽功能需要配置 `DndContext` 组件。
+
+
diff --git a/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx b/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx
index 980050f365..1537576a18 100644
--- a/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx
+++ b/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx
@@ -8,32 +8,38 @@
*/
import { useFieldSchema } from '@formily/react';
-import { Skeleton } from 'antd';
-import React from 'react';
-import { useInView } from 'react-intersection-observer';
+import { Skeleton, CardProps } from 'antd';
+import React, { FC } from 'react';
+import { IntersectionOptions, useInView } from 'react-intersection-observer';
import { useSchemaTemplate } from '../../../schema-templates';
import { BlockItem } from '../block-item';
import { BlockItemCard } from '../block-item/BlockItemCard';
import { BlockItemError } from '../block-item/BlockItemError';
import useStyles from './style';
-interface Props {
- children?: React.ReactNode;
- /** 区块标识 */
+interface CardItemProps extends CardProps {
name?: string;
- [key: string]: any;
+ children?: React.ReactNode;
+ /**
+ * lazy render options
+ * @default { threshold: 0, initialInView: true, triggerOnce: true }
+ * @see https://github.com/thebuilder/react-intersection-observer
+ */
+ lazyRender?: IntersectionOptions & { element?: React.JSX.Element };
}
-export const CardItem = (props: Props) => {
- const { children, name, ...restProps } = props;
+export const CardItem: FC = (props) => {
+ const { children, name, lazyRender = {}, ...restProps } = props;
const template = useSchemaTemplate();
const fieldSchema = useFieldSchema();
const templateKey = fieldSchema?.['x-template-key'];
+ const { element: lazyRenderElement, ...resetLazyRenderOptions } = lazyRender;
const { ref, inView } = useInView({
threshold: 0,
initialInView: true,
triggerOnce: true,
skip: !!process.env.__E2E__,
+ ...resetLazyRenderOptions,
});
const { wrapSSR, componentCls, hashId } = useStyles();
@@ -42,7 +48,7 @@ export const CardItem = (props: Props) => {
- {inView ? props.children : }
+ {inView ? props.children : lazyRenderElement ?? }
,
diff --git a/packages/core/client/src/schema-component/antd/card-item/demos/new-demos/basic.tsx b/packages/core/client/src/schema-component/antd/card-item/demos/new-demos/basic.tsx
new file mode 100644
index 0000000000..0fcaaccba3
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/card-item/demos/new-demos/basic.tsx
@@ -0,0 +1,58 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { SchemaSettings } from '@nocobase/client';
+
+const simpleSettings = new SchemaSettings({
+ name: 'simpleSettings',
+ items: [
+ {
+ name: 'delete',
+ type: 'remove',
+ },
+ ],
+});
+
+const App = getAppComponent({
+ designable: true,
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': 'DndContext',
+ properties: {
+ block1: {
+ type: 'void',
+ 'x-component': 'CardItem',
+ 'x-component-props': {
+ title: 'Block 1',
+ },
+ 'x-settings': 'simpleSettings',
+ properties: {
+ hello: {
+ type: 'void',
+ 'x-component': 'div',
+ 'x-content': 'Hello Card!',
+ },
+ },
+ },
+ block2: {
+ type: 'void',
+ 'x-component': 'CardItem',
+ 'x-settings': 'simpleSettings',
+ 'x-component-props': {
+ title: 'Block 2',
+ },
+ properties: {
+ hello: {
+ type: 'void',
+ 'x-component': 'div',
+ 'x-content': 'Hello Card!',
+ },
+ },
+ },
+ },
+ },
+ appOptions: {
+ schemaSettings: [simpleSettings],
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/card-item/demos/new-demos/lazy-render.tsx b/packages/core/client/src/schema-component/antd/card-item/demos/new-demos/lazy-render.tsx
new file mode 100644
index 0000000000..e050ed3074
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/card-item/demos/new-demos/lazy-render.tsx
@@ -0,0 +1,84 @@
+import { getAppComponent } from '@nocobase/test/web';
+import { SchemaSettings } from '@nocobase/client';
+
+const simpleSettings = new SchemaSettings({
+ name: 'simpleSettings',
+ items: [
+ {
+ name: 'delete',
+ type: 'remove',
+ },
+ ],
+});
+
+const App = getAppComponent({
+ designable: true,
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'DndContext',
+ 'x-component': 'div',
+ 'x-component-props': {
+ style: {
+ height: 300,
+ overflow: 'auto',
+ border: '1px solid #f0f0f0',
+ },
+ },
+ properties: {
+ block1: {
+ type: 'void',
+ 'x-component': 'CardItem',
+ 'x-component-props': {
+ title: 'Block 1',
+ },
+ 'x-settings': 'simpleSettings',
+ properties: {
+ hello: {
+ type: 'void',
+ 'x-component': 'div',
+ 'x-content': 'Hello Card!',
+ },
+ },
+ },
+ block2: {
+ type: 'void',
+ 'x-component': 'CardItem',
+ 'x-settings': 'simpleSettings',
+ 'x-component-props': {
+ title: 'Block 2',
+ },
+ properties: {
+ hello: {
+ type: 'void',
+ 'x-component': 'div',
+ 'x-content': 'Hello Card!',
+ },
+ },
+ },
+ block3: {
+ type: 'void',
+ 'x-component': 'CardItem',
+ 'x-settings': 'simpleSettings',
+ 'x-component-props': {
+ title: 'Block 3',
+ lazyRender: {
+ threshold: 1,
+ },
+ },
+ properties: {
+ hello: {
+ type: 'void',
+ 'x-component': 'div',
+ 'x-content': 'Hello Card!',
+ },
+ },
+ },
+ },
+ },
+ appOptions: {
+ schemaSettings: [simpleSettings],
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/card-item/index.md b/packages/core/client/src/schema-component/antd/card-item/index.md
index 29a0ec13e8..78b426912d 100644
--- a/packages/core/client/src/schema-component/antd/card-item/index.md
+++ b/packages/core/client/src/schema-component/antd/card-item/index.md
@@ -1,13 +1,27 @@
----
-group:
- title: Schema Components
- order: 3
----
-
# CardItem
-卡片装饰器。除此之外,也继承了 BlockItem 的功能。
+卡片装饰器。主要功能为:
-## Example
+- 拖拽和 [SchemaToolbar](/core/ui-schema/schema-toolbar) 和 [SchemaSettings](/core/ui-schema/schema-settings) 的渲染,继承自[BlockItem](/components/block-item)
+- 懒渲染
-
+其基于 ant-design [Card](https://ant.design/components/card-cn/) 组件进行封装,懒加载基于 [react-intersection-observer](https://github.com/thebuilder/react-intersection-observer) 实现。
+
+```ts
+interface CardItemProps extends CardProps {
+ name?: string;
+ /**
+ * lazy render options
+ * @see https://github.com/thebuilder/react-intersection-observer
+ */
+ lazyRender?: IntersectionOptions;
+}
+```
+
+## Basic
+
+
+
+## Custom lazy render
+
+
diff --git a/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx b/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx
index 8ed483eaf9..1af4621609 100644
--- a/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx
+++ b/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx
@@ -11,107 +11,140 @@ import { LoadingOutlined } from '@ant-design/icons';
import { ArrayField } from '@formily/core';
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
import { toArr } from '@formily/shared';
-import { Cascader as AntdCascader, Space } from 'antd';
+import { Cascader as AntdCascader, Space, CascaderProps as AntdCascaderProps } from 'antd';
import { isBoolean, omit } from 'lodash';
import React from 'react';
-import { useRequest } from '../../../api-client';
+import { UseRequestResult, useRequest } from '../../../api-client';
import { ReadPretty } from './ReadPretty';
import { defaultFieldNames } from './defaultFieldNames';
+import { BaseOptionType } from 'antd/es/select';
+import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
-const useDefDataSource = (options) => {
+const useDefDataSource = (options, props: any) => {
const field = useField();
- return useRequest(() => Promise.resolve({ data: field.dataSource || [] }), options);
+ return useRequest(() => Promise.resolve({ data: field.dataSource || props.options || [] }), {
+ ...options,
+ refreshDeps: [field.dataSource, props.options],
+ });
};
const useDefLoadData = (props: any) => {
return props?.loadData;
};
-export const Cascader = connect(
- (props: any) => {
- const field = useField();
- const {
- value,
- onChange,
- labelInValue,
- // fieldNames = defaultFieldNames,
- useDataSource = useDefDataSource,
- useLoadData = useDefLoadData,
- changeOnSelectLast,
- changeOnSelect,
- maxLevel,
- ...others
- } = props;
- const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
- const loadData = useLoadData(props);
- const { loading, run } = useDataSource({
- onSuccess(data) {
- field.dataSource = data?.data || [];
- },
- });
- // 兼容值为 object[] 的情况
- const toValue = () => {
- return toArr(value).map((item) => {
- if (typeof item === 'object') {
- return item[fieldNames.value];
- }
- return item;
- });
- };
- const displayRender = (labels: string[], selectedOptions: any[]) => {
- return (
-
- {labels.map((label, index) => {
- if (selectedOptions[index]) {
- return {label};
- }
- const item = toArr(value)
- .filter(Boolean)
- .find((item) => item[fieldNames.value] === label);
- return {item?.[fieldNames.label] || label};
- })}
-
- );
- };
- const handelDropDownVisible = (value) => {
- if (value && !field.dataSource?.length) {
- run();
- }
- };
+export type CascaderProps = AntdCascaderProps & {
+ /**
+ * @deprecated use `x-use-component-props` instead
+ */
+ useLoadData: (props: CascaderProps) => AntdCascaderProps['loadData'];
+ /**
+ * @deprecated use `x-use-component-props` instead
+ */
+ useDataSource?: (options: any) => UseRequestResult;
+ /**
+ * Whether to wrap the label of option into the value
+ */
+ labelInValue?: boolean;
+ /**
+ * must select the last level
+ */
+ changeOnSelectLast?: boolean;
+ onChange?: (value: any) => void;
+ maxLevel?: number;
+};
- return (
- {
- if (value && labelInValue) {
- onChange(selectedOptions.map((option) => omit(option, [fieldNames.children])));
- } else {
- onChange(value);
+export const Cascader = withDynamicSchemaProps(
+ connect(
+ (props: CascaderProps) => {
+ const field = useField();
+ const {
+ value,
+ onChange,
+ labelInValue,
+ options,
+ // fieldNames = defaultFieldNames,
+ useDataSource = useDefDataSource,
+ useLoadData = useDefLoadData,
+ changeOnSelectLast,
+ changeOnSelect,
+ maxLevel,
+ ...others
+ } = props;
+ const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
+ const loadData = useLoadData(props);
+ const { loading, run } = useDataSource(
+ {
+ onSuccess(data) {
+ field.dataSource = data?.data || [];
+ },
+ },
+ props,
+ );
+ // 兼容值为 object[] 的情况
+ const toValue = () => {
+ return toArr(value).map((item) => {
+ if (typeof item === 'object') {
+ return item[fieldNames.value];
}
- }}
- />
- );
- },
- mapProps(
- {
- dataSource: 'options',
- },
- (props, field) => {
- return {
- ...props,
- suffixIcon: field?.['loading'] || field?.['validating'] ? : props.suffixIcon,
+ return item;
+ });
};
+ const displayRender = (labels: string[], selectedOptions: any[]) => {
+ return (
+
+ {labels.map((label, index) => {
+ if (selectedOptions[index]) {
+ return {label};
+ }
+ const item = toArr(value)
+ .filter(Boolean)
+ .find((item) => item[fieldNames.value] === label);
+ return {item?.[fieldNames.label] || label};
+ })}
+
+ );
+ };
+ const handelDropDownVisible = (value) => {
+ if (value && !field.dataSource?.length) {
+ run();
+ }
+ };
+
+ return (
+ {
+ if (value && labelInValue) {
+ onChange(selectedOptions.map((option) => omit(option, [fieldNames.children])));
+ } else {
+ onChange(value);
+ }
+ }}
+ />
+ );
},
+ mapProps(
+ {
+ dataSource: 'options',
+ },
+ (props, field) => {
+ return {
+ ...props,
+ suffixIcon: field?.['loading'] || field?.['validating'] ? : props.suffixIcon,
+ };
+ },
+ ),
+ mapReadPretty(ReadPretty),
),
- mapReadPretty(ReadPretty),
+ { displayName: 'Cascader' },
);
export default Cascader;
diff --git a/packages/core/client/src/schema-component/antd/cascader/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/cascader/ReadPretty.tsx
index 4274ecafbf..8ef232548e 100644
--- a/packages/core/client/src/schema-component/antd/cascader/ReadPretty.tsx
+++ b/packages/core/client/src/schema-component/antd/cascader/ReadPretty.tsx
@@ -13,7 +13,18 @@ import { toArr } from '@formily/shared';
import React from 'react';
import { defaultFieldNames } from './defaultFieldNames';
-export const ReadPretty: React.FC = (props: any) => {
+interface FieldNames {
+ label: string;
+ value: string;
+ children: string;
+}
+
+export interface CascaderReadPrettyProps {
+ fieldNames?: FieldNames;
+ value?: any;
+}
+
+export const ReadPretty: React.FC = (props) => {
const { fieldNames = defaultFieldNames } = props;
const values = toArr(props.value);
const len = values.length;
diff --git a/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/basic.tsx b/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/basic.tsx
new file mode 100644
index 0000000000..5b81588382
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/basic.tsx
@@ -0,0 +1,56 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const options = [
+ {
+ value: 'zhejiang',
+ label: 'Zhejiang',
+ children: [
+ {
+ value: 'hangzhou',
+ label: 'Hangzhou',
+ children: [
+ {
+ value: 'xihu',
+ label: 'West Lake',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ value: 'jiangsu',
+ label: 'Jiangsu',
+ children: [
+ {
+ value: 'nanjing',
+ label: 'Nanjing',
+ children: [
+ {
+ value: 'zhonghuamen',
+ label: 'Zhong Hua Men',
+ },
+ ],
+ },
+ ],
+ },
+];
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'string',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ enum: options,
+ 'x-component': 'Cascader',
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/changeOnSelectLast.tsx b/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/changeOnSelectLast.tsx
new file mode 100644
index 0000000000..158fbc6654
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/changeOnSelectLast.tsx
@@ -0,0 +1,59 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const options = [
+ {
+ value: 'zhejiang',
+ label: 'Zhejiang',
+ children: [
+ {
+ value: 'hangzhou',
+ label: 'Hangzhou',
+ children: [
+ {
+ value: 'xihu',
+ label: 'West Lake',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ value: 'jiangsu',
+ label: 'Jiangsu',
+ children: [
+ {
+ value: 'nanjing',
+ label: 'Nanjing',
+ children: [
+ {
+ value: 'zhonghuamen',
+ label: 'Zhong Hua Men',
+ },
+ ],
+ },
+ ],
+ },
+];
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'string',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ enum: options,
+ 'x-component': 'Cascader',
+ 'x-component-props': {
+ changeOnSelectLast: false,
+ },
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/labelInValue.tsx b/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/labelInValue.tsx
new file mode 100644
index 0000000000..1d080b1745
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/labelInValue.tsx
@@ -0,0 +1,59 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const options = [
+ {
+ value: 'zhejiang',
+ label: 'Zhejiang',
+ children: [
+ {
+ value: 'hangzhou',
+ label: 'Hangzhou',
+ children: [
+ {
+ value: 'xihu',
+ label: 'West Lake',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ value: 'jiangsu',
+ label: 'Jiangsu',
+ children: [
+ {
+ value: 'nanjing',
+ label: 'Nanjing',
+ children: [
+ {
+ value: 'zhonghuamen',
+ label: 'Zhong Hua Men',
+ },
+ ],
+ },
+ ],
+ },
+];
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'string',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ enum: options,
+ 'x-component': 'Cascader',
+ 'x-component-props': {
+ labelInValue: true,
+ },
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/loadData.tsx b/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/loadData.tsx
new file mode 100644
index 0000000000..195172f74c
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/loadData.tsx
@@ -0,0 +1,76 @@
+import { useField } from '@formily/react';
+import { getAppComponent } from '@nocobase/test/web';
+import React, { useState } from 'react';
+
+interface Option {
+ value?: string | number | null;
+ label: React.ReactNode;
+ children?: Option[];
+ isLeaf?: boolean;
+}
+
+const optionLists: Option[] = [
+ {
+ value: 'zhejiang',
+ label: 'Zhejiang',
+ isLeaf: false,
+ },
+ {
+ value: 'jiangsu',
+ label: 'Jiangsu',
+ isLeaf: false,
+ },
+];
+
+const useCustomCascaderProps = () => {
+ const field = useField();
+ field.dataSource = optionLists;
+ const loadData = (selectedOptions: Option[]) => {
+ const targetOption = selectedOptions[selectedOptions.length - 1];
+
+ // load options lazily
+ setTimeout(() => {
+ targetOption.children = [
+ {
+ label: `${targetOption.label} Dynamic 1`,
+ value: 'dynamic1',
+ },
+ {
+ label: `${targetOption.label} Dynamic 2`,
+ value: 'dynamic2',
+ },
+ ];
+ field.dataSource = [...field.dataSource];
+ }, 1000);
+ };
+
+ return {
+ changeOnSelect: true,
+ loadData,
+ };
+};
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'string',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Cascader',
+ 'x-use-component-props': 'useCustomCascaderProps',
+ },
+ },
+ },
+ appOptions: {
+ scopes: {
+ useCustomCascaderProps,
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/read-pretty.tsx b/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/read-pretty.tsx
new file mode 100644
index 0000000000..b58e74d342
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/cascader/demos/new-demos/read-pretty.tsx
@@ -0,0 +1,58 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const options = [
+ {
+ value: 'zhejiang',
+ label: 'Zhejiang',
+ children: [
+ {
+ value: 'hangzhou',
+ label: 'Hangzhou',
+ children: [
+ {
+ value: 'xihu',
+ label: 'West Lake',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ value: 'jiangsu',
+ label: 'Jiangsu',
+ children: [
+ {
+ value: 'nanjing',
+ label: 'Nanjing',
+ children: [
+ {
+ value: 'zhonghuamen',
+ label: 'Zhong Hua Men',
+ },
+ ],
+ },
+ ],
+ },
+];
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ 'x-pattern': 'readPretty',
+ properties: {
+ test: {
+ type: 'string',
+ title: 'Test',
+ default: ['zhejiang', 'hangzhou', 'xihu'],
+ 'x-decorator': 'FormItem',
+ enum: options,
+ 'x-component': 'Cascader',
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/cascader/index.md b/packages/core/client/src/schema-component/antd/cascader/index.md
index abaeae2274..78d63bd9c1 100644
--- a/packages/core/client/src/schema-component/antd/cascader/index.md
+++ b/packages/core/client/src/schema-component/antd/cascader/index.md
@@ -1,36 +1,53 @@
----
-group:
- title: Schema Components
- order: 3
----
-
# Cascader
-## Examples
-
-### Cascader
-
-
-
-### Asynchronous Data Source
-
-
-
-## API
-
-基于 antd 的 [Cascader](https://ant.design/components/cascader/#API) 附加的一些属性:
-
-- `labelInValue` 是否把每个选项的 label 包装到 value 中
-- `changeOnSelectLast` 必须选到最后一级
-- `useLoadData` 可调用 hook 的 loadData
+级联选择器,其基于 ant-design [Cascader](https://ant.design/components/cascader-cn/) 组件封装。
```ts
-{
- useLoadData: (props) => {
- // 这里可以写 hook
- return function loadData(selectedOptions) {
- // Cascader 的 loadData
- }
- }
+type CascaderProps = AntdCascaderProps & {
+ /**
+ * Whether to wrap the label of option into the value
+ */
+ labelInValue?: boolean;
+ /**
+ * must select the last level
+ */
+ changeOnSelectLast?: boolean;
}
```
+
+## Basic Usage
+
+
+
+## Asynchronous Data Source
+
+
+
+## labelInValue
+
+如果设置 `labelInValue` 为 `true`,则选中的数据为 `{ label: string, value: string }` 格式,否则为 `string` 格式。
+
+
+
+## changeOnSelectLast
+
+如果设置 `changeOnSelectLast` 为 `true`,则必须选择最后一级,如果为 `false`,则可以选择任意级。
+
+
+
+## Read Pretty
+
+```ts
+interface FieldNames {
+ label: string;
+ value: string;
+ children: string;
+}
+
+export interface CascaderReadPrettyProps {
+ fieldNames?: FieldNames;
+ value?: any;
+}
+```
+
+
diff --git a/packages/core/client/src/schema-component/antd/checkbox/Checkbox.tsx b/packages/core/client/src/schema-component/antd/checkbox/Checkbox.tsx
index f683d43b46..46dd9e77dd 100644
--- a/packages/core/client/src/schema-component/antd/checkbox/Checkbox.tsx
+++ b/packages/core/client/src/schema-component/antd/checkbox/Checkbox.tsx
@@ -11,21 +11,29 @@ import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
import { isValid } from '@formily/shared';
import { Checkbox as AntdCheckbox, Tag } from 'antd';
-import type { CheckboxGroupProps, CheckboxProps } from 'antd/es/checkbox';
+import type {
+ CheckboxGroupProps as AntdCheckboxGroupProps,
+ CheckboxProps as AntdCheckboxProps,
+} from 'antd/es/checkbox';
import uniq from 'lodash/uniq';
-import React, { useMemo } from 'react';
+import React, { FC, useMemo } from 'react';
import { useCollectionField } from '../../../data-source/collection-field/CollectionFieldProvider';
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
type ComposedCheckbox = React.ForwardRefExoticComponent<
Pick, string | number | symbol> & React.RefAttributes
> & {
- Group?: React.FC;
+ Group?: React.FC;
__ANT_CHECKBOX?: boolean;
- ReadPretty?: React.FC;
+ ReadPretty?: React.FC;
};
-const ReadPretty = (props) => {
+export interface CheckboxReadPrettyProps {
+ showUnchecked?: boolean;
+ value?: boolean;
+}
+
+const ReadPretty: FC = (props) => {
if (props.value) {
return ;
}
@@ -33,7 +41,7 @@ const ReadPretty = (props) => {
};
export const Checkbox: ComposedCheckbox = connect(
- (props: any) => {
+ (props: AntdCheckboxProps) => {
const changeHandler = (val) => {
props?.onChange(val);
};
@@ -52,12 +60,17 @@ Checkbox.ReadPretty.displayName = 'Checkbox.ReadPretty';
Checkbox.__ANT_CHECKBOX = true;
+export interface CheckboxGroupReadPrettyProps {
+ value?: any[];
+ ellipsis?: boolean;
+}
+
Checkbox.Group = connect(
AntdCheckbox.Group,
mapProps({
dataSource: 'options',
}),
- mapReadPretty((props) => {
+ mapReadPretty((props: CheckboxGroupReadPrettyProps) => {
if (!isValid(props.value)) {
return null;
}
diff --git a/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/basic.tsx b/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/basic.tsx
new file mode 100644
index 0000000000..780323b173
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/basic.tsx
@@ -0,0 +1,20 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'boolean',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox',
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/group-read-pretty.tsx b/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/group-read-pretty.tsx
new file mode 100644
index 0000000000..1ed3d01edf
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/group-read-pretty.tsx
@@ -0,0 +1,57 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const options = [
+ {
+ label: '选项1',
+ value: 1,
+ color: 'red',
+ },
+ {
+ label: '选项2',
+ value: 2,
+ color: 'blue',
+ },
+ {
+ label: '选项3',
+ value: 3,
+ color: 'yellow',
+ },
+];
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ 'x-pattern': 'readPretty',
+ properties: {
+ test1: {
+ type: 'array',
+ default: [1, 2],
+ title: 'Test1',
+ enum: options,
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox.Group',
+ },
+ test2: {
+ type: 'array',
+ default: [1, 2, 3],
+ title: 'Test2',
+ enum: options,
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox.Group',
+ 'x-decorator-props': {
+ style: {
+ width: 100,
+ },
+ },
+ 'x-component-props': {
+ ellipsis: true,
+ },
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/group.tsx b/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/group.tsx
new file mode 100644
index 0000000000..c1cfa50e6c
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/group.tsx
@@ -0,0 +1,34 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const options = [
+ {
+ label: '选项1',
+ value: 1,
+ color: 'red',
+ },
+ {
+ label: '选项2',
+ value: 2,
+ color: 'blue',
+ },
+];
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'array',
+ title: 'Test',
+ enum: options,
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox.Group',
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/read-pretty.tsx b/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/read-pretty.tsx
new file mode 100644
index 0000000000..4b1348079c
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/checkbox/demos/new-demos/read-pretty.tsx
@@ -0,0 +1,39 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ 'x-pattern': 'readPretty',
+ properties: {
+ test: {
+ type: 'boolean',
+ default: true,
+ title: 'Test1',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox',
+ },
+ test2: {
+ type: 'boolean',
+ default: false,
+ title: 'Test2',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox',
+ },
+ test3: {
+ type: 'boolean',
+ default: false,
+ title: 'Test3',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox',
+ 'x-component-props': {
+ showUnchecked: true,
+ },
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/checkbox/index.md b/packages/core/client/src/schema-component/antd/checkbox/index.md
index a24296c51c..57a25e1cff 100644
--- a/packages/core/client/src/schema-component/antd/checkbox/index.md
+++ b/packages/core/client/src/schema-component/antd/checkbox/index.md
@@ -1,17 +1,46 @@
----
-group:
- title: Schema Components
- order: 3
----
-
# Checkbox
-## Examples
+复选框,其基于 ant-design [Checkbox](https://ant.design/components/checkbox/) 组件封装。
-### 勾选
-
+## Basic Usage
-### 组
+```ts
+type CheckboxProps = AntdCheckboxProps;
+```
-
+
+
+## Read Pretty
+
+```ts
+interface CheckboxReadPrettyProps {
+ showUnchecked?: boolean;
+ value?: boolean;
+}
+```
+
+如果值为 `false`,默认情况下不显示内容,可以通过 `showUnchecked` 属性来显示未选中的复选框。
+
+
+
+## Checkbox Group
+
+```ts
+type CheckboxGroupProps = CheckboxGroupProps;
+```
+
+注意 schema 的 type 属性为 `array`。
+
+
+
+## Checkbox Group Read Pretty
+
+```ts
+export interface CheckboxGroupReadPrettyProps {
+ value?: any[];
+ ellipsis?: boolean;
+}
+```
+
+
diff --git a/packages/core/client/src/schema-component/antd/collection-select/demos/basic.tsx b/packages/core/client/src/schema-component/antd/collection-select/demos/basic.tsx
new file mode 100644
index 0000000000..1d5c91f958
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/collection-select/demos/basic.tsx
@@ -0,0 +1,20 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'string',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'CollectionSelect',
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/collection-select/demos/multiple.tsx b/packages/core/client/src/schema-component/antd/collection-select/demos/multiple.tsx
new file mode 100644
index 0000000000..7049ebb9ae
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/collection-select/demos/multiple.tsx
@@ -0,0 +1,23 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'array',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'CollectionSelect',
+ 'x-component-props': {
+ mode: 'multiple',
+ },
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/collection-select/demos/read-pretty.tsx b/packages/core/client/src/schema-component/antd/collection-select/demos/read-pretty.tsx
new file mode 100644
index 0000000000..851d59d3c2
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/collection-select/demos/read-pretty.tsx
@@ -0,0 +1,22 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-component': 'ShowFormData',
+ 'x-decorator': 'FormV2',
+ 'x-read-pretty': true,
+ properties: {
+ test: {
+ type: 'string',
+ default: 'users',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'CollectionSelect',
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/collection-select/index.md b/packages/core/client/src/schema-component/antd/collection-select/index.md
index a1d9a0c005..28fd451655 100644
--- a/packages/core/client/src/schema-component/antd/collection-select/index.md
+++ b/packages/core/client/src/schema-component/antd/collection-select/index.md
@@ -1,8 +1,24 @@
----
-group:
- title: Schema Components
----
-
# CollectionSelect
-## Example
+用于选择当前数据源的数据表。
+
+```ts
+type CollectionSelectProps = SelectProps & {
+ filter?: (item: any, index: number, array: any[]) => boolean;
+ isTableOid?: boolean;
+};
+```
+
+## Basic Usage
+
+
+
+## Multiple Selection
+
+`type` 需要改为 `array`,并且属性需要增加 `mode: 'multiple'`。
+
+
+
+## Read Pretty
+
+
diff --git a/packages/core/client/src/schema-component/antd/color-picker/ColorPicker.tsx b/packages/core/client/src/schema-component/antd/color-picker/ColorPicker.tsx
index 33b99abf74..45a88c92f3 100644
--- a/packages/core/client/src/schema-component/antd/color-picker/ColorPicker.tsx
+++ b/packages/core/client/src/schema-component/antd/color-picker/ColorPicker.tsx
@@ -10,12 +10,16 @@
import { css } from '@emotion/css';
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
import { connect, mapProps, mapReadPretty } from '@formily/react';
-import { ColorPicker as AntdColorPicker } from 'antd';
+import { ColorPicker as AntdColorPicker, ColorPickerProps as AntdColorPickerProps } from 'antd';
import cls from 'classnames';
import React from 'react';
+export interface ColorPickerProps extends Omit {
+ onChange?: (color: string) => void;
+}
+
export const ColorPicker = connect(
- (props) => {
+ (props: ColorPickerProps) => {
const { value, onChange, ...others } = props;
return (
diff --git a/packages/core/client/src/schema-component/antd/color-picker/demos/new-demos/basic.tsx b/packages/core/client/src/schema-component/antd/color-picker/demos/new-demos/basic.tsx
new file mode 100644
index 0000000000..f45203b915
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/color-picker/demos/new-demos/basic.tsx
@@ -0,0 +1,20 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ properties: {
+ test: {
+ type: 'string',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'ColorPicker',
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/color-picker/demos/new-demos/read-pretty.tsx b/packages/core/client/src/schema-component/antd/color-picker/demos/new-demos/read-pretty.tsx
new file mode 100644
index 0000000000..d666c4ac11
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/color-picker/demos/new-demos/read-pretty.tsx
@@ -0,0 +1,22 @@
+import { getAppComponent } from '@nocobase/test/web';
+
+const App = getAppComponent({
+ schema: {
+ type: 'void',
+ name: 'root',
+ 'x-decorator': 'FormV2',
+ 'x-component': 'ShowFormData',
+ 'x-pattern': 'readPretty',
+ properties: {
+ test: {
+ type: 'string',
+ default: '#8BBB11',
+ title: 'Test',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'ColorPicker',
+ },
+ },
+ },
+});
+
+export default App;
diff --git a/packages/core/client/src/schema-component/antd/color-picker/index.md b/packages/core/client/src/schema-component/antd/color-picker/index.md
index f30325a471..9240492f54 100644
--- a/packages/core/client/src/schema-component/antd/color-picker/index.md
+++ b/packages/core/client/src/schema-component/antd/color-picker/index.md
@@ -1,18 +1,17 @@
----
-group:
- title: Schema Components
- order: 3
----
-
# ColorPicker
-## Examples
-
-### Basic
-
-
+颜色选择器,其基于 ant-design [ColorPicker](https://ant.design/components/color-picker/) 组件进行封装。
+```ts
+interface ColorPickerProps extends Omit
{
+ onChange?: (color: string) => void;
+}
+```
+## Basic Usage
+
+## Read Pretty
+
diff --git a/packages/core/client/src/schema-component/antd/color-select/ColorSelect.tsx b/packages/core/client/src/schema-component/antd/color-select/ColorSelect.tsx
index 3daf9d95b8..7324b4dae7 100644
--- a/packages/core/client/src/schema-component/antd/color-select/ColorSelect.tsx
+++ b/packages/core/client/src/schema-component/antd/color-select/ColorSelect.tsx
@@ -9,7 +9,7 @@
import { LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty } from '@formily/react';
-import { Select, Tag } from 'antd';
+import { Select, SelectProps, Tag } from 'antd';
import React from 'react';
import { useCompile } from '../../hooks/useCompile';
@@ -28,8 +28,12 @@ const colors = {
default: '{{t("Default")}}',
};
+export interface ColorSelectProps extends SelectProps {
+ suffix?: React.ReactNode;
+}
+
export const ColorSelect = connect(
- (props) => {
+ (props: ColorSelectProps) => {
const compile = useCompile();
return (