client components (#4216)

* docs: update docs components

* docs: add more component docs

* docs: add more docs

* fix: add more docs

* fix: build bug

* feat: docs

* fix: build error

* fix: docs

* fix: change x-read-pretty to x-patten

* fix: upgrade docs and types

* fix: build bug

* fix: add more docs

* fix: build bug

* fix: cascader component

* fix: bug

* fix: add more docs

* fix: add backend ci time

---------

Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
jack zhang 2024-04-30 21:21:17 +08:00 committed by GitHub
parent 5313b8e495
commit 95fef86880
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
285 changed files with 9613 additions and 888 deletions

View File

@ -68,7 +68,7 @@ jobs:
DB_STORAGE: /tmp/db.sqlite
DB_TEST_PREFIX: test_
DB_UNDERSCORED: ${{ matrix.underscored }}
timeout-minutes: 40
timeout-minutes: 60
postgres-test:
strategy:
@ -132,7 +132,7 @@ jobs:
COLLECTION_MANAGER_SCHEMA: ${{ matrix.collection_schema }}
DB_TEST_DISTRIBUTOR_PORT: 23450
DB_TEST_PREFIX: test_
timeout-minutes: 40
timeout-minutes: 60
mysql-test:
strategy:
@ -183,7 +183,7 @@ jobs:
DB_UNDERSCORED: ${{ matrix.underscored }}
DB_TEST_DISTRIBUTOR_PORT: 23450
DB_TEST_PREFIX: test_
timeout-minutes: 40
timeout-minutes: 60
mariadb-test:
strategy:
matrix:
@ -233,4 +233,4 @@ jobs:
DB_UNDERSCORED: ${{ matrix.underscored }}
DB_TEST_DISTRIBUTOR_PORT: 23450
DB_TEST_PREFIX: test_
timeout-minutes: 40
timeout-minutes: 60

View File

@ -1,5 +1,3 @@
import path from 'path';
import glob from 'glob';
import _ from 'lodash'
import { getUmiConfig } from '@nocobase/devtools/umiConfig';
import { defineConfig } from 'dumi';
@ -11,19 +9,6 @@ const lang = process.env.DOC_LANG;
console.log('process.env.DOC_LANG', lang);
const componentsDir = 'src/schema-component/antd';
function getComponentsMenu() {
const cwd = path.join(__dirname, componentsDir);
const ignores = ['table/index.md', 'form/index.md']; // 老版本,不需要展示
const files = glob.sync('*/index.md', { cwd, ignore: ignores });
return files.map((file) => ({
title: _.upperFirst(_.camelCase(file.replace('/index.md', ''))),
link: `/components/${file.replace('/index.md', '')}`,
}));
}
export default defineConfig({
hash: true,
alias: {
@ -39,7 +24,7 @@ export default defineConfig({
resolve: {
docDirs: [`./docs/${lang}`],
atomDirs: [
{ type: 'component', dir: componentsDir },
{ type: 'component', dir: 'src/schema-component/antd' },
],
},
locales: [
@ -92,6 +77,10 @@ export default defineConfig({
title: 'PluginSettingsManager',
link: '/core/application/plugin-settings-manager',
},
{
title: 'Request',
link: '/core/request',
},
],
},
{
@ -225,7 +214,196 @@ export default defineConfig({
]
}
],
'/components': getComponentsMenu(),
'/components': [
{
title: 'Action',
type: 'group',
children: [
{
"title": "Action",
"link": "/components/action"
},
{
"title": "Filter",
"link": "/components/filter"
},
]
},
{
title: 'Field',
type: 'group',
children: [
{
"title": "Checkbox",
"link": "/components/checkbox"
},
{
"title": "Cascader",
"link": "/components/cascader"
},
{
"title": "ColorPicker",
"link": "/components/color-picker"
},
{
"title": "ColorSelect",
"link": "/components/color-select"
},
{
"title": "DatePicker",
"link": "/components/date-picker"
},
{
"title": "TimePicker",
"link": "/components/time-picker"
},
{
"title": "IconPicker",
"link": "/components/icon-picker"
},
{
"title": "InputNumber",
"link": "/components/input-number"
},
{
"title": "Input",
"link": "/components/input"
},
{
"title": "AutoComplete",
"link": "/components/auto-complete"
},
{
"title": "NanoIDInput",
"link": "/components/nanoid-input"
},
{
"title": "Password",
"link": "/components/password"
},
{
"title": "Percent",
"link": "/components/percent"
},
{
"title": "Radio",
"link": "/components/radio"
},
{
"title": "Select",
"link": "/components/select"
},
{
"title": "RemoteSelect",
"link": "/components/remote-select"
},
{
"title": "TreeSelect",
"link": "/components/tree-select"
},
{
"title": "Upload",
"link": "/components/upload"
},
{
"title": "CollectionSelect",
"link": "/components/collection-select"
},
{
"title": "Cron",
"link": "/components/cron"
},
{
"title": "Markdown",
"link": "/components/markdown"
},
{
"title": "Variable",
"link": "/components/variable"
},
{
"title": "QuickEdit",
"link": "/components/quick-edit"
},
{
"title": "RichText",
"link": "/components/rich-text"
}
]
},
{
title: 'Block',
type: 'group',
children: [
{
"title": "BlockItem",
"link": "/components/block-item"
},
{
"title": "CardItem",
"link": "/components/card-item"
},
{
"title": "FormItem",
"link": "/components/form-item"
},
{
"title": "FormV2",
"link": "/components/form-v2"
},
{
"title": "TableV2",
"link": "/components/table-v2"
},
{
"title": "Details",
"link": "/components/details"
},
{
"title": "GridCard",
"link": "/components/grid-card"
},
{
"title": "Grid",
"link": "/components/grid"
},
{
"title": "List",
"link": "/components/list"
},
]
},
{
title: 'Others',
type: 'group',
children: [
{
"title": "Tabs",
"link": "/components/tabs"
},
{
"title": "ErrorFallback",
"link": "/components/error-fallback"
},
{
"title": "G2Plot",
"link": "/components/g2plot"
},
{
"title": "Menu",
"link": "/components/menu"
},
{
"title": "Pagination",
"link": "/components/pagination"
},
{
"title": "Preview",
"link": "/components/preview"
},
]
},
]
// '/ui-schema': [
// {
// title: 'Overview',

View File

@ -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` 提供数据记录

View File

@ -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

View File

@ -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 <div>{data?.data?.name}</div>;
});

View File

@ -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 <div>{data?.data?.name}</div>;
});

View File

@ -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 (
<Table
pagination={false}
rowKey={'id'}
loading={loading}
dataSource={data?.data}
columns={[{ title: 'Name', dataIndex: 'name' }]}
/>
);
};
const ComponentB = () => {
console.log('ComponentB');
const apiClient = useAPIClient();
return (
<Space>
<Input />
<Button onClick={() => apiClient.service('test')?.run()}></Button>
</Space>
);
};
export default () => {
return (
<APIClientProvider apiClient={apiClient}>
<ComponentB />
<br />
<br />
<ComponentA />
</APIClientProvider>
);
};

View File

@ -0,0 +1,167 @@
# APIClient
## APIClient
```ts
class APIClient {
// axios 实例
axios: AxiosInstance;
// 缓存带 uid 的 useRequest({}, {uid}) 的结果,可供其他组件调用
services: Record<string, Result<any, any>>;
// 构造器
constructor(instance?: AxiosInstance | AxiosRequestConfig);
// 客户端请求,支持 AxiosRequestConfig 和 ResourceActionOptions
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R>;
// 获取资源
resource<R = IResource>(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 的请求数据
<code src="./demos/demo3.tsx"></code>
## APIClientProvider
提供 APIClient 实例的上下文。
```tsx | pure
const apiClient = new APIClient();
<APIClientProvider apiClient={apiClient}></APIClientProvider>
```
## useAPIClient
获取当前上下文的 APIClient 实例。
```ts
const apiClient = useAPIClient();
```
## useRequest
```ts
function useRequest<P>(
service: AxiosRequestConfig<P> | ResourceActionOptions<P> | FunctionService,
options?: Options<any, any>,
);
```
支持 `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,
}
});
```
例子如下:
<code src="./demos/demo2.tsx"></code>
或者是 NocoBase 的 resource & action 请求:
```ts
const { data, run } = useRequest({
resource: 'users',
action: 'list',
params: {
pageSize: 20,
},
});
// useRequest 传的是 ResourceActionOptions所以 run 直接传 action params 就可以了。
run({
pageSize: 50,
});
```
例子如下:
<code src="./demos/demo1.tsx"></code>
也可以是自定义的异步函数:
```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)

View File

@ -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` 提供数据记录

View File

@ -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

View File

@ -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 <div>{data?.data?.name}</div>;
});

View File

@ -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 <div>{data?.data?.name}</div>;
});

View File

@ -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 (
<Table
pagination={false}
rowKey={'id'}
loading={loading}
dataSource={data?.data}
columns={[{ title: 'Name', dataIndex: 'name' }]}
/>
);
};
const ComponentB = () => {
console.log('ComponentB');
const apiClient = useAPIClient();
return (
<Space>
<Input />
<Button onClick={() => apiClient.service('test')?.run()}></Button>
</Space>
);
};
export default () => {
return (
<APIClientProvider apiClient={apiClient}>
<ComponentB />
<br />
<br />
<ComponentA />
</APIClientProvider>
);
};

View File

@ -0,0 +1,167 @@
# APIClient
## APIClient
```ts
class APIClient {
// axios 实例
axios: AxiosInstance;
// 缓存带 uid 的 useRequest({}, {uid}) 的结果,可供其他组件调用
services: Record<string, Result<any, any>>;
// 构造器
constructor(instance?: AxiosInstance | AxiosRequestConfig);
// 客户端请求,支持 AxiosRequestConfig 和 ResourceActionOptions
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R>;
// 获取资源
resource<R = IResource>(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 的请求数据
<code src="./demos/demo3.tsx"></code>
## APIClientProvider
提供 APIClient 实例的上下文。
```tsx | pure
const apiClient = new APIClient();
<APIClientProvider apiClient={apiClient}></APIClientProvider>
```
## useAPIClient
获取当前上下文的 APIClient 实例。
```ts
const apiClient = useAPIClient();
```
## useRequest
```ts
function useRequest<P>(
service: AxiosRequestConfig<P> | ResourceActionOptions<P> | FunctionService,
options?: Options<any, any>,
);
```
支持 `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,
}
});
```
例子如下:
<code src="./demos/demo2.tsx"></code>
或者是 NocoBase 的 resource & action 请求:
```ts
const { data, run } = useRequest({
resource: 'users',
action: 'list',
params: {
pageSize: 20,
},
});
// useRequest 传的是 ResourceActionOptions所以 run 直接传 action params 就可以了。
run({
pageSize: 50,
});
```
例子如下:
<code src="./demos/demo1.tsx"></code>
也可以是自定义的异步函数:
```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)

