From 48722c7c99ee66391cd1312a88cb8e5054ceaedb Mon Sep 17 00:00:00 2001 From: SemmyWong <67748948+semmywong@users.noreply.github.com> Date: Fri, 21 Jan 2022 09:32:13 +0800 Subject: [PATCH] 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 * feat: add input into schema component (#158) * feat: add Input Component to schema compoennt * code cleanup Co-authored-by: chenos * 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 * feature/nocobase next password (#159) * feat: add Password Component into schema components * improve code Co-authored-by: chenos * feat: add InputNumber Component into schema component (#160) * feat: add InputNumber Component into schema component * improve code Co-authored-by: chenos * 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 * 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 * 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 * 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 * 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 Co-authored-by: ChengLei Shao --- .../antd/cascader/Cascader.tsx | 96 ++++++++++++++ .../antd/cascader/ReadPretty.tsx | 35 +++++ .../antd/cascader/defaultFieldNames.ts | 5 + .../antd/cascader/demos/demo1.tsx | 83 ++++++++++++ .../antd/cascader/demos/demo2.tsx | 123 ++++++++++++++++++ .../schema-component/antd/cascader/index.md | 29 +++++ .../schema-component/antd/cascader/index.ts | 1 + .../client/src/schema-component/antd/index.ts | 2 + 8 files changed, 374 insertions(+) create mode 100644 packages/client/src/schema-component/antd/cascader/Cascader.tsx create mode 100644 packages/client/src/schema-component/antd/cascader/ReadPretty.tsx create mode 100644 packages/client/src/schema-component/antd/cascader/defaultFieldNames.ts create mode 100644 packages/client/src/schema-component/antd/cascader/demos/demo1.tsx create mode 100644 packages/client/src/schema-component/antd/cascader/demos/demo2.tsx create mode 100644 packages/client/src/schema-component/antd/cascader/index.ts diff --git a/packages/client/src/schema-component/antd/cascader/Cascader.tsx b/packages/client/src/schema-component/antd/cascader/Cascader.tsx new file mode 100644 index 000000000..c7f8898c9 --- /dev/null +++ b/packages/client/src/schema-component/antd/cascader/Cascader.tsx @@ -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(); + 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 {option[fieldNames.label]}; + } + return {option[fieldNames.label]} / ; + }); + }; + // if (loadData) { + // Object.assign(others, { + // loadData: (selectedOptions: any[]) => { + // // 将 field 传给 loadData + // loadData(selectedOptions, field); + // }, + // }); + // } + return ( + { + 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'] ? : props.suffixIcon, + }; + }, + ), + mapReadPretty(ReadPretty), +); + +export default Cascader; diff --git a/packages/client/src/schema-component/antd/cascader/ReadPretty.tsx b/packages/client/src/schema-component/antd/cascader/ReadPretty.tsx new file mode 100644 index 000000000..aa4457e96 --- /dev/null +++ b/packages/client/src/schema-component/antd/cascader/ReadPretty.tsx @@ -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 = (props: any) => { + const { fieldNames = defaultFieldNames } = props; + const values = toArr(props.value); + const len = values.length; + const field = useField(); + 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 ( +
+ {data.map((item, index) => { + return ( + + {typeof item === 'object' ? item[fieldNames.label] : item} + {len > index + 1 && ' / '} + + ); + })} +
+ ); +}; diff --git a/packages/client/src/schema-component/antd/cascader/defaultFieldNames.ts b/packages/client/src/schema-component/antd/cascader/defaultFieldNames.ts new file mode 100644 index 000000000..da692434e --- /dev/null +++ b/packages/client/src/schema-component/antd/cascader/defaultFieldNames.ts @@ -0,0 +1,5 @@ +export const defaultFieldNames = { + label: 'label', + value: 'value', + children: 'children', +}; diff --git a/packages/client/src/schema-component/antd/cascader/demos/demo1.tsx b/packages/client/src/schema-component/antd/cascader/demos/demo1.tsx new file mode 100644 index 000000000..ac0099b1d --- /dev/null +++ b/packages/client/src/schema-component/antd/cascader/demos/demo1.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/client/src/schema-component/antd/cascader/demos/demo2.tsx b/packages/client/src/schema-component/antd/cascader/demos/demo2.tsx new file mode 100644 index 000000000..a23b5794d --- /dev/null +++ b/packages/client/src/schema-component/antd/cascader/demos/demo2.tsx @@ -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(); + 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 ( + + + + + + ); +}; diff --git a/packages/client/src/schema-component/antd/cascader/index.md b/packages/client/src/schema-component/antd/cascader/index.md index f0c79efbd..59dbe23e2 100644 --- a/packages/client/src/schema-component/antd/cascader/index.md +++ b/packages/client/src/schema-component/antd/cascader/index.md @@ -6,3 +6,32 @@ group: --- # Cascader + +## Examples + +### Cascader + + + +### Asynchronous Data Source + + + +## 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 + } + } +} +``` \ No newline at end of file diff --git a/packages/client/src/schema-component/antd/cascader/index.ts b/packages/client/src/schema-component/antd/cascader/index.ts new file mode 100644 index 000000000..9da1b7038 --- /dev/null +++ b/packages/client/src/schema-component/antd/cascader/index.ts @@ -0,0 +1 @@ +export * from './Cascader'; diff --git a/packages/client/src/schema-component/antd/index.ts b/packages/client/src/schema-component/antd/index.ts index 796aab998..c14d01fcd 100644 --- a/packages/client/src/schema-component/antd/index.ts +++ b/packages/client/src/schema-component/antd/index.ts @@ -1,5 +1,6 @@ export * from './action'; export * from './block-item'; +export * from './cascader'; export * from './checkbox'; export * from './color-select'; export * from './date-picker'; @@ -15,3 +16,4 @@ export * from './page'; export * from './password'; export * from './radio'; export * from './time-picker'; +