feat: 表单项字段绑定

Change-Id: I3699a5371f78225246d4745a3f8e17721d28b30b
This commit is contained in:
yangwei9012 2022-07-19 14:30:04 +08:00
parent 92e68b01fa
commit 90ab02b9e3
3 changed files with 257 additions and 155 deletions

View File

@ -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
);
}
}
}

View File

@ -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
);
}
}
}

View File

@ -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 {}