View File

@ -166,8 +166,3 @@ useResource 用法:
const resource = useResource('posts');
const resource = useResource('posts.comments', 1);
```
resource 的实际场景用例参见:
- [useCollection()](collection-manager#usecollection)
- [useCollectionField()](collection-manager#usecollectionfield)

View File

@ -386,7 +386,7 @@ describe('Application', () => {
render(<Root />);
await sleep(10);
expect(screen.getByText('Load Plugin Error')).toBeInTheDocument();
expect(screen.getByText('App Error')).toBeInTheDocument();
});
it('replace Component', async () => {

View File

@ -14,7 +14,7 @@ const Loading: FC = () => <div>Loading...</div>;
const AppError: FC<{ error: Error }> = ({ error }) => {
return (
<div>
<div>Load Plugin Error</div>
<div>App Error</div>
{error?.message}
{process.env.__TEST__ && error?.stack}
</div>

View File

@ -18,7 +18,10 @@ interface WithSchemaHookOptions {
displayName?: string;
}
export function withDynamicSchemaProps<T = any>(Component: any, options: WithSchemaHookOptions = {}) {
export function withDynamicSchemaProps<T = any>(
Component: React.ComponentType<T>,
options: WithSchemaHookOptions = {},
) {
const displayName = options.displayName || Component.displayName || Component.name;
const ComponentWithProps: ComponentType<T> = (props) => {
const { dn, findComponent } = useDesignable();

View File

@ -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';

View File

@ -0,0 +1,3 @@
export * from './useDetailsWithPaginationBlockParams';
export * from './useDetailsWithPaginationProps';
export * from './useDetailsWithPaginationDecoratorProps';

View File

@ -0,0 +1,2 @@
export * from './hooks';
export * from './setDataLoadingModeSettingsItem';

View File

@ -0,0 +1,2 @@
export * from './useDetailsDecoratorProps';
export * from './useDetailsProps';

View File

@ -0,0 +1 @@
export * from './hooks';

View File

@ -16,3 +16,4 @@ export * from './tableColumnSettings';
export * from './TableColumnInitializers';
export * from './createTableBlockUISchema';
export * from './hooks/useTableBlockDecoratorProps';
export * from './hooks/useTableBlockProps';

View File

@ -0,0 +1,2 @@
export * from './data-blocks/details-multi';
export * from './data-blocks/details-single';

View File

@ -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';

View File

@ -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<OpenSize, string>([
['small', '40%'],

View File

@ -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.
*/

View File

@ -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 ? <Icon type={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}
<Designer {...designerProps} />

View File

@ -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();
// 新版 UISchema1.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']);

View File

@ -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<ActionContextProps>({});
ActionContext.displayName = 'ActionContext';
@ -46,21 +45,3 @@ export const ActionContextProvider: React.FC<ActionContextProps & { value?: Acti
</ActionContext.Provider>
);
};
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;
}

View File

@ -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;

View File

@ -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 (
<SchemaComponentProvider components={{ Form, Action, Input, FormItem }}>
<ActionContextProvider value={{ visible, setVisible }}>
<a onClick={() => setVisible(true)}>Open</a>
<SchemaComponent schema={schema} />
</ActionContextProvider>
</SchemaComponentProvider>
);
});

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,38 @@
import { getAppComponent } from '@nocobase/test/web';
import { Button, Space } from 'antd';
import React from 'react';
const ComponentButton = (props) => {
return <Button {...props}>Custom Component</Button>;
};
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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
<code src="./demos/demo1.tsx"></code>
/**
* custom component, replace the default Button component
*/
component?: string | ComponentType<any>;
### 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;
<code src="./demos/demo2.tsx"></code>
/**
* When the button is clicked, whether a pop-up confirmation is required
*/
confirm?: false | {
title: string;
content: string;
};
}
```
### 不同的打开方式
### Basic Usage
<code src="./demos/demo3.tsx"></code>
- `ButtonProp`
- title
### Action + Action.Popover
<code src="./demos/new-demos/basic.tsx"></code>
<code src="./demos/demo4.tsx"></code>
### Custom Component
<code src="./demos/demo5.tsx"></code>
- component
### ActionBar
<code src="./demos/new-demos/custom-component.tsx"></code>
<code src="./demos/demo6.tsx"></code>
### 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)。
<code src="./demos/new-demos/dynamic-props.tsx"></code>
### Confirm
- confirm
<code src="./demos/new-demos/confirm.tsx"></code>
## Action.Link
`Button` 组件替换为 `a` 标签。
<code src="./demos/new-demos/action-link.tsx"></code>
## Action.Drawer
主要用于在右侧弹出一个抽屉。
```ts
interface ActionDrawer extends DrawerProps {}
```
### Basic Usage
- `DrawerProps`
<code src="./demos/new-demos/drawer-basic.tsx"></code>
### openSize
<code src="./demos/new-demos/drawer-openSize.tsx"></code>
### Footer
Footer 可以放一些按钮,比如取消或者提交等。
其 Schema `x-component` 必须为 `Action.Drawer.Footer` 组件。
<code src="./demos/new-demos/drawer-footer.tsx"></code>
### With Form
<code src="./demos/new-demos/drawer-with-form.tsx"></code>
## Action.Modal
```ts
interface ActionModal extends ModalProps {}
```
其用法和 `Action.Drawer` 类似,这里只举一个例子。
<code src="./demos/new-demos/action-modal.tsx"></code>
## Action.Popover
注意,此时 Action 的 `popover` 属性必须为 `true`
<code src="./demos/new-demos/action-popover.tsx"></code>
## Action.Container
当根据需要动态渲染内容时,可以使用 `Action.Container` + Action `openMode` 属性动态决定。
<code src="./demos/new-demos/action-container.tsx"></code>
## 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` 的查找和删除。
<code src="./demos/new-demos/actionbar-one-column.tsx"></code>
### two-columns
左右布局,通过 `x-align` 控制。
<code src="./demos/new-demos/actionbar-two-columns.tsx"></code>
## 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 组件进行自定义。
<code src="./demos/new-demos/action-context.tsx"></code>
## ActionSchemaToolbar
用于单个按钮渲染 [SchemaToolbar](/core/ui-schema/schema-toolbar) 和 [SchemaSettings](/core/ui-schema/schema-settings)。
<code src="./demos/new-demos/schema-toolbar.tsx"></code>
## Hooks
### useActionContext()
获取 `ActionContext` 上下文。
```ts
const { visible, setVisible, fieldSchema } = useActionContext();
```

