fix(amis-editor): 数据源构造器兼容历史存量Schema

This commit is contained in:
lurunze1226 2023-09-20 21:33:27 +08:00
parent 56b6e3110d
commit d8e4d31fc2
12 changed files with 397 additions and 210 deletions

View File

@ -585,8 +585,8 @@ export function JsonGenerateID(json: any) {
return;
}
if (json.type) {
// && !json.id
/** 脚手架构建的Schema提前构建好了组件 ID此时无需生成 ID避免破坏事件动作 */
if (json.type && (!json.__origin || json.__origin !== 'scaffold')) {
json.id = generateNodeId();
}

View File

@ -94,8 +94,9 @@ export class ApiDSBuilder extends DSBuilder<
return (this.constructor as typeof ApiDSBuilder).key;
}
match(schema: any) {
const apiSchema = schema?.api;
match(schema: any, key?: string) {
const sourceKey = key && typeof key === 'string' ? key : 'api';
const apiSchema = schema?.[sourceKey];
if (schema?.dsType === this.key || apiSchema?.sourceType === this.key) {
return true;
@ -112,9 +113,17 @@ export class ApiDSBuilder extends DSBuilder<
return false;
}
const maybeApiUrl =
typeof apiSchema === 'string'
? apiSchema
: isObject(apiSchema)
? apiSchema?.url || ''
: '';
if (
typeof apiSchema === 'string' &&
/^(get|post|put|delete|option):/.test(apiSchema)
typeof maybeApiUrl === 'string' &&
(/^(get|post|put|delete|patch|option|jsonp):/.test(apiSchema) ||
!~maybeApiUrl.indexOf('api://'))
) {
return true;
}
@ -1359,13 +1368,12 @@ export class ApiDSBuilder extends DSBuilder<
primaryField = 'id',
listApi,
editApi,
bulkEditApi,
simpleQueryFields
bulkEditApi
} = scaffoldConfig || {};
const enableBulkEdit = feats?.includes('BulkEdit');
const enableBulkDelete = feats?.includes('BulkDelete');
const enableEdit = feats?.includes('Edit');
const multiple = enableBulkEdit || enableBulkDelete;
const enableMultiple = enableBulkEdit || enableBulkDelete;
const id = generateNodeId();
/** 暂时不考虑 cards 和 list */
@ -1375,18 +1383,10 @@ export class ApiDSBuilder extends DSBuilder<
mode: 'table2',
dsType: this.key,
syncLocation: true,
multiple: multiple,
/** 通过脚手架创建的单条操作入口都在操作列中所以rowSelection暂时不需要radio */
...(multiple
? {
rowSelection: {
type: 'checkbox',
keyField: primaryField
}
}
: {}),
loadType: 'pagination',
/** CRUD2使用 selectable + multiple 控制Table2使用 rowSelection 控制 */
...(enableMultiple ? {selectable: true, multiple: true} : {}),
primaryField: primaryField,
loadType: 'pagination',
api: listApi,
...(enableBulkEdit ? {quickSaveApi: bulkEditApi} : {}),
...(enableEdit ? {quickSaveItemApi: editApi} : {}),

View File

@ -67,7 +67,7 @@ export interface DSBuilderInterface<
filterByFeat(feat: any): boolean;
/** 根据schema判断是否匹配当前数据源 */
match(schema?: any): boolean;
match(schema?: any, key?: string): boolean;
/** 当前上下文中使用的字段 */
getContextFields(options: T): Promise<any>;
@ -209,7 +209,7 @@ export abstract class DSBuilder<T extends DSBuilderBaseOptions>
return feat && this.features.includes(feat);
}
abstract match(schema?: any): boolean;
abstract match(schema?: any, key?: string): boolean;
abstract getContextFields(options: T): Promise<any>;

View File

