feat: support use select field (#1105)

* feat: add RemoteSelect

* feat: match

* feat: support use select field

* fix: revert no need code

* fix: remove console

* feat: reuse select

* feat: support multiple value and labelInValue

* fix: incorrect import

* fix: missing title in RemoteSelect

* feat: add association-select

* fix: edit

* feat: support sort

* feat: support filter

* fix: break

* fix: loss of filter data after closing the drawer

* fix: multiple not support in select

* fix: some bugs in multiple

* feat: support select in o2m,o2o

* feat: support more association field

* feat: improve

* fix: remove unused files

* feat: make o2m also support pattern

* fix: missing value on edit
This commit is contained in:
Dunqing 2022-11-28 10:08:01 +08:00 committed by GitHub
parent 4a113774bd
commit 0b6fed79d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1201 additions and 236 deletions

View File

@ -10,7 +10,7 @@ import { assign } from './assign';
type FunctionService = (...args: any[]) => Promise<any>; type FunctionService = (...args: any[]) => Promise<any>;
type ResourceActionOptions<P = any> = { export type ResourceActionOptions<P = any> = {
resource?: string; resource?: string;
resourceOf?: any; resourceOf?: any;
action?: string; action?: string;

View File

@ -47,10 +47,19 @@ export const linkTo: IField = {
}, },
schemaInitialize(schema: ISchema, { readPretty, block }) { schemaInitialize(schema: ISchema, { readPretty, block }) {
if (block === 'Form') { if (block === 'Form') {
schema['properties'] = { if (schema['x-component'] === 'AssociationSelect') {
viewer: cloneDeep(recordPickerViewer), Object.assign(schema, {
selector: cloneDeep(recordPickerSelector), type: 'string',
}; 'x-designer': 'AssociationSelect.Designer',
});
} else {
schema.type = 'string';
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
selector: cloneDeep(recordPickerSelector),
};
}
return schema
} else { } else {
if (readPretty) { if (readPretty) {
schema['properties'] = { schema['properties'] = {

View File

@ -1,7 +1,13 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { defaultProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties'; import {
defaultProps,
recordPickerSelector,
recordPickerViewer,
relationshipType,
reverseFieldProperties,
} from './properties';
import { IField } from './types'; import { IField } from './types';
export const m2m: IField = { export const m2m: IField = {
@ -47,20 +53,28 @@ export const m2m: IField = {
}, },
schemaInitialize(schema: ISchema, { readPretty, block }) { schemaInitialize(schema: ISchema, { readPretty, block }) {
if (block === 'Form') { if (block === 'Form') {
schema['properties'] = { if (schema['x-component'] === 'AssociationSelect') {
viewer: cloneDeep(recordPickerViewer), Object.assign(schema, {
selector: cloneDeep(recordPickerSelector), type: 'string',
}; 'x-designer': 'AssociationSelect.Designer',
} else { });
if (readPretty) { } else {
schema.type = 'string';
schema['properties'] = { schema['properties'] = {
viewer: cloneDeep(recordPickerViewer), viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector), selector: cloneDeep(recordPickerSelector),
} };
} }
return schema;
}
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
};
} }
if (['Table', 'Kanban'].includes(block)) { if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props'] = schema['x-component-props'] || {};

View File

@ -1,6 +1,12 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties'; import {
constraintsProps,
recordPickerSelector,
recordPickerViewer,
relationshipType,
reverseFieldProperties,
} from './properties';
import { IField } from './types'; import { IField } from './types';
export const m2o: IField = { export const m2o: IField = {
@ -44,63 +50,30 @@ export const m2o: IField = {
}, },
}, },
}, },
schemaInitialize(schema: ISchema, { field, block, readPretty, action }) { schemaInitialize(schema: ISchema, { block, readPretty }) {
if (block === 'Form' && schema['x-component'] === 'FormField') { if (block === 'Form') {
const association = `${field.collectionName}.${field.name}`; if (schema['x-component'] === 'AssociationSelect') {
schema.type = 'void'; Object.assign(schema, {
schema.properties = { type: 'string',
block: { 'x-designer': 'AssociationSelect.Designer',
type: 'void', });
'x-decorator': 'FormFieldProvider', } else {
'x-decorator-props': { schema.type = 'string';
collection: field.target,
association: association,
resource: association,
action,
fieldName: field.name,
readPretty
},
'x-component': 'CardItem',
'x-component-props': {
bordered: true,
},
properties: {
[field.name]: {
type: 'object',
'x-component': 'FormV2',
'x-component-props': {
useProps: '{{ useFormFieldProps }}',
},
properties: {
__form_grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'FormItemInitializers',
properties: {},
},
}
},
},
},
}
} else {
schema.type = 'string';
if (block === 'Form') {
schema['properties'] = { schema['properties'] = {
viewer: cloneDeep(recordPickerViewer), viewer: cloneDeep(recordPickerViewer),
selector: cloneDeep(recordPickerSelector), selector: cloneDeep(recordPickerSelector),
}; };
} else {
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
}
}
} }
return schema;
}
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
};
} }
if (['Table', 'Kanban'].includes(block)) { if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props'] = schema['x-component-props'] || {};

View File

@ -1,6 +1,12 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties'; import {
constraintsProps,
recordPickerSelector,
recordPickerViewer,
relationshipType,
reverseFieldProperties,
} from './properties';
import { IField } from './types'; import { IField } from './types';
export const o2m: IField = { export const o2m: IField = {
@ -45,76 +51,72 @@ export const o2m: IField = {
}, },
}, },
schemaInitialize(schema: ISchema, { field, block, readPretty }) { schemaInitialize(schema: ISchema, { field, block, readPretty }) {
if (block === 'Form' && schema['x-component'] === 'TableField') { if (block === 'Form') {
const association = `${field.collectionName}.${field.name}`; if (schema['x-component'] === 'TableField') {
schema['type'] = 'void'; const association = `${field.collectionName}.${field.name}`;
schema['properties'] = { schema['type'] = 'void';
block: { schema['properties'] = {
type: 'void', block: {
'x-decorator': 'TableFieldProvider', type: 'void',
'x-decorator-props': { 'x-decorator': 'TableFieldProvider',
collection: field.target, 'x-decorator-props': {
association: association, collection: field.target,
resource: association, association: association,
action: 'list', resource: association,
params: { action: 'list',
paginate: false, params: {
paginate: false,
},
showIndex: true,
dragSort: false,
fieldName: field.name,
}, },
showIndex: true, properties: {
dragSort: false, actions: {
fieldName: field.name, type: 'void',
}, 'x-initializer': 'SubTableActionInitializers',
properties: { 'x-component': 'TableField.ActionBar',
actions: { 'x-component-props': {},
type: 'void', },
'x-initializer': 'SubTableActionInitializers', [field.name]: {
'x-component': 'TableField.ActionBar', type: 'array',
'x-component-props': {}, 'x-initializer': 'TableColumnInitializers',
}, 'x-component': 'TableV2',
[field.name]: { 'x-component-props': {
type: 'array', rowSelection: {
'x-initializer': 'TableColumnInitializers', type: 'checkbox',
'x-component': 'TableV2', },
'x-component-props': { useProps: '{{ useTableFieldProps }}',
rowSelection: {
type: 'checkbox',
}, },
useProps: '{{ useTableFieldProps }}',
}, },
}, },
}, },
}, };
}; } else if (schema['x-component'] === 'AssociationSelect') {
} else { Object.assign(schema, {
// schema['x-component'] = 'CollectionField'; type: 'string',
// schema.type = 'array'; 'x-designer': 'AssociationSelect.Designer',
});
if (block === 'Form') { } else {
schema.type = 'string';
schema['properties'] = { schema['properties'] = {
viewer: cloneDeep(recordPickerViewer), viewer: cloneDeep(recordPickerViewer),
selector: cloneDeep(recordPickerSelector), selector: cloneDeep(recordPickerSelector),
}; };
} else {
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
};
}
} }
return schema;
} }
// if (readPretty) {
// schema['properties'] = { if (readPretty) {
// viewer: cloneDeep(recordPickerViewer), schema['properties'] = {
// }; viewer: cloneDeep(recordPickerViewer),
// } else { };
// schema['properties'] = { } else {
// selector: cloneDeep(recordPickerSelector), schema['properties'] = {
// }; selector: cloneDeep(recordPickerSelector),
// } };
}
if (['Table', 'Kanban'].includes(block)) { if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props'] = schema['x-component-props'] || {};
schema['x-component-props']['ellipsis'] = true; schema['x-component-props']['ellipsis'] = true;

View File

@ -1,65 +1,77 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties'; import {
constraintsProps,
recordPickerSelector,
recordPickerViewer,
relationshipType,
reverseFieldProperties,
} from './properties';
import { IField } from './types'; import { IField } from './types';
const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, action }) => { const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, action }) => {
if (block === 'Form' && schema['x-component'] === 'FormField') { if (block === 'Form') {
const association = `${field.collectionName}.${field.name}`; if (schema['x-component'] === 'FormField') {
schema.type = 'void'; const association = `${field.collectionName}.${field.name}`;
schema.properties = { schema.type = 'void';
block: { schema.properties = {
type: 'void', block: {
'x-decorator': 'FormFieldProvider', type: 'void',
'x-decorator-props': { 'x-decorator': 'FormFieldProvider',
collection: field.target, 'x-decorator-props': {
association: association, collection: field.target,
resource: association, association: association,
action: action, resource: association,
fieldName: field.name, action: action,
readPretty, fieldName: field.name,
}, readPretty,
'x-component': 'CardItem', },
'x-component-props': { 'x-component': 'CardItem',
bordered: true, 'x-component-props': {
}, bordered: true,
properties: { },
[field.name]: { properties: {
type: 'object', [field.name]: {
'x-component': 'FormV2', type: 'object',
'x-component-props': { 'x-component': 'FormV2',
useProps: '{{ useFormFieldProps }}', 'x-component-props': {
}, useProps: '{{ useFormFieldProps }}',
properties: { },
__form_grid: { properties: {
type: 'void', __form_grid: {
'x-component': 'Grid', type: 'void',
'x-initializer': 'FormItemInitializers', 'x-component': 'Grid',
properties: {}, 'x-initializer': 'FormItemInitializers',
properties: {},
},
}, },
}, },
}, },
}, },
}, };
}; } else if (schema['x-component'] === 'AssociationSelect') {
} else { Object.assign(schema, {
schema.type = 'string'; type: 'string',
if (block === 'Form') { 'x-designer': 'AssociationSelect.Designer',
});
} else {
schema.type = 'string';
schema['properties'] = { schema['properties'] = {
viewer: cloneDeep(recordPickerViewer), viewer: cloneDeep(recordPickerViewer),
selector: cloneDeep(recordPickerSelector), selector: cloneDeep(recordPickerSelector),
}; };
} else {
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
};
}
} }
return schema;
}
schema.type = 'string';
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
};
} }
}; };

