mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 11:56:29 +00:00
feat: cacader component
* feat: add Cacader component into schema component * docs: change document language * Nocobase next color select (#157) * feat: add useCompile hook * feat: add ColorSelect component into schema component * optimize: rename checkbox component file (#155) * optimize: rename checkbox component file * optimize: rename checkbox component file * feat: optimize antd-config-orovider * feat: move admin-layout and auth-layout to route-switch * feat: add application demo * feat: add radio into schema component (#154) * feat: add radio to schema component * docs: add Radio demos into schema component * rafactor: change import path * docs: change demo language * fix: add title and description to demos Co-authored-by: chenos <chenlinxh@gmail.com> * feat: add input into schema component (#158) * feat: add Input Component to schema compoennt * code cleanup Co-authored-by: chenos <chenlinxh@gmail.com> * feat: add RemoteSchemaComponent * feat: add admin layout * feat: add menu into schema component * fix: remove antd/dist/antd.css * feat: improve application demo * feat: add DatePicker into schema components (#161) * feat: add DatePicker into schema components * improve code Co-authored-by: chenos <chenlinxh@gmail.com> * feature/nocobase next password (#159) * feat: add Password Component into schema components * improve code Co-authored-by: chenos <chenlinxh@gmail.com> * feat: add InputNumber Component into schema component (#160) * feat: add InputNumber Component into schema component * improve code Co-authored-by: chenos <chenlinxh@gmail.com> * feat(client-menu): improve code * feat: acl (#153) * feat: getRepository * getRepository return type * export action * add: acl * feat: setResourceAction * feat: action alias * chore: code struct * feat: removeResourceAction * chore: file name * ignorecase * remove ACL * feat: ACL * feat: role toJSON * using emit Co-authored-by: chenos <chenlinxh@gmail.com> * Acl (#162) * feat: getRepository * getRepository return type * export action * add: acl * feat: setResourceAction * feat: action alias * chore: code struct * feat: removeResourceAction * chore: file name * ignorecase * remove ACL * feat: ACL * feat: role toJSON * using emit * chore: test Co-authored-by: chenos <chenlinxh@gmail.com> * Feat/plugin collection manager (#147) * refactor: collection manager plugin * feat(database): magic attribute model * MagicAttributeModel * load collections & fields options * collections filterTargetKey * Feat/plugin UI schema v0.6 (#143) * v0.6 * plugin-ui-schema: insert && getJsonSchema * plugin-ui-schema: insert schema with sort * plugin-ui-schema: node with x-index * insert adjacent method * chore: insert * typo * insert with x-uid * fix: getSchema by subtree * add ui-schema actions * fix: mysql compatibility * remove ui-schema when remove node tree * ui schema patch * ui_schemas.create * test cases * test cases * fix(database): reset changed before update * feat: insert ui schema node after created * feat: patch ui schema node after updated * fix: sqlite error * uid * cleanup * test cases * feat: ui_schema items type support * fix: insert items node * fix: get inner type * change items struct * add insert return value * add insert return value Co-authored-by: chenos <chenlinxh@gmail.com> * update yarn.lock * rename * feat(client): plugin manager toolbar * feat: add demo for plugin manager toolbar * feat: improve the toolbar of the plugin manager * feat: improve document title and page title support * feat: add IconPicker component into schema components * feat: improve icon component * migrate TimePicker component into schema components (#164) * feat: add TimePicker component into schema components * improve * TimePicker.RangePicker Co-authored-by: chenos <chenlinxh@gmail.com> * Load Options Lazily * feat: extract read pretty * fix: useEffect only on update * feat: add Cacader component into schema component * docs: change document language * feat: extract read pretty * Load Options Lazily * improve code Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: ChengLei Shao <chareice@live.com>
This commit is contained in:
parent
f882ab42c5
commit
48722c7c99
@ -0,0 +1,96 @@
|
|||||||
|
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 } from 'antd';
|
||||||
|
import { isBoolean, omit } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import { defaultFieldNames } from './defaultFieldNames';
|
||||||
|
import { ReadPretty } from './ReadPretty';
|
||||||
|
|
||||||
|
const useDefLoadData = (props: any) => props.loadData;
|
||||||
|
|
||||||
|
export const Cascader = connect(
|
||||||
|
(props: any) => {
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
labelInValue,
|
||||||
|
fieldNames = defaultFieldNames,
|
||||||
|
useLoadData = useDefLoadData,
|
||||||
|
changeOnSelectLast,
|
||||||
|
changeOnSelect,
|
||||||
|
...others
|
||||||
|
} = props;
|
||||||
|
const loadData = useLoadData(props);
|
||||||
|
// 兼容值为 object[] 的情况
|
||||||
|
const toValue = () => {
|
||||||
|
return toArr(value).map((item) => {
|
||||||
|
if (typeof item === 'object') {
|
||||||
|
return item[fieldNames.value];
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const displayRender = (labels: string[], selectedOptions: any[]) => {
|
||||||
|
const values = toArr(value);
|
||||||
|
if (values.length !== labels.length) {
|
||||||
|
labels = toValue();
|
||||||
|
selectedOptions = values;
|
||||||
|
}
|
||||||
|
if (selectedOptions.length === 0) {
|
||||||
|
selectedOptions = values;
|
||||||
|
}
|
||||||
|
return labels.map((label, i) => {
|
||||||
|
let option = selectedOptions[i];
|
||||||
|
if (!option || typeof option === 'string' || typeof option === 'number') {
|
||||||
|
option = { [fieldNames.label]: label, [fieldNames.value]: label };
|
||||||
|
}
|
||||||
|
if (i === labels.length - 1) {
|
||||||
|
return <span key={option[fieldNames.value]}>{option[fieldNames.label]}</span>;
|
||||||
|
}
|
||||||
|
return <span key={option[fieldNames.value]}>{option[fieldNames.label]} / </span>;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// if (loadData) {
|
||||||
|
// Object.assign(others, {
|
||||||
|
// loadData: (selectedOptions: any[]) => {
|
||||||
|
// // 将 field 传给 loadData
|
||||||
|
// loadData(selectedOptions, field);
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
return (
|
||||||
|
<AntdCascader
|
||||||
|
{...others}
|
||||||
|
loadData={loadData}
|
||||||
|
changeOnSelect={isBoolean(changeOnSelectLast) ? !changeOnSelectLast : changeOnSelect}
|
||||||
|
value={toValue()}
|
||||||
|
fieldNames={fieldNames}
|
||||||
|
displayRender={displayRender}
|
||||||
|
onChange={(value, selectedOptions) => {
|
||||||
|
if (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),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Cascader;
|
@ -0,0 +1,35 @@
|
|||||||
|
import { ArrayField } from '@formily/core';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { toArr } from '@formily/shared';
|
||||||
|
import React from 'react';
|
||||||
|
import { defaultFieldNames } from './defaultFieldNames';
|
||||||
|
|
||||||
|
export const ReadPretty: React.FC<unknown> = (props: any) => {
|
||||||
|
const { fieldNames = defaultFieldNames } = props;
|
||||||
|
const values = toArr(props.value);
|
||||||
|
const len = values.length;
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
let dataSource = field.dataSource;
|
||||||
|
const data = [];
|
||||||
|
for (const item of values) {
|
||||||
|
if (typeof item === 'object') {
|
||||||
|
data.push(item);
|
||||||
|
} else {
|
||||||
|
const curr = dataSource?.find((v) => v[fieldNames.value] === item);
|
||||||
|
dataSource = curr?.[fieldNames.children] || [];
|
||||||
|
data.push(curr || { label: item, value: item });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{data.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<span key={index}>
|
||||||
|
{typeof item === 'object' ? item[fieldNames.label] : item}
|
||||||
|
{len > index + 1 && ' / '}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,5 @@
|
|||||||
|
export const defaultFieldNames = {
|
||||||
|
label: 'label',
|
||||||
|
value: 'value',
|
||||||
|
children: 'children',
|
||||||
|
};
|
@ -0,0 +1,83 @@
|
|||||||
|
import { FormItem } from '@formily/antd';
|
||||||
|
import { Cascader, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
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 schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'string',
|
||||||
|
title: `编辑模式`,
|
||||||
|
name: 'name1',
|
||||||
|
enum: options,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Cascader',
|
||||||
|
'x-component-props': {
|
||||||
|
// changeOnSelect: true,
|
||||||
|
},
|
||||||
|
'x-reactions': {
|
||||||
|
target: 'read',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: 'string',
|
||||||
|
title: `阅读模式`,
|
||||||
|
enum: options,
|
||||||
|
name: 'name2',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Cascader',
|
||||||
|
'x-component-props': {
|
||||||
|
changeOnSelect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Cascader, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,123 @@
|
|||||||
|
import { FormItem } from '@formily/antd';
|
||||||
|
import { ArrayField } from '@formily/core';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { action } from '@formily/reactive';
|
||||||
|
import { APIClient, APIClientProvider, Cascader, SchemaComponent, SchemaComponentProvider } 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('/china_regions').reply(200, {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: 'zhejiang',
|
||||||
|
label: 'Zhejiang',
|
||||||
|
isLeaf: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'jiangsu',
|
||||||
|
label: 'Jiangsu',
|
||||||
|
isLeaf: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const useAsyncDataSource = (api: APIClient) => (field) => {
|
||||||
|
field.loading = true;
|
||||||
|
api.request({ url: 'china_regions' }).then(
|
||||||
|
action.bound((res) => {
|
||||||
|
field.dataSource = res?.data?.data || [];
|
||||||
|
field.loading = false;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useLoadData = (api: APIClient) => {
|
||||||
|
return () => {
|
||||||
|
// hook 写在这里
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
return (selectedOptions) => {
|
||||||
|
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||||
|
targetOption.loading = true;
|
||||||
|
// load options lazily
|
||||||
|
setTimeout(() => {
|
||||||
|
targetOption.loading = false;
|
||||||
|
targetOption.children = [
|
||||||
|
{
|
||||||
|
label: `${targetOption.label} Dynamic 1`,
|
||||||
|
value: 'dynamic1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `${targetOption.label} Dynamic 2`,
|
||||||
|
value: 'dynamic2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
field.dataSource = [...field.dataSource];
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'string',
|
||||||
|
title: `编辑模式`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Cascader',
|
||||||
|
'x-component-props': {
|
||||||
|
changeOnSelectLast: false,
|
||||||
|
labelInValue: true,
|
||||||
|
maxLevel: 3,
|
||||||
|
useLoadData: '{{useLoadData(apiClient)}}',
|
||||||
|
// fieldNames: {
|
||||||
|
// label: 'name',
|
||||||
|
// value: 'code',
|
||||||
|
// children: 'children',
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
'x-reactions': [
|
||||||
|
'{{useAsyncDataSource(apiClient)}}',
|
||||||
|
{
|
||||||
|
target: 'read',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: 'string',
|
||||||
|
title: `阅读模式`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Cascader',
|
||||||
|
'x-component-props': {
|
||||||
|
changeOnSelectLast: false,
|
||||||
|
labelInValue: true,
|
||||||
|
maxLevel: 3,
|
||||||
|
// fieldNames: {
|
||||||
|
// label: 'name',
|
||||||
|
// value: 'code',
|
||||||
|
// children: 'children',
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<APIClientProvider apiClient={apiClient}>
|
||||||
|
<SchemaComponentProvider components={{ Cascader, FormItem }}>
|
||||||
|
<SchemaComponent scope={{ apiClient, useLoadData, useAsyncDataSource }} schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
</APIClientProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -6,3 +6,32 @@ group:
|
|||||||
---
|
---
|
||||||
|
|
||||||
# Cascader
|
# Cascader
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Cascader
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx" />
|
||||||
|
|
||||||
|
### Asynchronous Data Source
|
||||||
|
|
||||||
|
<code src="./demos/demo2.tsx" />
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
基于 antd 的 [Cascader](https://ant.design/components/cascader/#API) 附加的一些属性:
|
||||||
|
|
||||||
|
- `labelInValue` 是否把每个选项的 label 包装到 value 中
|
||||||
|
- `changeOnSelectLast` 必须选到最后一级
|
||||||
|
- `useLoadData` 可调用 hook 的 loadData
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
useLoadData: (props) => {
|
||||||
|
// 这里可以写 hook
|
||||||
|
return function loadData(selectedOptions) {
|
||||||
|
// Cascader 的 loadData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Cascader';
|
@ -1,5 +1,6 @@
|
|||||||
export * from './action';
|
export * from './action';
|
||||||
export * from './block-item';
|
export * from './block-item';
|
||||||
|
export * from './cascader';
|
||||||
export * from './checkbox';
|
export * from './checkbox';
|
||||||
export * from './color-select';
|
export * from './color-select';
|
||||||
export * from './date-picker';
|
export * from './date-picker';
|
||||||
@ -15,3 +16,4 @@ export * from './page';
|
|||||||
export * from './password';
|
export * from './password';
|
||||||
export * from './radio';
|
export * from './radio';
|
||||||
export * from './time-picker';
|
export * from './time-picker';
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user