@ -4,7 +4,10 @@
*/
import {builderFactory, DSBuilderInterface} from './DSBuilder';
import {EditorManager} from 'amis-editor-core';
import type {EditorManager} from 'amis-editor-core';
import type {GenericSchema} from './type';
import type {Option} from 'amis-core';
export class DSBuilderManager {
private builders: Map<string, DSBuilderInterface>;
@ -42,6 +45,11 @@ export class DSBuilderManager {
return builder ? builder : this.getDefaultBuilder();
}
/**
* Key
*
* @returns Key
*/
getDefaultBuilderKey() {
const collections = Array.from(this.builders.entries()).filter(
([_, builder]) => builder?.disabledOn?.() !== true
@ -56,6 +64,11 @@ export class DSBuilderManager {
return defaultKey;
}
/**
*
*
* @returns {Object}
*/
getDefaultBuilder() {
const collections = Array.from(this.builders.entries()).filter(
([_, builder]) => builder?.disabledOn?.() !== true
@ -70,6 +83,11 @@ export class DSBuilderManager {
return defaultBuilder;
}
/**
*
*
* @returns
*/
getAvailableBuilders() {
return Array.from(this.builders.entries())
.filter(([_, builder]) => builder?.disabledOn?.() !== true)
@ -78,24 +96,65 @@ export class DSBuilderManager {
});
}
getDSSelectorSchema(patch: Record<string, any>) {
/**
* Schema
*
* @param patch -
* @param config -
* @returns pipeIn等属性
*/
getDSSelectorSchema(
patch: Record<string, any>,
config?: {
/** 组件 Schema */
schema: GenericSchema;
/** 组件数据源 Key */
sourceKey: string;
/** 获取默认值函数 */
getDefautlValue?: (key: string, builder: DSBuilderInterface) => Boolean;
}
) {
const {schema, sourceKey, getDefautlValue} = config || {};
const builders = this.getAvailableBuilders();
const options = builders.map(([key, builder]) => ({
label: builder.name,
value: key
}));
let defaultValue: string | undefined = schema?.dsType;
const options: Option[] = [];
for (const [key, builder] of builders) {
if (schema && !defaultValue) {
if (
getDefautlValue &&
typeof getDefautlValue === 'function' &&
getDefautlValue(key, builder)
) {
defaultValue = key;
} else if (builder.match(schema, sourceKey)) {
defaultValue = key;
}
}
options.push({
label: builder.name,
value: key
});
}
return {
type: 'radios',
label: '数据来源',
name: 'dsType',
visible: options.length > 0,
selectFirst: true,
options: options,
...(defaultValue ? {value: defaultValue} : {}),
...patch
};
}
/**
*
*
* @param callback
* @returns
*/
buildCollectionFromBuilders(
callback: (
builder: DSBuilderInterface,

View File

@ -128,3 +128,7 @@ export const FormOperatorMap: Record<FormOperatorValue, FormOperator> = {
};
export const ModelDSBuilderKey = 'model-entity';
export const ApiDSBuilderKey = 'api';
export const ApiCenterDSBuilderKey = 'apicenter';

View File

@ -25,7 +25,8 @@ import {
DSBuilderManager,
DSFeatureEnum,
DSFeatureType,
ModelDSBuilderKey
ModelDSBuilderKey,
ApiDSBuilderKey
} from '../../builder';
import {
getEventControlConfig,
@ -58,11 +59,11 @@ export class BaseCRUDPlugin extends BasePlugin {
rendererName = 'crud2';
name = '增删改查';
name = '表格2.0';
panelTitle = '增删改查';
panelTitle = '表格2.0';
subPanelTitle = '增删改查';
subPanelTitle = '表格2.0';
icon = 'fa fa-table';
@ -320,6 +321,9 @@ export class BaseCRUDPlugin extends BasePlugin {
scaffoldConfig: config
});
/** 脚手架构建的 Schema 加个标识符避免addChild替换 Schema ID */
schema.__origin = 'scaffold';
return schema;
},
validate: (data: CRUDScaffoldConfig, form: IFormStore) => {
@ -515,13 +519,11 @@ export class BaseCRUDPlugin extends BasePlugin {
renderBasicPropsCollapse(context: BuildPanelEventContext) {
/** 动态加载的配置集合 */
const dc = this.dynamicControls;
return {
title: '基本',
order: 1,
body: [
/** 数据源类型 */
this.dsManager.getDSSelectorSchema({
/** 数据源控件 */
const generateDSControls = () => {
/** 数据源类型 */
const dsTypeSelector = this.dsManager.getDSSelectorSchema(
{
type: 'select',
label: '数据源',
onChange: (
@ -546,12 +548,19 @@ export class BaseCRUDPlugin extends BasePlugin {
}
return value;
}
}),
/** 数据源配置 */
...this.dsManager.buildCollectionFromBuilders((builder, builderKey) => {
},
{schema: context?.schema, sourceKey: 'api'}
);
/** 默认数据源类型 */
const defaultDsType = dsTypeSelector.value;
/** 数据源配置 */
const dsSettings = this.dsManager.buildCollectionFromBuilders(
(builder, builderKey) => {
return {
type: 'container',
visibleOn: `data.dsType == null || data.dsType === '${builderKey}'`,
visibleOn: `data.dsType == null ? '${builderKey}' === '${
defaultDsType || ApiDSBuilderKey
}' : data.dsType === '${builderKey}'`,
body: builder.makeSourceSettingForm({
feat: 'List',
renderer: 'crud',
@ -563,9 +572,55 @@ export class BaseCRUDPlugin extends BasePlugin {
/** 因为会使用 container 包裹,所以加一个 margin-bottom */
className: 'mb-3'
};
}),
}
);
return [dsTypeSelector, ...dsSettings];
};
return {
title: '基本',
order: 1,
body: [
...generateDSControls(),
/** 主键配置TODO支持联合主键 */
dc?.primaryField?.(context),
/** 可选择配置,这里的配置会覆盖底层 Table 的 rowSelection 中的配置 */
getSchemaTpl('switch', {
name: 'selectable',
label: tipedLabel('可选择', '开启后支持选择表格行数据'),
pipeIn: (value: boolean | undefined, formStore: IFormStore) => {
if (typeof value === 'boolean') {
return value;
}
const rowSelection = formStore?.data?.rowSelection;
return rowSelection && isObject(rowSelection);
}
}),
{
type: 'container',
className: 'ae-ExtendMore mb-3',
visibleOn:
"data.selectable || (data.rowSelection && data.rowSelection?.type !== 'radio')",
body: [
getSchemaTpl('switch', {
name: 'multiple',
label: '可多选',
pipeIn: (value: boolean | undefined, formStore: IFormStore) => {
if (typeof value === 'boolean') {
return value;
}
const rowSelection = formStore?.data?.rowSelection;
return rowSelection && isObject(rowSelection)
? rowSelection.type !== 'radio'
: false;
}
})
]
},
{
name: 'placeholder',
pipeIn: defaultValue('暂无数据'),

View File

@ -25,7 +25,8 @@ import {
DSFeatureType,
DSBuilderManager,
DSFeatureEnum,
ModelDSBuilderKey
ModelDSBuilderKey,
ApiDSBuilderKey
} from '../../builder';
import {FormOperatorMap} from '../../builder/constants';
import {getEventControlConfig} from '../../renderer/event-control/helper';
@ -538,6 +539,9 @@ export class FormPlugin extends BasePlugin {
scaffoldConfig: config
});
/** 脚手架构建的 Schema 加个标识符避免addChild替换 Schema ID */
schema.__origin = 'scaffold';
return schema;
},
validate: (data: FormScaffoldConfig, form: IFormStore) => {
@ -625,6 +629,89 @@ export class FormPlugin extends BasePlugin {
).startsWith('model://')) &&
!schema.api.strategy;
/** 数据源控件 */
const generateDSControls = () => {
const dsTypeSelector = this.dsManager.getDSSelectorSchema(
{
type: 'select',
label: '数据源',
onChange: (
value: string,
oldValue: string,
model: IFormItemStore,
form: IFormStore
) => {
if (value !== oldValue) {
const data = form.data;
Object.keys(data).forEach(key => {
if (
/^(insert|edit|bulkEdit)Fields$/i.test(key) ||
/^(insert|edit|bulkEdit)Api$/i.test(key)
) {
form.deleteValueByName(key);
}
});
form.deleteValueByName('__fields');
form.deleteValueByName('__relations');
form.deleteValueByName('initApi');
form.deleteValueByName('api');
}
return value;
}
},
{
schema: context?.schema,
sourceKey: 'api',
getDefautlValue: (key, builder) => {
const schema = context?.schema;
let dsType = schema?.dsType;
// TODO: api和initApi可能是混合模式的场景
if (
builder.match(schema, 'api') ||
builder.match(schema, 'initApi')
) {
dsType = key;
}
return dsType;
}
}
);
/** 默认数据源类型 */
const defaultDsType = dsTypeSelector.value;
/** 数据源配置 */
const dsSettings = flatten(
this.Features.map(feat =>
this.dsManager.buildCollectionFromBuilders(
(builder, builderKey, index) => ({
type: 'container',
className: 'form-item-gap',
visibleOn: `data.feat === '${
feat.value
}' && (data.dsType == null ? '${builderKey}' === '${
defaultDsType || ApiDSBuilderKey
}' : data.dsType === '${builderKey}')`,
body: flatten([
builder.makeSourceSettingForm({
feat: feat.value,
renderer: 'form',
inScaffold: false,
sourceSettings: {
renderLabel: true,
userOrders: false
}
})
])
})
)
)
);
return [dsTypeSelector, ...dsSettings];
};
return [
getSchemaTpl('tabs', [
{
@ -639,8 +726,25 @@ export class FormPlugin extends BasePlugin {
type: 'select',
name: 'feat',
label: '使用场景',
value: DSFeatureEnum.Insert,
options: this.Features,
pipeIn: (
value: FormPluginFeat | undefined,
formStore: IFormStore
) => {
let feat = value;
if (!value) {
feat =
formStore?.data?.initApi != null
? DSFeatureEnum.Edit
: DSFeatureEnum.Insert;
}
/** 存量数据可能未设置过feat, 需要在数据域中 set 一下 */
formStore.setValueByName('feat', feat);
return feat;
},
onChange: (
value: FormPluginFeat,
oldValue: FormPluginFeat,
@ -660,83 +764,7 @@ export class FormPlugin extends BasePlugin {
}
}
},
this.dsManager.getDSSelectorSchema({
type: 'select',
label: '数据源',
pipeIn: (value: any, form: any) => {
if (value !== undefined) {
return value;
}
const api = form.data?.api || form.data?.initApi;
let dsType = 'api';
if (!api) {
} else if (typeof api === 'string') {
dsType = api.startsWith('api://')
? 'apicenter'
: 'api';
} else if (api?.url) {
dsType = api.url.startsWith('api://')
? 'apicenter'
: 'api';
} else if (api?.entity) {
dsType = ModelDSBuilderKey;
}
// 需要 set 一下,否则 buildCollectionFromBuilders 里的内容条件不满足
form.setValueByName('dsType', dsType);
return dsType;
},
onChange: (
value: string,
oldValue: string,
model: IFormItemStore,
form: IFormStore
) => {
if (value !== oldValue) {
const data = form.data;
Object.keys(data).forEach(key => {
if (
/^(insert|edit|bulkEdit)Fields$/i.test(key) ||
/^(insert|edit|bulkEdit)Api$/i.test(key)
) {
form.deleteValueByName(key);
}
});
form.deleteValueByName('__fields');
form.deleteValueByName('__relations');
form.deleteValueByName('initApi');
form.deleteValueByName('api');
}
return value;
}
}),
/** 数据源配置 */
...flatten(
this.Features.map(feat =>
this.dsManager.buildCollectionFromBuilders(
(builder, builderKey, index) => ({
type: 'container',
className: 'form-item-gap',
visibleOn: `data.feat === '${feat.value}' && (data.dsType === '${builderKey}' || (!data.dsType && ${index} === 0))`,
body: flatten([
builder.makeSourceSettingForm({
feat: feat.value,
renderer: 'form',
inScaffold: false,
sourceSettings: {
renderLabel: true,
userOrders: false
}
})
])
})
)
)
)
...generateDSControls()
]
},
{

View File

@ -13,7 +13,7 @@ import {
tipedLabel
} from 'amis-editor-core';
import {DSBuilderManager} from '../builder/DSBuilderManager';
import {DSFeatureEnum, ModelDSBuilderKey} from '../builder';
import {DSFeatureEnum, ModelDSBuilderKey, ApiDSBuilderKey} from '../builder';
import {getEventControlConfig} from '../renderer/event-control/helper';
import type {
@ -186,66 +186,75 @@ export class ServicePlugin extends BasePlugin {
panelBodyCreator = (context: BaseEventContext) => {
const dsManager = this.dsManager;
/** 数据来源选择器 */
const dsTypeSelect = () =>
dsManager.getDSSelectorSchema({
type: 'select',
mode: 'horizontal',
horizontal: {
justify: true,
left: 'col-sm-4'
},
onChange: (value: any, oldValue: any, model: any, form: any) => {
if (value !== oldValue) {
const data = form.data;
Object.keys(data).forEach(key => {
if (
key?.toLowerCase()?.endsWith('fields') ||
key?.toLowerCase().endsWith('api')
) {
form.deleteValueByName(key);
}
});
form.deleteValueByName('__fields');
form.deleteValueByName('__relations');
form.setValueByName('api', undefined);
/** 数据源控件 */
const generateDSControls = () => {
const dsTypeSelector = dsManager.getDSSelectorSchema(
{
type: 'select',
mode: 'horizontal',
horizontal: {
justify: true,
left: 'col-sm-4'
},
onChange: (value: any, oldValue: any, model: any, form: any) => {
if (value !== oldValue) {
const data = form.data;
Object.keys(data).forEach(key => {
if (
key?.toLowerCase()?.endsWith('fields') ||
key?.toLowerCase().endsWith('api')
) {
form.deleteValueByName(key);
}
});
form.deleteValueByName('__fields');
form.deleteValueByName('__relations');
form.setValueByName('api', undefined);
}
return value;
}
return value;
}
});
/** 数据源配置 */
const dsSetting = dsManager.buildCollectionFromBuilders(
(builder, builderKey) => {
return {
type: 'container',
visibleOn: `this.dsType == null || this.dsType === '${builderKey}'`,
body: flattenDeep([
builder.makeSourceSettingForm({
feat: 'View',
renderer: 'service',
inScaffold: false,
sourceSettings: {
name: 'api',
label: '接口配置',
mode: 'horizontal',
...(builderKey === 'api' || builderKey === 'apicenter'
? {
horizontalConfig: {
labelAlign: 'left',
horizontal: {
justify: true,
left: 4
},
{schema: context?.schema, sourceKey: 'api'}
);
/** 默认数据源类型 */
const defaultDsType = dsTypeSelector.value;
const dsSettings = dsManager.buildCollectionFromBuilders(
(builder, builderKey) => {
return {
type: 'container',
visibleOn: `data.dsType == null ? '${builderKey}' === '${
defaultDsType || ApiDSBuilderKey
}' : data.dsType === '${builderKey}'`,
body: flattenDeep([
builder.makeSourceSettingForm({
feat: 'View',
renderer: 'service',
inScaffold: false,
sourceSettings: {
name: 'api',
label: '接口配置',
mode: 'horizontal',
...(builderKey === 'api' || builderKey === 'apicenter'
? {
horizontalConfig: {
labelAlign: 'left',
horizontal: {
justify: true,
left: 4
}
}
}
}
: {}),
useFieldManager: builderKey === ModelDSBuilderKey
}
})
])
};
}
);
: {}),
useFieldManager: builderKey === ModelDSBuilderKey
}
})
])
};
}
);
return [dsTypeSelector, ...dsSettings];
};
return getSchemaTpl('tabs', [
{
@ -257,8 +266,7 @@ export class ServicePlugin extends BasePlugin {
title: '基本',
body: [
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
dsTypeSelect(),
...dsSetting
...generateDSControls()
]
},
{

View File

@ -30,6 +30,7 @@ import {
import {resolveArrayDatasource} from '../util';
import type {SchemaObject} from 'amis';
import type {IFormItemStore, IFormStore} from 'amis-core';
import type {EditorManager} from 'amis-editor-core';
export const Table2RenderereEvent: RendererPluginEvent[] = [
@ -741,7 +742,7 @@ export class Table2Plugin extends BasePlugin {
}
panelBodyCreator = (context: BaseEventContext) => {
const isCRUDBody = this.isCRUDContext(context);
const isCRUDContext = this.isCRUDContext(context);
const dc = this.dynamicControls;
return getSchemaTpl('tabs', [
@ -755,13 +756,13 @@ export class Table2Plugin extends BasePlugin {
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
getSchemaTpl('formulaControl', {
label: tipedLabel('数据源', '绑定当前上下文变量'),
hidden: isCRUDBody,
hidden: isCRUDContext,
name: 'source',
pipeIn: defaultValue('${items}')
}),
dc?.primaryField?.(context),
isCRUDBody ? null : dc?.quickSaveApi?.(context),
isCRUDBody ? null : dc?.quickSaveItemApi?.(context),
isCRUDContext ? null : dc?.primaryField?.(context),
isCRUDContext ? null : dc?.quickSaveApi?.(context),
isCRUDContext ? null : dc?.quickSaveItemApi?.(context),
getSchemaTpl('switch', {
name: 'title',
label: '显示标题',
@ -824,7 +825,7 @@ export class Table2Plugin extends BasePlugin {
}
}),
getSchemaTpl('tablePlaceholder', {
hidden: isCRUDBody
hidden: isCRUDContext
})
// TODD: 组件功能没有支持,暂时隐藏
// {
@ -844,7 +845,7 @@ export class Table2Plugin extends BasePlugin {
pipeIn: (value: any) => !!value,
pipeOut: (value: any) => value
}),
isCRUDBody
isCRUDContext
? null
: {
type: 'ae-Switch-More',
@ -894,28 +895,44 @@ export class Table2Plugin extends BasePlugin {
mode: 'normal',
name: 'rowSelection',
label: '可选择',
visibleOn: 'data.selectable',
hiddenOnDefault: true,
formType: 'extend',
form: {
body: [
dc?.rowSelectionKeyField?.(context),
{
name: 'rowSelection.type',
label: '选择类型',
type: 'button-group-select',
options: [
{
label: '多选',
value: 'checkbox'
/** 如果为 CRUD 背景下,主键配置、选择类型在 CRUD 面板中,此处应该隐藏 */
isCRUDContext
? null
: dc?.rowSelectionKeyField?.(context),
isCRUDContext
? null
: {
name: 'rowSelection.type',
label: '选择类型',
type: 'button-group-select',
options: [
{
label: '多选',
value: 'checkbox'
},
{
label: '单选',
value: 'radio'
}
],
pipeIn: (value: any, formStore: IFormStore) => {
if (value != null && typeof value === 'string') {
return value;
}
const schema = formStore?.data;
return schema?.selectable === true
? schema.multiple
? 'checkbox'
: 'radio'
: 'checkbox';
}
},
{
label: '单选',
value: 'radio'
}
],
pipeIn: defaultValue('checkbox')
},
getSchemaTpl('switch', {
name: 'rowSelection.fixed',
label: '固定选择列'
@ -980,7 +997,7 @@ export class Table2Plugin extends BasePlugin {
}));
}
}
]
].filter(Boolean)
}
},
getSchemaTpl('formulaControl', {

View File

@ -439,6 +439,7 @@ export class FieldSetting extends React.Component<
className={'w-full'}
hasError={!!fieldState.error}
searchable
simpleValue
disabled={false}
clearable={false}
popOverContainer={popOverContainer}
@ -522,6 +523,7 @@ export class FieldSetting extends React.Component<
className={'w-full'}
hasError={!!fieldState.error}
searchable
simpleValue
disabled={false}
clearable={false}
popOverContainer={popOverContainer}

View File

@ -1695,6 +1695,7 @@ setSchemaTpl('anchorNavTitle', {
required: true
});
/** 给 CRUD2 使用 */
setSchemaTpl('primaryField', {
type: 'input-text',
name: 'primaryField',
@ -1702,5 +1703,17 @@ setSchemaTpl('primaryField', {
'主键',
'每行记录的唯一标识符,通常用于行选择、批量操作等场景。'
),
pipeIn: defaultValue('id')
pipeIn: (value: any, formStore: any) => {
const rowSelection = formStore?.data?.rowSelection;
if (value == null || typeof value !== 'string') {
return rowSelection &&
rowSelection?.keyField &&
typeof rowSelection.keyField === 'string'
? rowSelection?.keyField
: 'id';
}
return value;
}
});

View File

@ -126,6 +126,7 @@
margin-left: px2rem(4px);
display: flex;
justify-content: center;
align-items: center;
}
&-singleValue {