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
 | 
			
		||||
 | 
			
		||||
## 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 './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';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user