mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-05 05:18:34 +08:00
feat: 表单项字段绑定
Change-Id: I3699a5371f78225246d4745a3f8e17721d28b30b
This commit is contained in:
parent
92e68b01fa
commit
90ab02b9e3
@ -96,7 +96,17 @@ const Tools: Array<FeatOption> = [
|
||||
resolveSchema: (setting: any = {}, builder: DSBuilder) => {
|
||||
const form: FormSchema = {
|
||||
type: 'form',
|
||||
body: []
|
||||
body: [],
|
||||
onEvent: {
|
||||
submitSucc: {
|
||||
actions: [
|
||||
{
|
||||
actionType: 'search',
|
||||
componentId: setting.id
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
builder.resolveCreateSchema({
|
||||
schema: form,
|
||||
@ -136,7 +146,17 @@ const Tools: Array<FeatOption> = [
|
||||
type: 'form',
|
||||
// @ts-ignore
|
||||
behavior: 'BulkEdit',
|
||||
body: []
|
||||
body: [],
|
||||
onEvent: {
|
||||
submitSucc: {
|
||||
actions: [
|
||||
{
|
||||
actionType: 'search',
|
||||
componentId: setting.id
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
builder.resolveCreateSchema({
|
||||
schema: form,
|
||||
@ -162,7 +182,8 @@ const Tools: Array<FeatOption> = [
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
disabledOn: 'selectedItems != null && selectedItems.length < 1'
|
||||
};
|
||||
},
|
||||
align: 'left'
|
||||
@ -187,6 +208,11 @@ const Tools: Array<FeatOption> = [
|
||||
feat: 'BulkDelete'
|
||||
});
|
||||
|
||||
button.onEvent?.click?.actions?.push({
|
||||
actionType: 'search',
|
||||
componentId: setting.id
|
||||
});
|
||||
|
||||
return button;
|
||||
},
|
||||
align: 'left'
|
||||
@ -382,7 +408,17 @@ const DataOperators: Array<FeatOption> = [
|
||||
resolveSchema: (setting: any = {}, builder: DSBuilder) => {
|
||||
const form: FormSchema = {
|
||||
type: 'form',
|
||||
body: []
|
||||
body: [],
|
||||
onEvent: {
|
||||
submitSucc: {
|
||||
actions: [
|
||||
{
|
||||
actionType: 'search',
|
||||
componentId: setting.id
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
builder.resolveCreateSchema({
|
||||
schema: form,
|
||||
@ -430,6 +466,11 @@ const DataOperators: Array<FeatOption> = [
|
||||
feat: 'Delete'
|
||||
});
|
||||
|
||||
button.onEvent?.click?.actions?.push({
|
||||
actionType: 'search',
|
||||
componentId: setting.id
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
}
|
||||
@ -1249,6 +1290,16 @@ export class CRUDPlugin extends BasePlugin {
|
||||
|
||||
this.resolveListField(value, schema, builder);
|
||||
|
||||
this.addFeatToToolbar(
|
||||
schema,
|
||||
{
|
||||
type: 'pagination',
|
||||
behavior: 'Pagination',
|
||||
layout: ['total', 'perPage', 'pager', 'go']
|
||||
},
|
||||
'footer',
|
||||
'right'
|
||||
);
|
||||
return schema;
|
||||
},
|
||||
canRebuild: true
|
||||
@ -1456,11 +1507,14 @@ export class CRUDPlugin extends BasePlugin {
|
||||
'api'
|
||||
);
|
||||
if (builder && scopeNode.schema.api) {
|
||||
return builder.getAvailableContextFileds({
|
||||
schema: scopeNode.schema,
|
||||
sourceKey: 'api',
|
||||
feat: 'List'
|
||||
});
|
||||
return builder.getAvailableContextFileds(
|
||||
{
|
||||
schema: scopeNode.schema,
|
||||
sourceKey: 'api',
|
||||
feat: 'List'
|
||||
},
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -418,10 +418,44 @@ export class FormPlugin extends BasePlugin {
|
||||
? null
|
||||
: {
|
||||
title: '数据源',
|
||||
body: builder.makeSourceSettingForm({
|
||||
name: 'api',
|
||||
feat: 'Edit'
|
||||
})
|
||||
body: [
|
||||
{
|
||||
type: 'radios',
|
||||
name: '__scene',
|
||||
label: '场景',
|
||||
options: Features,
|
||||
value: 'Insert',
|
||||
pipeIn(value: any, data: any) {
|
||||
return value ?? (data.initApi ? 'Edit' : 'Insert');
|
||||
},
|
||||
onChange: (
|
||||
value: any,
|
||||
oldValue: any,
|
||||
model: any,
|
||||
form: any
|
||||
) => {
|
||||
if (value === 'Insert') {
|
||||
form.setValueByName(`initApi`, undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
visibleOn: `__scene === 'Insert'`,
|
||||
body: builder.makeSourceSettingForm({
|
||||
name: 'api',
|
||||
feat: 'Insert'
|
||||
})
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
visibleOn: `__scene === 'Edit'`,
|
||||
body: builder.makeSourceSettingForm({
|
||||
name: 'api',
|
||||
feat: 'Edit'
|
||||
})
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '基本',
|
||||
@ -760,18 +794,18 @@ export class FormPlugin extends BasePlugin {
|
||||
|
||||
async getAvailableContextFields(
|
||||
scopeNode: EditorNodeType,
|
||||
node: EditorNodeType,
|
||||
target: EditorNodeType,
|
||||
region?: EditorNodeType
|
||||
) {
|
||||
// 只有表单项组件可以使用表单组件的数据域
|
||||
if (node.info.renderer.isFormItem) {
|
||||
if (target.info.renderer.isFormItem) {
|
||||
if (
|
||||
scopeNode.parent?.type === 'crud2' &&
|
||||
scopeNode.schemaPath.startsWith('body/0/filter/')
|
||||
) {
|
||||
return scopeNode.parent.info.plugin.getAvailableContextFields?.(
|
||||
scopeNode.parent,
|
||||
node,
|
||||
target,
|
||||
region
|
||||
);
|
||||
}
|
||||
@ -782,11 +816,14 @@ export class FormPlugin extends BasePlugin {
|
||||
'api'
|
||||
);
|
||||
if (builder && scopeNode.schema.api) {
|
||||
return builder.getAvailableContextFileds({
|
||||
schema: scopeNode.schema,
|
||||
sourceKey: 'api',
|
||||
feat: 'Insert'
|
||||
});
|
||||
return builder.getAvailableContextFileds(
|
||||
{
|
||||
schema: scopeNode.schema,
|
||||
sourceKey: 'api',
|
||||
feat: 'Insert'
|
||||
},
|
||||
target
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,12 @@ import {
|
||||
Checkbox,
|
||||
Spinner
|
||||
} from 'amis';
|
||||
import {FormControlProps, generateIcon} from 'amis-core';
|
||||
import {
|
||||
FormControlProps,
|
||||
generateIcon,
|
||||
Renderer,
|
||||
RendererProps
|
||||
} from 'amis-core';
|
||||
import {debounce, remove} from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
@ -21,11 +26,11 @@ import {
|
||||
DSFieldGroup
|
||||
} from 'amis-editor-core';
|
||||
import {matchSorter} from 'match-sorter';
|
||||
import {SchemaCollection} from 'amis/lib/Schema';
|
||||
|
||||
export interface DataBindingProps extends FormControlProps {
|
||||
node: EditorNodeType;
|
||||
manager: EditorManager;
|
||||
samePredicate?: (a: any, b: any) => boolean;
|
||||
onBindingChange?: (
|
||||
value: DSField,
|
||||
onBulkChange: (value: any) => void
|
||||
@ -33,10 +38,15 @@ export interface DataBindingProps extends FormControlProps {
|
||||
}
|
||||
|
||||
export interface DataBindingState {
|
||||
filteredFields: DSFieldGroup[];
|
||||
sourceFields: DSFieldGroup[];
|
||||
loading: boolean;
|
||||
hint: string | void;
|
||||
schema?: SchemaCollection;
|
||||
}
|
||||
|
||||
// 所有的内容组件都必须继承这个,用来做内容选择面板
|
||||
export interface DataBindingPanelProps extends RendererProps {
|
||||
onSelect: (value: any) => void;
|
||||
isSelected?: (value: any) => boolean;
|
||||
}
|
||||
|
||||
export class DataBindingControl extends React.Component<
|
||||
@ -45,22 +55,14 @@ export class DataBindingControl extends React.Component<
|
||||
> {
|
||||
constructor(props: DataBindingProps) {
|
||||
super(props);
|
||||
this.handleSearchDebounced = debounce(this.handleSearch, 250, {
|
||||
trailing: true,
|
||||
leading: false
|
||||
});
|
||||
this.state = {
|
||||
filteredFields: [],
|
||||
sourceFields: [],
|
||||
loading: false,
|
||||
hint: undefined
|
||||
};
|
||||
}
|
||||
|
||||
handleSearchDebounced;
|
||||
|
||||
@autobind
|
||||
handleConfirm(result: {label: string; value: string}) {
|
||||
handleConfirm(result: DSField) {
|
||||
const {manager, data, onChange, onBulkChange, onBindingChange} = this.props;
|
||||
|
||||
if (result?.value) {
|
||||
@ -71,62 +73,44 @@ export class DataBindingControl extends React.Component<
|
||||
}
|
||||
|
||||
@autobind
|
||||
handlePickerOpen() {
|
||||
async handlePickerOpen() {
|
||||
const {manager, node} = this.props;
|
||||
|
||||
// 如果node没变化,就不再重复加载
|
||||
if (this.state.sourceFields.length) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
schema: undefined
|
||||
});
|
||||
|
||||
let schema;
|
||||
try {
|
||||
schema = await manager.getAvailableContextFields(node);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
hint: '加载可用字段失败,请联系管理员!'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
sourceFields: [],
|
||||
filteredFields: [],
|
||||
loading: true
|
||||
});
|
||||
|
||||
manager
|
||||
.getAvailableContextFields(node)
|
||||
.then(groupedFields => {
|
||||
this.setState({
|
||||
sourceFields: groupedFields || [],
|
||||
filteredFields: groupedFields || [],
|
||||
loading: false,
|
||||
hint: groupedFields ? undefined : '暂无可绑定字段'
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
hint: '加载可用字段失败,请联系管理员!'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
async handleSearch(keywords: string) {
|
||||
this.setState({
|
||||
filteredFields: matchSorter(this.state.sourceFields, keywords, {
|
||||
keys: ['label', 'value', 'children']
|
||||
})
|
||||
loading: false,
|
||||
hint: schema ? undefined : '暂无可绑定字段',
|
||||
schema: schema ?? undefined
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleSelect() {}
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
classnames: cx,
|
||||
value,
|
||||
onChange,
|
||||
samePredicate = (a, b) => a.value === b.value,
|
||||
multiple,
|
||||
disabled
|
||||
disabled,
|
||||
render
|
||||
} = this.props;
|
||||
|
||||
const {filteredFields, loading, hint} = this.state;
|
||||
const {schema, loading, hint} = this.state;
|
||||
return (
|
||||
<PickerContainer
|
||||
onPickerOpen={this.handlePickerOpen}
|
||||
@ -151,89 +135,10 @@ export class DataBindingControl extends React.Component<
|
||||
return <p className={cx('ae-DataBindingList-hint')}>{hint}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('ae-DataBindingList')}>
|
||||
<div className={cx('ae-DataBindingList-searchBox')}>
|
||||
<SearchBox
|
||||
mini={false}
|
||||
placeholder={'输入名称搜索'}
|
||||
onSearch={this.handleSearchDebounced}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cx('ae-DataBindingList-body')}>
|
||||
<CollapseGroup
|
||||
className={cx('ae-DataBindingList-collapseGroup')}
|
||||
defaultActiveKey={filteredFields.map(item => item.value!)}
|
||||
expandIcon={
|
||||
generateIcon(
|
||||
cx,
|
||||
'fa fa-chevron-right ae-DataBindingList-expandIcon',
|
||||
'Icon'
|
||||
)!
|
||||
}
|
||||
expandIconPosition="right"
|
||||
// accordion={true}
|
||||
>
|
||||
{filteredFields.map(item => (
|
||||
<Collapse
|
||||
className={cx('ae-DataBindingList-collapse')}
|
||||
headingClassName={cx('ae-DataBindingList-collapse-title')}
|
||||
bodyClassName={cx('ae-DataBindingList-collapse-body')}
|
||||
propKey={item.value}
|
||||
key={item.value}
|
||||
header={item.label}
|
||||
>
|
||||
{Array.isArray(item.children) &&
|
||||
item.children.length > 0 ? (
|
||||
item.children.map((childItem: DSField) => {
|
||||
if (multiple) {
|
||||
const checked = !!value.find((i: any) =>
|
||||
samePredicate(i, childItem)
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={childItem.value}
|
||||
className={cx('ae-DataBindingList-item')}
|
||||
onClick={() =>
|
||||
onChange(
|
||||
checked
|
||||
? value.concat(childItem)
|
||||
: remove(value, childItem)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Checkbox value={checked}>
|
||||
{childItem.label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('ae-DataBindingList-item', {
|
||||
'is-active':
|
||||
value && childItem.value === value.value
|
||||
})}
|
||||
onClick={() => onChange(childItem)}
|
||||
key={childItem.value}
|
||||
>
|
||||
{childItem.label}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className={cx('ae-DataBindingList-hint')}>
|
||||
暂无可用字段
|
||||
</p>
|
||||
)}
|
||||
</Collapse>
|
||||
))}
|
||||
</CollapseGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return render('content', schema!, {
|
||||
onSelect: onChange,
|
||||
value
|
||||
});
|
||||
}}
|
||||
value={value}
|
||||
onConfirm={this.handleConfirm}
|
||||
@ -266,3 +171,109 @@ export class DataBindingControl extends React.Component<
|
||||
type: 'ae-DataBindingControl'
|
||||
})
|
||||
export class DataBindingControlRenderer extends DataBindingControl {}
|
||||
|
||||
export interface SimpleDataBindingProps extends DataBindingPanelProps {
|
||||
fields: DSFieldGroup[];
|
||||
}
|
||||
|
||||
export interface SimpleDataBindingState {
|
||||
filteredFields: DSFieldGroup[];
|
||||
}
|
||||
|
||||
export class SimpleDataBindingControl extends React.Component<
|
||||
SimpleDataBindingProps,
|
||||
SimpleDataBindingState
|
||||
> {
|
||||
constructor(props: SimpleDataBindingProps) {
|
||||
super(props);
|
||||
this.handleSearchDebounced = debounce(this.handleSearch, 250, {
|
||||
trailing: true,
|
||||
leading: false
|
||||
});
|
||||
this.state = {
|
||||
filteredFields: props.fields
|
||||
};
|
||||
}
|
||||
|
||||
handleSearchDebounced;
|
||||
|
||||
@autobind
|
||||
async handleSearch(keywords: string) {
|
||||
this.setState({
|
||||
filteredFields: matchSorter(this.props.fields, keywords, {
|
||||
keys: ['label', 'value', 'children']
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleSelect() {}
|
||||
|
||||
render() {
|
||||
const {className, classnames: cx, value, onSelect, isSelected} = this.props;
|
||||
|
||||
const {filteredFields} = this.state;
|
||||
return (
|
||||
<div className={cx('ae-DataBindingList', className)}>
|
||||
<div className={cx('ae-DataBindingList-searchBox')}>
|
||||
<SearchBox
|
||||
mini={false}
|
||||
placeholder={'输入名称搜索'}
|
||||
onSearch={this.handleSearchDebounced}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cx('ae-DataBindingList-body')}>
|
||||
<CollapseGroup
|
||||
className={cx('ae-DataBindingList-collapseGroup')}
|
||||
defaultActiveKey={filteredFields.map(
|
||||
item => item.value || item.label
|
||||
)}
|
||||
expandIcon={
|
||||
generateIcon(cx, 'fa fa-chevron-right expandIcon', 'Icon')!
|
||||
}
|
||||
expandIconPosition="right"
|
||||
// accordion={true}
|
||||
>
|
||||
{filteredFields.map((item, index) => (
|
||||
<Collapse
|
||||
className={cx('ae-DataBindingList-collapse')}
|
||||
headingClassName={cx('ae-DataBindingList-collapse-title')}
|
||||
bodyClassName={cx('ae-DataBindingList-collapse-body')}
|
||||
propKey={item.value || item.label}
|
||||
key={item.value || item.label}
|
||||
header={<span>{item.label}</span>}
|
||||
>
|
||||
{Array.isArray(item.children) && item.children.length > 0 ? (
|
||||
item.children.map((childItem: DSField) => {
|
||||
const checked = isSelected
|
||||
? isSelected(childItem)
|
||||
: childItem.value === value;
|
||||
return (
|
||||
<div
|
||||
className={cx('ae-DataBindingList-item', {
|
||||
'is-active': checked
|
||||
})}
|
||||
onClick={() => onSelect(childItem)}
|
||||
key={childItem.value}
|
||||
>
|
||||
{childItem.label}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className={cx('ae-DataBindingList-hint')}>暂无可用字段</p>
|
||||
)}
|
||||
</Collapse>
|
||||
))}
|
||||
</CollapseGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Renderer({
|
||||
type: 'ae-SimpleDataBindingPanel'
|
||||
})
|
||||
export class SimpleDataBindingControlRenderer extends SimpleDataBindingControl {}
|
||||
|
Loading…
Reference in New Issue
Block a user