View File

@ -16,3 +16,4 @@ export * from './hooks/useGetAriaLabelOfDrawer';
export * from './hooks/useGetAriaLabelOfModal';
export * from './hooks/useGetAriaLabelOfPopover';
export * from './Action.Designer';
export * from './types';

View File

@ -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<void>;
};
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<void>;
};
export interface ActionProps extends ButtonProps {
/**
* button title
*/
title?: string;
/**
* custom component, replace the default Button component
*/
component?: string | ComponentType<any>;
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<ActionProps> & {
Drawer?: ComposedActionDrawer;
[key: string]: any;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,27 +1,24 @@
---
group:
title: Schema Components
order: 3
---
# AssociationSelect
## Examples
<code src="./demos/demo1.tsx"></code>
## 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<P = any> = RemoteSelectProps<P> & {
action?: string;
multiple?: boolean;
};
```
## Basic Usage
<code src="./demos/new-demos/basic.tsx"></code>
## Multiple Selection
`type` 需要改为 `array`,并且属性需要增加 `multiple: true`
<code src="./demos/new-demos/multiple.tsx"></code>
## Read Pretty
<code src="./demos/new-demos/read-pretty.tsx"></code>

View File

@ -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),
),
);

View File

@ -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<any>();
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;

View File

@ -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;

View File

@ -0,0 +1,20 @@
# AutoComplete
自动完成输出框。其基于 ant-design [AutoComplete](https://ant.design/components/auto-complete) 组件封装。
## Basic Usage
```ts
type AutoCompleteProps = AntdAutoCompleteProps;
```
<code src="./demos/basic.tsx"></code>
## Read Pretty
```ts
type AutoCompleteReadPrettyProps = InputReadPrettyProps;
```
<code src="./demos/read-pretty.tsx"></code>

View File

@ -67,7 +67,13 @@ const useStyles = createStyles(({ css, token }: CustomCreateStylesUtils) => {
`;
});
export const BlockItem: React.FC<any> = withDynamicSchemaProps(
export interface BlockItemProps {
name?: string;
className?: string;
children?: React.ReactNode;
}
export const BlockItem: React.FC<BlockItemProps> = withDynamicSchemaProps(
(props) => {
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { className, children } = useProps(props);

View File

@ -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 (
<div
className="nc-block-item"
style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}
>
{fieldSchema.name}
<DragHandler />
</div>
);
},
{ 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;

View File

@ -1,11 +1,22 @@
---
group:
title: Schema Components
order: 3
---
# BlockItem
普通的装饰器Decorator组件特殊 UI 效果,一般用在 x-decorator 中。用于提供区块的管理,如拖拽功能、当前节点的 SettingsForm。CardItem 和 FormItem 组件都是基于 BlockItem 实现,也具备以上相同功能。
普通的装饰器Decorator组件无 UI 效果,一般用在 `x-decorator` 中。
<code src="./demos/demo1.tsx"></code>
主要提供了以下 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` 组件。
<code src="./demos/new-demos/basic.tsx"></code>

View File

@ -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<CardItemProps> = (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) => {
<BlockItemError>
<BlockItem name={name} className={`${componentCls} ${hashId} noco-card-item`}>
<BlockItemCard ref={ref} {...restProps}>
{inView ? props.children : <Skeleton paragraph={{ rows: 4 }} />}
{inView ? props.children : lazyRenderElement ?? <Skeleton paragraph={{ rows: 4 }} />}
</BlockItemCard>
</BlockItem>
</BlockItemError>,

View File

@ -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;

View File

@ -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;

View File

@ -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)
- 懒渲染
<code src="./demos/demo1.tsx" ></code>
其基于 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
<code src="./demos/new-demos/basic.tsx" ></code>
## Custom lazy render
<code src="./demos/new-demos/lazy-render.tsx" ></code>

View File

@ -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<ArrayField>();
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<ArrayField>();
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 (
<Space split={'/'}>
{labels.map((label, index) => {
if (selectedOptions[index]) {
return <span key={index}>{label}</span>;
}
const item = toArr(value)
.filter(Boolean)
.find((item) => item[fieldNames.value] === label);
return <span key={index}>{item?.[fieldNames.label] || label}</span>;
})}
</Space>
);
};
const handelDropDownVisible = (value) => {
if (value && !field.dataSource?.length) {
run();
}
};
export type CascaderProps<DataNodeType extends BaseOptionType = any> = AntdCascaderProps<DataNodeType> & {
/**
* @deprecated use `x-use-component-props` instead
*/
useLoadData: (props: CascaderProps) => AntdCascaderProps['loadData'];
/**
* @deprecated use `x-use-component-props` instead
*/
useDataSource?: (options: any) => UseRequestResult<unknown>;
/**
* 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 (
<AntdCascader
loading={loading}
{...others}
options={field.dataSource}
loadData={loadData}
changeOnSelect={isBoolean(changeOnSelectLast) ? !changeOnSelectLast : changeOnSelect}
value={toValue()}
fieldNames={fieldNames}
displayRender={displayRender}
onDropdownVisibleChange={handelDropDownVisible}
onChange={(value, selectedOptions) => {
if (value && labelInValue) {
onChange(selectedOptions.map((option) => omit(option, [fieldNames.children])));
} else {
onChange(value);
export const Cascader = withDynamicSchemaProps(
connect(
(props: CascaderProps) => {
const field = useField<ArrayField>();
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'] ? <LoadingOutlined /> : props.suffixIcon,
return item;
});
};
const displayRender = (labels: string[], selectedOptions: any[]) => {
return (
<Space split={'/'}>
{labels.map((label, index) => {
if (selectedOptions[index]) {
return <span key={index}>{label}</span>;
}
const item = toArr(value)
.filter(Boolean)
.find((item) => item[fieldNames.value] === label);
return <span key={index}>{item?.[fieldNames.label] || label}</span>;
})}
</Space>
);
};
const handelDropDownVisible = (value) => {
if (value && !field.dataSource?.length) {
run();
}
};
return (
<AntdCascader
loading={loading}
{...others}
options={field.dataSource}
loadData={loadData}
changeOnSelect={isBoolean(changeOnSelectLast) ? !changeOnSelectLast : changeOnSelect}
value={toValue()}
fieldNames={fieldNames}
displayRender={displayRender}
onDropdownVisibleChange={handelDropDownVisible}
onChange={(value, selectedOptions: any) => {
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'] ? <LoadingOutlined /> : props.suffixIcon,
};
},
),
mapReadPretty(ReadPretty),
),
mapReadPretty(ReadPretty),
{ displayName: 'Cascader' },
);
export default Cascader;

View File

@ -13,7 +13,18 @@ import { toArr } from '@formily/shared';
import React from 'react';
import { defaultFieldNames } from './defaultFieldNames';
export const ReadPretty: React.FC<unknown> = (props: any) => {
interface FieldNames {
label: string;
value: string;
children: string;
}
export interface CascaderReadPrettyProps {
fieldNames?: FieldNames;
value?: any;
}
export const ReadPretty: React.FC<CascaderReadPrettyProps> = (props) => {
const { fieldNames = defaultFieldNames } = props;
const values = toArr(props.value);
const len = values.length;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<any>();
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;

View File

@ -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;

View File

@ -1,36 +1,53 @@
---
group:
title: Schema Components
order: 3
---
# Cascader
## Examples
### Cascader
<code src="./demos/demo1.tsx"></code>
### Asynchronous Data Source
<code src="./demos/demo2.tsx"></code>
## 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<DataNodeType extends BaseOptionType = any> = AntdCascaderProps<DataNodeType> & {
/**
* Whether to wrap the label of option into the value
*/
labelInValue?: boolean;
/**
* must select the last level
*/
changeOnSelectLast?: boolean;
}
```
## Basic Usage
<code src="./demos/new-demos/basic.tsx"></code>
## Asynchronous Data Source
<code src="./demos/new-demos/loadData.tsx"></code>
## labelInValue
如果设置 `labelInValue``true`,则选中的数据为 `{ label: string, value: string }` 格式,否则为 `string` 格式。
<code src="./demos/new-demos/labelInValue.tsx"></code>
## changeOnSelectLast
如果设置 `changeOnSelectLast``true`,则必须选择最后一级,如果为 `false`,则可以选择任意级。
<code src="./demos/new-demos/changeOnSelectLast.tsx"></code>
## Read Pretty
```ts
interface FieldNames {
label: string;
value: string;
children: string;
}
export interface CascaderReadPrettyProps {
fieldNames?: FieldNames;
value?: any;
}
```
<code src="./demos/new-demos/read-pretty.tsx"></code>

View File

@ -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<Partial<any>, string | number | symbol> & React.RefAttributes<unknown>
> & {
Group?: React.FC<CheckboxGroupProps>;
Group?: React.FC<AntdCheckboxGroupProps>;
__ANT_CHECKBOX?: boolean;
ReadPretty?: React.FC<CheckboxProps>;
ReadPretty?: React.FC<CheckboxReadPrettyProps>;
};
const ReadPretty = (props) => {
export interface CheckboxReadPrettyProps {
showUnchecked?: boolean;
value?: boolean;
}
const ReadPretty: FC<CheckboxReadPrettyProps> = (props) => {
if (props.value) {
return <CheckOutlined style={{ color: '#52c41a' }} />;
}
@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,17 +1,46 @@
---
group:
title: Schema Components
order: 3
---
# Checkbox
## Examples
复选框,其基于 ant-design [Checkbox](https://ant.design/components/checkbox/) 组件封装。
### 勾选
<code src="./demos/checkbox.tsx"></code>
## Basic Usage
### 组
```ts
type CheckboxProps = AntdCheckboxProps;
```
<code src="./demos/checkbox.group.tsx"></code>
<code src="./demos/new-demos/basic.tsx"></code>
## Read Pretty
```ts
interface CheckboxReadPrettyProps {
showUnchecked?: boolean;
value?: boolean;
}
```
如果值为 `false`,默认情况下不显示内容,可以通过 `showUnchecked` 属性来显示未选中的复选框。
<code src="./demos/new-demos/read-pretty.tsx"></code>
## Checkbox Group
```ts
type CheckboxGroupProps = CheckboxGroupProps;
```
注意 schema 的 type 属性为 `array`
<code src="./demos/new-demos/group.tsx"></code>
## Checkbox Group Read Pretty
```ts
export interface CheckboxGroupReadPrettyProps {
value?: any[];
ellipsis?: boolean;
}
```
<code src="./demos/new-demos/group-read-pretty.tsx"></code>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +1,24 @@
---
group:
title: Schema Components
---
# CollectionSelect
## Example
用于选择当前数据源的数据表。
```ts
type CollectionSelectProps = SelectProps<any, any> & {
filter?: (item: any, index: number, array: any[]) => boolean;
isTableOid?: boolean;
};
```
## Basic Usage
<code src="./demos/basic.tsx"></code>
## Multiple Selection
`type` 需要改为 `array`,并且属性需要增加 `mode: 'multiple'`
<code src="./demos/multiple.tsx"></code>
## Read Pretty
<code src="./demos/read-pretty.tsx"></code>

View File

@ -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<AntdColorPickerProps, 'onChange'> {
onChange?: (color: string) => void;
}
export const ColorPicker = connect(
(props) => {
(props: ColorPickerProps) => {
const { value, onChange, ...others } = props;
return (
<div role="button" aria-label="color-picker-normal" style={{ display: 'inline-block' }}>

View File

@ -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;

View File

@ -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;

View File

@ -1,18 +1,17 @@
---
group:
title: Schema Components
order: 3
---
# ColorPicker
## Examples
### Basic
<code src="./demos/demo1.tsx"></code>
颜色选择器,其基于 ant-design [ColorPicker](https://ant.design/components/color-picker/) 组件进行封装。
```ts
interface ColorPickerProps extends Omit<AntdColorPickerProps, 'onChange'> {
onChange?: (color: string) => void;
}
```
## Basic Usage
<code src="./demos/new-demos/basic.tsx"></code>
## Read Pretty
<code src="./demos/new-demos/read-pretty.tsx"></code>

View File

@ -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 (
<Select {...props}>

View File

@ -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': 'ColorSelect',
},
},
},
});
export default App;

View File

@ -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: 'red',
title: 'Test',
'x-decorator': 'FormItem',
'x-component': 'ColorSelect',
},
},
},
});
export default App;

View File

@ -1,13 +1,22 @@
---
group:
title: Schema Components
order: 3
---
# ColorSelect
## Examples
颜色下拉选择器,其基于 ant-design [Select](https://ant.design/components/select/) 组件封装。
### ColorSelect usage
## Basic Usage
```ts
type ColorSelectProps = ColorSelectProps;
```
<code src="./demos/new-demos/basic.tsx"></code>
## Read Pretty
```ts
interface ColorSelectReadPrettyProps {
value?: string;
}
```
<code src="./demos/new-demos/read-pretty.tsx"></code>
<code src="./demos/demo1.tsx"></code>

View File

@ -12,11 +12,15 @@ import { connect, mapReadPretty } from '@formily/react';
import { error } from '@nocobase/utils/client';
import cronstrue from 'cronstrue';
import React, { useMemo } from 'react';
import { CronProps, Cron as ReactCron } from 'react-js-cron';
import { CronProps as ReactJsCronProps, Cron as ReactCron } from 'react-js-cron';
import 'react-js-cron/dist/styles.css';
import { useAPIClient } from '../../../api-client';
const Input = (props: Omit<CronProps, 'setValue'> & { onChange: (value: string) => void }) => {
export interface CronProps extends Omit<ReactJsCronProps, 'setValue'> {
onChange: (value: string) => void;
}
const Input = (props: CronProps) => {
const { onChange, ...rest } = props;
return (
<fieldset
@ -45,7 +49,11 @@ const Input = (props: Omit<CronProps, 'setValue'> & { onChange: (value: string)
);
};
const ReadPretty = (props) => {
interface CronReadPrettyProps {
value?: string;
}
const ReadPretty = (props: CronReadPrettyProps) => {
const api = useAPIClient();
const locale = api.auth.getLocale();
const value = useMemo(() => {

View File

@ -15,7 +15,7 @@ import { useCompile } from '../../hooks';
import { EllipsisWithTooltip } from '../input';
import Cron from './Cron';
interface CronSetProps extends SelectProps {
export interface CronSetProps extends SelectProps {
onChange: (v: string) => void;
}
@ -92,7 +92,12 @@ const CronSetInternal = (props: CronSetProps) => {
);
};
const ReadPretty = (props: CronSetProps) => {
export interface CronReadPrettyProps {
value?: string;
options?: SelectProps['options'];
}
const ReadPretty = (props: CronReadPrettyProps) => {
const { value } = props;
const compile = useCompile();
const fieldSchema = useFieldSchema();

View File

@ -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': 'Cron',
},
},
},
});
export default App;

View File

@ -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: '13 6 11 * *',
title: 'Test',
'x-decorator': 'FormItem',
'x-component': 'Cron',
},
},
},
});
export default App;

View File

@ -0,0 +1,40 @@
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': 'CronSet',
'x-component-props': {
options: [
{
label: '{{t("Daily")}}',
value: '0 0 0 * * ?',
},
{
label: '{{t("Weekly")}}',
value: 'every_week',
},
{
label: '{{t("Monthly")}}',
value: 'every_month',
},
{
label: '{{t("Yearly")}}',
value: 'every_year',
},
],
},
},
},
},
});
export default App;