View File

@ -0,0 +1,692 @@
import { LoadingOutlined } from '@ant-design/icons';
import { ArrayCollapse, ArrayItems, FormLayout } from '@formily/antd';
import { Field } from '@formily/core';
import { connect, ISchema, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import _ from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useFormBlockContext, useFilterByTk } from '../../../block-provider';
import {
useCollectionManager,
useCollection,
useSortFields,
useCollectionFilterOptions,
} from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useDesignable, useCompile, useFieldComponentOptions } from '../../hooks';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import { defaultFieldNames } from '../select';
import { ReadPretty } from './ReadPretty';
import useServiceOptions from './useServiceOptions';
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
action?: string;
multiple?: boolean;
};
const divWrap = (schema: ISchema) => {
return {
type: 'void',
'x-component': 'div',
properties: {
[schema.name || uid()]: schema,
},
};
};
const InternalAssociationSelect = connect(
(props: AssociationSelectProps) => {
const { fieldNames, objectValue = true } = props;
const service = useServiceOptions(props);
const field = useField();
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const collectionField = getField(fieldSchema.name);
const normalizeValues = useCallback(
(obj) => {
if (!objectValue && typeof obj === 'object') {
return obj[fieldNames.value];
}
return obj;
},
[objectValue, fieldNames.value],
);
const value = useMemo(() => {
if (props.value === undefined || props.value === null) {
return;
}
if (Array.isArray(props.value)) {
return props.value.map(normalizeValues);
} else {
return normalizeValues(props.value);
}
}, [props.value, normalizeValues]);
useEffect(() => {
if (!field.title) {
field.title = collectionField.uiSchema.title;
}
}, collectionField.title);
return <RemoteSelect {...props} objectValue={objectValue} value={value} service={service}></RemoteSelect>;
},
mapProps(
{
dataSource: 'options',
loading: true,
},
(props, field) => {
return {
...props,
fieldNames: { ...defaultFieldNames, ...props.fieldNames, ...field.componentProps.fieldNames },
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
};
},
),
mapReadPretty(ReadPretty),
);
interface RemoteSelectInterface {
(props: any): React.ReactElement;
Designer: React.FC;
}
export const AssociationSelect = InternalAssociationSelect as unknown as RemoteSelectInterface;
AssociationSelect.Designer = () => {
const { getCollectionFields, getInterface, getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const { form } = useFormBlockContext();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const tk = useFilterByTk();
const {} = useCollection();
const { dn, refresh, insertAdjacent } = useDesignable();
const compile = useCompile();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const fieldComponentOptions = useFieldComponentOptions();
const isSubFormAssocitionField = field.address.segments.includes('__form_grid');
const interfaceConfig = getInterface(collectionField?.interface);
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
const originalTitle = collectionField?.uiSchema?.title;
const targetFields = collectionField?.target ? getCollectionFields(collectionField.target) : [];
const initialValue = {
title: field.title === originalTitle ? undefined : field.title,
};
const sortFields = useSortFields(collectionField.target);
const defaultSort = field.componentProps?.service?.params?.sort || [];
const defaultFilter = field.componentProps?.service?.params?.filter || {};
const dataSource = useCollectionFilterOptions(collectionField.target);
console.log(collectionField?.target, ['CollectionField', 'AssociationSelect'].includes(fieldSchema['x-component']));
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
});
if (!field.readPretty) {
initialValue['required'] = field.required;
}
const options = targetFields
.filter((field) => !field?.target && field.type !== 'boolean')
.map((field) => ({
value: field?.name,
label: compile(field?.uiSchema?.title) || field?.name,
}));
let readOnlyMode = 'editable';
if (fieldSchema['x-disabled'] === true) {
readOnlyMode = 'readonly';
}
if (fieldSchema['x-read-pretty'] === true) {
readOnlyMode = 'read-pretty';
}
return (
<GeneralSchemaDesigner>
{collectionField && (
<SchemaSettings.ModalItem
key="edit-field-title"
title={t('Edit field title')}
schema={
{
type: 'object',
title: t('Edit field title'),
properties: {
title: {
title: t('Field title'),
default: field?.title,
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ title }) => {
if (title) {
field.title = title;
fieldSchema.title = title;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
title: fieldSchema.title,
},
});
}
dn.refresh();
}}
/>
)}
{!field.readPretty && (
<SchemaSettings.ModalItem
key="edit-description"
title={t('Edit description')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
description: {
// title: t('Description'),
default: field?.description,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ description }) => {
field.description = description;
fieldSchema.description = description;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
description: fieldSchema.description,
},
});
dn.refresh();
}}
/>
)}
{field.readPretty && (
<SchemaSettings.ModalItem
key="edit-tooltip"
title={t('Edit tooltip')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
tooltip: {
default: fieldSchema?.['x-decorator-props']?.tooltip,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ tooltip }) => {
field.decoratorProps.tooltip = tooltip;
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
}}
/>
)}
{!field.readPretty && (
<SchemaSettings.SwitchItem
key="required"
title={t('Required')}
checked={fieldSchema.required as boolean}
onChange={(required) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
field.required = required;
fieldSchema['required'] = required;
schema['required'] = required;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{form && !form?.readPretty && validateSchema && (
<SchemaSettings.ModalItem
title={t('Set validation rules')}
components={{ ArrayCollapse, FormLayout }}
schema={
{
type: 'object',
title: t('Set validation rules'),
properties: {
rules: {
type: 'array',
default: fieldSchema?.['x-validator'],
'x-component': 'ArrayCollapse',
'x-decorator': 'FormItem',
'x-component-props': {
accordion: true,
},
maxItems: 3,
items: {
type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': {
header: '{{ t("Validation rule") }}',
},
properties: {
index: {
type: 'void',
'x-component': 'ArrayCollapse.Index',
},
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelStyle: {
marginTop: '6px',
},
labelCol: 8,
wrapperCol: 16,
},
properties: {
...validateSchema,
message: {
type: 'string',
title: '{{ t("Error message") }}',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {
autoSize: {
minRows: 2,
maxRows: 2,
},
},
},
},
},
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
},
moveUp: {
type: 'void',
'x-component': 'ArrayCollapse.MoveUp',
},
moveDown: {
type: 'void',
'x-component': 'ArrayCollapse.MoveDown',
},
},
},
properties: {
add: {
type: 'void',
title: '{{ t("Add validation rule") }}',
'x-component': 'ArrayCollapse.Addition',
'x-reactions': {
dependencies: ['rules'],
fulfill: {
state: {
disabled: '{{$deps[0].length >= 3}}',
},
},
},
},
},
},
},
} as ISchema
}
onSubmit={(v) => {
const rules = [];
for (const rule of v.rules) {
rules.push(_.pickBy(rule, _.identity));
}
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
// return;
// if (['number'].includes(collectionField?.interface) && collectionField?.uiSchema?.['x-component-props']?.['stringMode'] === true) {
// rules['numberStringMode'] = true;
// }
if (['percent'].includes(collectionField?.interface)) {
for (const rule of rules) {
if (!!rule.maxValue || !!rule.minValue) {
rule['percentMode'] = true;
}
if (rule.percentFormat) {
rule['percentFormats'] = true;
}
}
}
const concatValidator = _.concat([], collectionField?.uiSchema?.['x-validator'] || [], rules);
field.validator = concatValidator;
fieldSchema['x-validator'] = rules;
schema['x-validator'] = rules;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{form && !form?.readPretty && collectionField?.uiSchema?.type && (
<SchemaSettings.ModalItem
title={t('Set default value')}
components={{ ArrayCollapse, FormLayout }}
schema={
{
type: 'object',
title: t('Set default value'),
properties: {
default: {
...collectionField.uiSchema,
name: 'default',
title: t('Default value'),
'x-decorator': 'FormItem',
default: fieldSchema.default || collectionField.defaultValue,
},
},
} as ISchema
}
onSubmit={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
if (field.value !== v.default) {
field.value = v.default;
}
fieldSchema.default = v.default;
schema.default = v.default;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{form && !isSubFormAssocitionField && fieldComponentOptions && (
<SchemaSettings.SelectItem
title={t('Field component')}
options={fieldComponentOptions}
value={fieldSchema['x-component']}
onChange={(type) => {
const schema: ISchema = {
name: collectionField.name,
type: 'void',
required: fieldSchema['required'],
description: fieldSchema['description'],
default: fieldSchema['default'],
'x-decorator': 'FormItem',
'x-designer': 'FormItem.Designer',
'x-component': type,
'x-validator': fieldSchema['x-validator'],
'x-collection-field': fieldSchema['x-collection-field'],
'x-decorator-props': fieldSchema['x-decorator-props'],
'x-component-props': {
...collectionField?.uiSchema?.['x-component-props'],
...fieldSchema['x-component-props'],
},
};
interfaceConfig?.schemaInitialize?.(schema, {
field: collectionField,
block: 'Form',
readPretty: field.readPretty,
action: tk ? 'get' : null,
});
if (type === 'CollectionField') {
schema['type'] = 'string';
}
insertAdjacent('beforeBegin', divWrap(schema), {
onSuccess: () => {
dn.remove(null, {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
});
},
});
}}
/>
)}
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ filter }) => {
_.set(field.componentProps, 'service.params.filter', filter);
fieldSchema['x-component-props'] = field.componentProps;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': field.componentProps,
},
});
}}
/>
<SchemaSettings.ModalItem
title={t('Set default sorting rules')}
components={{ ArrayItems }}
schema={
{
type: 'object',
title: t('Set default sorting rules'),
properties: {
sort: {
type: 'array',
default: sort,
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
sort: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.SortHandle',
},
field: {
type: 'string',
enum: sortFields,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
style: {
width: 260,
},
},
},
direction: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {
optionType: 'button',
},
enum: [
{
label: t('ASC'),
value: 'asc',
},
{
label: t('DESC'),
value: 'desc',
},
],
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: t('Add sort field'),
'x-component': 'ArrayItems.Addition',
},
},
},
},
} as ISchema
}
onSubmit={({ sort }) => {
const sortArr = sort.map((item) => {
return item.direction === 'desc' ? `-${item.field}` : item.field;
});
_.set(field.componentProps, 'service.params.sort', sortArr);
fieldSchema['x-component-props'] = field.componentProps;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': field.componentProps,
},
});
}}
/>
{form && !form?.readPretty && fieldSchema?.['x-component-props']?.['pattern-disable'] != true && (
<SchemaSettings.SelectItem
key="pattern"
title={t('Pattern')}
options={[
{ label: t('Editable'), value: 'editable' },
{ label: t('Readonly'), value: 'readonly' },
{ label: t('Easy-reading'), value: 'read-pretty' },
]}
value={readOnlyMode}
onChange={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
switch (v) {
case 'readonly': {
fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = true;
schema['x-read-pretty'] = false;
schema['x-disabled'] = true;
field.readPretty = false;
field.disabled = true;
break;
}
case 'read-pretty': {
fieldSchema['x-read-pretty'] = true;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = true;
schema['x-disabled'] = false;
field.readPretty = true;
break;
}
default: {
fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = false;
schema['x-disabled'] = false;
field.readPretty = false;
field.disabled = false;
break;
}
}
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
)}
{collectionField?.target && ['CollectionField', 'AssociationSelect'].includes(fieldSchema['x-component']) && (
<SchemaSettings.SelectItem
key="title-field"
title={t('Title field')}
options={options}
value={field?.componentProps?.fieldNames?.label}
onChange={(label) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
const fieldNames = {
...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
...field.componentProps.fieldNames,
label,
};
field.componentProps.fieldNames = fieldNames;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
schema['x-component-props'] = fieldSchema['x-component-props'];
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
)}
{collectionField && <SchemaSettings.Divider />}
<SchemaSettings.Remove
key="remove"
removeParentsIfNoChildren
confirm={{
title: t('Delete field'),
}}
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};
export default AssociationSelect;

View File

@ -0,0 +1,10 @@
import { observer } from '@formily/react';
import React from 'react';
import { RemoteSelect } from '../remote-select';
import useServiceOptions from './useServiceOptions';
export const ReadPretty = observer((props: any) => {
const service = useServiceOptions(props);
return <RemoteSelect.ReadPretty {...props} service={service}></RemoteSelect.ReadPretty>;
});

View File

@ -0,0 +1,26 @@
---
nav:
path: /client
group:
path: /schema-components
---
# RemoteSelect
## Examples
## API
基于 Ant Design 的 [Select](https://ant.design/components/select/#API),相关扩展属性有:
- `objectValue` 值为 object 类型
- `fieldNames` 默认值有区别
```ts
export const defaultFieldNames = {
label: 'label',
value: 'value',
color: 'color',
options: 'children',
};
```

View File

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

View File

@ -0,0 +1,20 @@
import { useFieldSchema } from "@formily/react";
import { useMemo } from "react";
import { useCollection } from "../../../collection-manager";
export default function useServiceOptions(props) {
const { action = 'list', service } = props
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const collectionField = useMemo(() => {
return getField(fieldSchema.name);
}, [fieldSchema.name]);
return useMemo(() => {
return {
resource: collectionField.target,
action,
...service,
};
}, [collectionField.target, action, service]);
}

View File

@ -6,7 +6,7 @@ import { uid } from '@formily/shared';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCompile, useDesignable } from '../..'; import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
import { useFilterByTk, useFormBlockContext } from '../../../block-provider'; import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
@ -41,7 +41,9 @@ export const FormItem: any = (props) => {
__html: HTMLEncode(field.description).split('\n').join('<br/>'), __html: HTMLEncode(field.description).split('\n').join('<br/>'),
}} }}
/> />
) : field.description ) : (
field.description
)
} }
/> />
</BlockItem> </BlockItem>
@ -56,13 +58,14 @@ FormItem.Designer = (props) => {
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { t } = useTranslation(); const { t } = useTranslation();
const { dn, refresh, insertAdjacent, insertBeforeBegin } = useDesignable(); const { dn, refresh, insertAdjacent } = useDesignable();
const compile = useCompile(); const compile = useCompile();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const interfaceConfig = getInterface(collectionField?.interface); const interfaceConfig = getInterface(collectionField?.interface);
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
const originalTitle = collectionField?.uiSchema?.title; const originalTitle = collectionField?.uiSchema?.title;
const targetFields = collectionField?.target ? getCollectionFields(collectionField.target) : []; const targetFields = collectionField?.target ? getCollectionFields(collectionField.target) : [];
const fieldComponentOptions = useFieldComponentOptions();
const isSubFormAssocitionField = field.address.segments.includes('__form_grid'); const isSubFormAssocitionField = field.address.segments.includes('__form_grid');
const initialValue = { const initialValue = {
title: field.title === originalTitle ? undefined : field.title, title: field.title === originalTitle ? undefined : field.title,
@ -328,62 +331,61 @@ FormItem.Designer = (props) => {
)} )}
{form && !form?.readPretty && collectionField?.uiSchema?.type && ( {form && !form?.readPretty && collectionField?.uiSchema?.type && (
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Set default value')} title={t('Set default value')}
components={{ ArrayCollapse, FormLayout }} components={{ ArrayCollapse, FormLayout }}
schema={{ schema={
type: 'object', {
title: t('Set default value'), type: 'object',
properties: { title: t('Set default value'),
default: { properties: {
...collectionField.uiSchema, default: {
name: 'default', ...collectionField.uiSchema,
title: t('Default value'), name: 'default',
'x-decorator': 'FormItem', title: t('Default value'),
default: fieldSchema.default || collectionField.defaultValue, 'x-decorator': 'FormItem',
default: fieldSchema.default || collectionField.defaultValue,
},
},
} as ISchema
}
onSubmit={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
if (field.value !== v.default) {
field.value = v.default;
} }
} fieldSchema.default = v.default;
} as ISchema} schema.default = v.default;
onSubmit={(v) => { dn.emit('patch', {
const schema: ISchema = { schema,
['x-uid']: fieldSchema['x-uid'], });
}; refresh();
if (field.value !== v.default) { }}
field.value = v.default; />
}
fieldSchema.default = v.default;
schema.default = v.default;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)} )}
{form && !isSubFormAssocitionField && ['o2o', 'oho', 'obo', 'o2m'].includes(collectionField?.interface) && ( {form && !isSubFormAssocitionField && fieldComponentOptions && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
title={t('Field component')} title={t('Field component')}
options={ options={fieldComponentOptions}
collectionField?.interface === 'o2m'
? [
{ label: t('Record picker'), value: 'CollectionField' },
{ label: t('Subtable'), value: 'TableField' },
]
: [
{ label: t('Record picker'), value: 'CollectionField' },
{ label: t('Subform'), value: 'FormField' },
]
}
value={fieldSchema['x-component']} value={fieldSchema['x-component']}
onChange={(v) => { onChange={(type) => {
const schema: ISchema = { const schema: ISchema = {
name: collectionField.name, name: collectionField.name,
type: 'void', type: 'void',
// title: compile(collectionField.uiSchema?.title), required: fieldSchema['required'],
description: fieldSchema['description'],
default: fieldSchema['default'],
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-designer': 'FormItem.Designer', 'x-designer': 'FormItem.Designer',
'x-component': v, 'x-component': type,
'x-component-props': {}, 'x-validator': fieldSchema['x-validator'],
'x-collection-field': fieldSchema['x-collection-field'], 'x-collection-field': fieldSchema['x-collection-field'],
'x-decorator-props': fieldSchema['x-decorator-props'],
'x-component-props': {
...collectionField?.uiSchema?.['x-component-props'],
...fieldSchema['x-component-props'],
},
}; };
interfaceConfig?.schemaInitialize?.(schema, { interfaceConfig?.schemaInitialize?.(schema, {
@ -393,10 +395,6 @@ FormItem.Designer = (props) => {
action: tk ? 'get' : null, action: tk ? 'get' : null,
}); });
if (v === 'CollectionField') {
schema['type'] = 'string';
}
insertAdjacent('beforeBegin', divWrap(schema), { insertAdjacent('beforeBegin', divWrap(schema), {
onSuccess: () => { onSuccess: () => {
dn.remove(null, { dn.remove(null, {

View File

@ -28,6 +28,8 @@ export * from './radio';
export * from './record-picker'; export * from './record-picker';
export * from './rich-text'; export * from './rich-text';
export * from './select'; export * from './select';
export * from './remote-select';
export * from './association-select';
export * from './space'; export * from './space';
export * from './table'; export * from './table';
export * from './table-v2'; export * from './table-v2';

View File

@ -0,0 +1,30 @@
import { observer, useField } from '@formily/react';
import React from 'react';
import { getValues } from './shared';
import { Select, defaultFieldNames } from '../select';
import { useRequest } from '../../../api-client';
export const ReadPretty = observer((props: any) => {
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
const field = useField<any>();
const { data } = useRequest(
{
action: 'list',
...props.service,
params: {
paginate: false,
filter: {
[fieldNames.value]: {
$in: getValues(field.value, fieldNames),
},
},
},
},
{
refreshDeps: [props.service, field.value],
},
);
return <Select.ReadPretty {...props} options={data?.data}></Select.ReadPretty>;
});

View File

@ -0,0 +1,89 @@
import { LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { SelectProps } from 'antd';
import _ from 'lodash';
import React from 'react';
import { ResourceActionOptions, useRequest } from '../../../api-client';
import { defaultFieldNames, Select } from '../select';
import { ReadPretty } from './ReadPretty';
export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
objectValue?: boolean;
onChange?: (v: any) => void;
target: string;
wait?: number;
service: ResourceActionOptions<P>;
};
const InternalRemoteSelect = connect(
(props: RemoteSelectProps) => {
const { fieldNames, service = {}, wait = 300, ...others } = props;
const { data, run } = useRequest(
{
action: 'list',
...service,
params: {
pageSize: 30,
...service?.params,
// search needs
filter: {
$and: [service?.params?.filter].filter(Boolean),
},
},
},
{
debounceWait: wait,
refreshDeps: [service],
},
);
const onSearch = async (search) => {
run({
filter: {
$and: [
{
[fieldNames.label]: {
$includes: search,
},
},
service?.params?.filter,
].filter(Boolean),
},
});
};
return (
<Select
autoClearSearchValue
filterOption={false}
filterSort={null}
fieldNames={fieldNames}
onSearch={onSearch}
{...others}
options={data?.data || []}
/>
);
},
mapProps(
{
dataSource: 'options',
loading: true,
},
(props, field) => {
return {
...props,
fieldNames: { ...defaultFieldNames, ...props.fieldNames, ...field.componentProps.fieldNames },
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
};
},
),
mapReadPretty(ReadPretty),
);
export const RemoteSelect = InternalRemoteSelect as unknown as typeof InternalRemoteSelect & {
ReadPretty: typeof ReadPretty;
};
RemoteSelect.ReadPretty = ReadPretty;
export default RemoteSelect;

View File

@ -0,0 +1,26 @@
---
nav:
path: /client
group:
path: /schema-components
---
# RemoteSelect
## Examples
## API
基于 Ant Design 的 [Select](https://ant.design/components/select/#API),相关扩展属性有:
- `objectValue` 值为 object 类型
- `fieldNames` 默认值有区别
```ts
export const defaultFieldNames = {
label: 'label',
value: 'value',
color: 'color',
options: 'children',
};
```

View File

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

View File

@ -0,0 +1,7 @@
import { castArray } from 'lodash';
export const getValues = (values, fieldNames) => {
return castArray(values)
.filter(item => item != null)
.map((val) => (typeof val === 'object' ? val[fieldNames.value] : val));
};

View File

@ -7,7 +7,7 @@ import React from 'react';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
import { defaultFieldNames, getCurrentOptions } from './shared'; import { defaultFieldNames, getCurrentOptions } from './shared';
type Props = SelectProps<any, any> & { objectValue?: boolean; onChange?: (v: any) => void }; type Props = SelectProps<any, any> & { objectValue?: boolean; onChange?: (v: any) => void; multiple: boolean };
const isEmptyObject = (val: any) => !isValid(val) || (typeof val === 'object' && Object.keys(val).length === 0); const isEmptyObject = (val: any) => !isValid(val) || (typeof val === 'object' && Object.keys(val).length === 0);
@ -28,7 +28,7 @@ const ObjectSelect = (props: Props) => {
value: val[fieldNames.value], value: val[fieldNames.value],
}; };
}); });
if (['tags', 'multiple'].includes(mode)) { if (['tags', 'multiple'].includes(mode) || props.multiple) {
return current; return current;
} }
return current.shift(); return current.shift();
@ -53,7 +53,7 @@ const ObjectSelect = (props: Props) => {
options, options,
fieldNames, fieldNames,
); );
if (['tags', 'multiple'].includes(mode)) { if (['tags', 'multiple'].includes(mode) || props.multiple) {
onChange(current); onChange(current);
} else { } else {
onChange(current.shift()); onChange(current.shift());
@ -67,11 +67,13 @@ const ObjectSelect = (props: Props) => {
const filterOption = (input, option) => (option?.label ?? '').toLowerCase().includes((input || '').toLowerCase()); const filterOption = (input, option) => (option?.label ?? '').toLowerCase().includes((input || '').toLowerCase());
export const Select = connect( const InternalSelect = connect(
(props: Props) => { (props: Props) => {
const { objectValue, ...others } = props; const { objectValue, ...others } = props;
const mode = props.mode || props.multiple ? 'multiple' : undefined;
if (objectValue) { if (objectValue) {
return <ObjectSelect {...others} />; return <ObjectSelect {...others} mode={mode} />;
} }
return ( return (
<AntdSelect <AntdSelect
@ -80,9 +82,9 @@ export const Select = connect(
allowClear allowClear
{...others} {...others}
onChange={(changed) => { onChange={(changed) => {
props.onChange(changed === undefined ? null : changed); props.onChange?.(changed === undefined ? null : changed);
}} }}
value={others.value || undefined} mode={mode}
/> />
); );
}, },
@ -102,4 +104,10 @@ export const Select = connect(
mapReadPretty(ReadPretty), mapReadPretty(ReadPretty),
); );
export const Select = InternalSelect as unknown as typeof InternalSelect & {
ReadPretty: typeof ReadPretty;
};
Select.ReadPretty = ReadPretty;
export default Select; export default Select;

View File

@ -1 +1,2 @@
export * from './Select'; export * from './Select';
export * from './shared'

View File

@ -26,6 +26,7 @@ export const getCurrentOptions = (values, dataSource, fieldNames) => {
.filter((item) => item != null) .filter((item) => item != null)
.map((val) => (typeof val === 'object' ? val[fieldNames.value] : val)); .map((val) => (typeof val === 'object' ? val[fieldNames.value] : val));
const findOptions = (options: any[]) => { const findOptions = (options: any[]) => {
if (!options) return []
let current = []; let current = [];
for (const value of values) { for (const value of values) {
const option = options.find((v) => v[fieldNames.value] === value) || { value: value, label: value }; const option = options.find((v) => v[fieldNames.value] === value) || { value: value, label: value };

View File

@ -5,4 +5,5 @@ export * from './useDesignable';
export * from './useDesigner'; export * from './useDesigner';
export * from './useFieldProps'; export * from './useFieldProps';
export * from './useSchemaComponentContext'; export * from './useSchemaComponentContext';
export * from './useFieldComponentOptions';

View File

@ -0,0 +1,42 @@
import { useFieldSchema } from "@formily/react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useCollectionManager, useCollection } from "../../collection-manager";
export const useFieldComponentOptions = () => {
const { getCollectionJoinField } = useCollectionManager();
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const { t } = useTranslation();
const fieldComponentOptions = useMemo(() => {
if (!collectionField?.interface) return;
switch (collectionField.interface) {
case 'o2m':
return [
{ label: t('Record picker'), value: 'CollectionField' },
{ label: t('Subtable'), value: 'TableField' },
{ label: t('Select'), value: 'AssociationSelect' },
];
case 'm2o':
case 'm2m':
case 'linkTo':
return [
{ label: t('Record picker'), value: 'CollectionField' },
{ label: t('Select'), value: 'AssociationSelect' },
]
default:
return [
{ label: t('Record picker'), value: 'CollectionField' },
{ label: t('Subform'), value: 'FormField' },
{ label: t('Select'), value: 'AssociationSelect' },
];
}
}, [t, collectionField?.interface]);
return fieldComponentOptions;
};