View File

@ -0,0 +1,34 @@
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: '0 0 0 * * ?',
title: 'Test',
'x-decorator': 'FormItem',
'x-component': 'CronSet',
'x-component-props': {
options: [
{
label: '{{t("Daily")}}',
value: '0 0 0 * * ?',
},
{
label: '{{t("Weekly")}}',
value: '* * * * * ?',
},
],
},
},
},
},
});
export default App;

View File

@ -1,16 +1,53 @@
---
group:
title: Schema Components
---
# Cron
## Example
定时任务表达式组件,其基于 [react-js-cron](https://github.com/xrutayisire/react-js-cron) 封装。
#### Cron
## Cron
<code src="./demos/demo1.tsx"></code>
## Basic Usage
#### CronSet
```ts
import { CronProps as ReactJsCronProps } from 'react-js-cron';
interface CronProps extends Omit<ReactJsCronProps, 'setValue'> {
onChange: (value: string) => void
}
```
<code src="./demos/new-demos/cron-basic.tsx"></code>
## Read Pretty
```ts
interface CronReadPrettyProps {
value?: string;
}
```
<code src="./demos/new-demos/cron-read-pretty.tsx"></code>
## CronSet
## Basic Usage
```ts
interface CronSetProps extends SelectProps {
onChange: (v: string) => void;
}
```
<code src="./demos/new-demos/cronset-basic.tsx"></code>
## Read Pretty
```ts
interface CronReadPrettyProps {
value?: string;
options?: SelectProps['options'];
}
```
<code src="./demos/new-demos/cronset-read-pretty.tsx"></code>
<code src="./demos/demo2.tsx"></code>

View File

@ -8,23 +8,24 @@
*/
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { DatePicker as AntdDatePicker } from 'antd';
import type {
DatePickerProps as AntdDatePickerProps,
RangePickerProps as AntdRangePickerProps,
} from 'antd/es/date-picker';
import React from 'react';
import { DatePicker as AntdDatePicker, DatePickerProps as AntdDatePickerProps } from 'antd';
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { ReadPretty } from './ReadPretty';
import { ReadPretty, ReadPrettyComposed } from './ReadPretty';
import { getDateRanges, mapDatePicker, mapRangePicker } from './util';
import { RangePickerProps } from 'antd/es/date-picker';
interface IDatePickerProps {
utc?: boolean;
}
type ComposedDatePicker = React.FC<AntdDatePickerProps> & {
ReadPretty?: React.FC<AntdDatePickerProps>;
RangePicker?: React.FC<AntdRangePickerProps>;
ReadPretty?: ReadPrettyComposed['DatePicker'];
RangePicker?: ComposedRangePicker;
};
type ComposedRangePicker = React.FC<RangePickerProps> & {
ReadPretty?: ReadPrettyComposed['DateRangePicker'];
};
const DatePickerContext = React.createContext<IDatePickerProps>({ utc: true });
@ -44,11 +45,11 @@ const InternalRangePicker = connect(
mapReadPretty(ReadPretty.DateRangePicker),
);
export const DatePicker = (props) => {
export const DatePicker: ComposedDatePicker = (props) => {
const { utc = true } = useDatePickerContext();
const value = Array.isArray(props.value) ? props.value[0] : props.value;
props = { utc, ...props };
return <InternalDatePicker {...props} value={value} />;
const newProps = { utc, ...props };
return <InternalDatePicker {...newProps} value={value} />;
};
DatePicker.ReadPretty = ReadPretty.DatePicker;
@ -78,8 +79,8 @@ DatePicker.RangePicker = function RangePicker(props) {
{ label: t('Last 90 days'), value: rangesValue.last90Days },
{ label: t('Next 90 days'), value: rangesValue.next90Days },
];
props = { utc, presets, ...props };
return <InternalRangePicker {...props} />;
const newProps: any = { utc, presets, ...props };
return <InternalRangePicker {...newProps} />;
};
export default DatePicker;

View File

@ -9,23 +9,32 @@
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
import { isArr } from '@formily/shared';
import { getDefaultFormat, str2moment } from '@nocobase/utils/client';
import type {
DatePickerProps as AntdDatePickerProps,
RangePickerProps as AntdRangePickerProps,
} from 'antd/es/date-picker';
import {
GetDefaultFormatProps,
Str2momentOptions,
Str2momentValue,
getDefaultFormat,
str2moment,
} from '@nocobase/utils/client';
import cls from 'classnames';
import dayjs from 'dayjs';
import React from 'react';
type Composed = {
DatePicker: React.FC<AntdDatePickerProps>;
DateRangePicker: React.FC<AntdRangePickerProps>;
export type ReadPrettyComposed = {
DatePicker: React.FC<ReadPrettyDatePickerProps>;
DateRangePicker: React.FC<DateRangePickerReadPrettyProps>;
};
export const ReadPretty: Composed = () => null;
export const ReadPretty: ReadPrettyComposed = () => null;
ReadPretty.DatePicker = function DatePicker(props: any) {
export interface ReadPrettyDatePickerProps extends Str2momentOptions, GetDefaultFormatProps {
value?: Str2momentValue;
className?: string;
prefixCls?: string;
showTime?: boolean;
}
ReadPretty.DatePicker = function DatePicker(props) {
const prefixCls = usePrefixCls('description-date-picker', props);
if (!props.value) {
@ -41,7 +50,14 @@ ReadPretty.DatePicker = function DatePicker(props: any) {
return <div className={cls(prefixCls, props.className)}>{getLabels()}</div>;
};
ReadPretty.DateRangePicker = function DateRangePicker(props: any) {
export interface DateRangePickerReadPrettyProps extends Str2momentOptions, GetDefaultFormatProps {
value?: Str2momentValue;
className?: string;
prefixCls?: string;
style?: React.CSSProperties;
}
ReadPretty.DateRangePicker = function DateRangePicker(props: DateRangePickerReadPrettyProps) {
const prefixCls = usePrefixCls('description-text', props);
const format = getDefaultFormat(props);
const getLabels = () => {

Some files were not shown because too many files have changed in this diff Show More