diff --git a/lerna.json b/lerna.json index 230db3a1b..52872941f 100644 --- a/lerna.json +++ b/lerna.json @@ -5,5 +5,5 @@ "packages/amis-ui", "packages/amis" ], - "version": "3.4.0" + "version": "3.4.0-alpha.4" } diff --git a/packages/amis-core/package.json b/packages/amis-core/package.json index fcd6e9345..4b5f0da9b 100644 --- a/packages/amis-core/package.json +++ b/packages/amis-core/package.json @@ -1,6 +1,6 @@ { "name": "amis-core", - "version": "3.4.0", + "version": "3.4.0-alpha.4", "description": "amis-core", "main": "lib/index.js", "module": "esm/index.js", @@ -46,7 +46,7 @@ "esm" ], "dependencies": { - "amis-formula": "^3.4.0", + "amis-formula": "^3.4.0-alpha.4", "classnames": "2.3.2", "file-saver": "^2.0.2", "hoist-non-react-statics": "^3.3.2", diff --git a/packages/amis-editor-core/package.json b/packages/amis-editor-core/package.json index 87b9790c8..d5964dc28 100644 --- a/packages/amis-editor-core/package.json +++ b/packages/amis-editor-core/package.json @@ -1,6 +1,6 @@ { "name": "amis-editor-core", - "version": "5.5.0", + "version": "5.5.1-alpha.8", "description": "amis 可视化编辑器", "main": "lib/index.js", "module": "esm/index.js", diff --git a/packages/amis-editor-core/scss/control/_crud2-control.scss b/packages/amis-editor-core/scss/control/_crud2-control.scss index 212b37f35..e120e0357 100644 --- a/packages/amis-editor-core/scss/control/_crud2-control.scss +++ b/packages/amis-editor-core/scss/control/_crud2-control.scss @@ -36,6 +36,7 @@ display: flex; align-items: center; justify-content: flex-start; + max-width: 140px; & > span { max-width: 100%; @@ -85,6 +86,7 @@ height: #{px2rem(20px)}; margin-right: #{px2rem(8px)}; scale: 0.9; + max-width: 80px; &--cascading { color: #531dab; diff --git a/packages/amis-editor-core/scss/control/_table-column-width-control.scss b/packages/amis-editor-core/scss/control/_table-column-width-control.scss index 78c7323b9..0eb31fd3f 100644 --- a/packages/amis-editor-core/scss/control/_table-column-width-control.scss +++ b/packages/amis-editor-core/scss/control/_table-column-width-control.scss @@ -8,5 +8,6 @@ display: flex; flex-direction: row; justify-content: space-between; + align-items: baseline; } } diff --git a/packages/amis-editor-core/scss/editor.scss b/packages/amis-editor-core/scss/editor.scss index 7f93a2448..73d4795c2 100644 --- a/packages/amis-editor-core/scss/editor.scss +++ b/packages/amis-editor-core/scss/editor.scss @@ -41,6 +41,7 @@ @import './control/_status'; @import './control/_icon-button-group-control'; @import './control/_flex-setting-control'; +@import './control/table-column-width-control.scss'; @import './control/crud2-control'; /* 样式控件 */ diff --git a/packages/amis-editor-core/src/manager.ts b/packages/amis-editor-core/src/manager.ts index 86bf3acb2..861fdc89b 100644 --- a/packages/amis-editor-core/src/manager.ts +++ b/packages/amis-editor-core/src/manager.ts @@ -230,6 +230,37 @@ export class EditorManager { // 自动加载预先注册的自定义组件 autoPreRegisterEditorCustomPlugins(); + /** 在顶层对外部注册的Plugin和builtInPlugins合并去重 */ + const externalPlugins = (config?.plugins || []).forEach(external => { + if ( + Array.isArray(external) || + !external.priority || + !Number.isInteger(external.priority) + ) { + return; + } + + const idx = builtInPlugins.findIndex( + builtIn => + !Array.isArray(builtIn) && + !Array.isArray(external) && + builtIn.id === external.id && + builtIn?.prototype instanceof BasePlugin + ); + + if (~idx) { + const current = builtInPlugins[idx] as PluginClass; + const currentPriority = + current.priority && Number.isInteger(current.priority) + ? current.priority + : 0; + /** 同ID Plugin根据优先级决定是否替换掉Builtin中的Plugin */ + if (external.priority > currentPriority) { + builtInPlugins.splice(idx, 1); + } + } + }); + this.plugins = (config.disableBultinPlugin ? [] : builtInPlugins) // 页面设计器注册的插件列表 .concat(this.normalizeScene(config?.plugins)) .filter(p => { diff --git a/packages/amis-editor/package.json b/packages/amis-editor/package.json index c2cb1a962..8516edcb0 100644 --- a/packages/amis-editor/package.json +++ b/packages/amis-editor/package.json @@ -1,6 +1,6 @@ { "name": "amis-editor", - "version": "5.5.0", + "version": "5.5.1-alpha.8", "description": "amis 可视化编辑器", "main": "lib/index.js", "module": "esm/index.js", diff --git a/packages/amis-editor/src/builder/ApiDSBuilder.ts b/packages/amis-editor/src/builder/ApiDSBuilder.ts index 59f768c34..f99258c81 100644 --- a/packages/amis-editor/src/builder/ApiDSBuilder.ts +++ b/packages/amis-editor/src/builder/ApiDSBuilder.ts @@ -277,7 +277,22 @@ export class ApiDSBuilder extends DSBuilder< )}` ) }) - : null + : null, + /** CRUD的快速编辑接口 */ + ...(feat === 'List' && renderer === 'crud' && !inScaffold + ? [ + getSchemaTpl('apiControl', { + ...baseApiSchemaConfig, + name: 'quickSaveApi', + label: tipedLabel('快速保存', '快速编辑后用来批量保存的 API') + }), + getSchemaTpl('apiControl', { + ...baseApiSchemaConfig, + name: 'quickSaveItemApi', + label: tipedLabel('快速保存单条', '即时保存时使用的 API') + }) + ] + : []) ].filter(Boolean); } @@ -300,7 +315,7 @@ export class ApiDSBuilder extends DSBuilder< label: renderLabel === false ? false : '字段', renderer, feat, - options: { + config: { showInputType: renderer === 'form' || (renderer === 'crud' && @@ -374,7 +389,7 @@ export class ApiDSBuilder extends DSBuilder< label: key, name: key, displayType: 'tpl', - inputType: inputType, + inputType, checked: true }); }); @@ -455,6 +470,7 @@ export class ApiDSBuilder extends DSBuilder< actions: [ { actionType: 'search', + groupType: 'component', componentId: componentId } ] @@ -529,20 +545,21 @@ export class ApiDSBuilder extends DSBuilder< buildBaseFormSchema( options: ApiDSBuilderOptions, - schemaPatch?: GenericSchema + schemaPatch?: GenericSchema, + componentId?: string ) { schemaPatch = schemaPatch || {}; const {feat, renderer, scaffoldConfig} = options || {}; if (!feat) { - return {...schemaPatch}; + return {...schemaPatch, ...(componentId ? {id: componentId} : {})}; } const fieldsKey = this.getFieldsKey(options); const apiKey = this.getApiKey(options); const fields: ScaffoldField[] = (scaffoldConfig as any)?.[fieldsKey] ?? []; const apiSchema = (scaffoldConfig as any)?.[apiKey]; - const id = generateNodeId(); + const id = componentId ?? generateNodeId(); let schema: GenericSchema = { id, type: 'form', @@ -580,7 +597,7 @@ export class ApiDSBuilder extends DSBuilder< schema.static = true; } - return {...schema, ...schemaPatch}; + return {...schema, ...schemaPatch, id}; } async buildInsertSchema( @@ -591,10 +608,10 @@ export class ApiDSBuilder extends DSBuilder< const {insertApi} = scaffoldConfig || {}; if (renderer === 'form') { - return this.buildBaseFormSchema({...options}); + return this.buildBaseFormSchema({...options}, undefined, componentId); } - const formId = generateNodeId(); + const formId = componentId ?? generateNodeId(); const formActions = [ { type: 'button', @@ -693,7 +710,7 @@ export class ApiDSBuilder extends DSBuilder< const isForm = renderer === 'form'; if (isForm) { - return this.buildBaseFormSchema(options); + return this.buildBaseFormSchema(options, undefined, componentId); } const {editApi, initApi} = scaffoldConfig || {}; @@ -750,7 +767,7 @@ export class ApiDSBuilder extends DSBuilder< const isForm = renderer === 'form'; if (isForm) { - return this.buildBaseFormSchema(options); + return this.buildBaseFormSchema(options, undefined, componentId); } const formId = generateNodeId(); @@ -816,15 +833,14 @@ export class ApiDSBuilder extends DSBuilder< actions: [ { actionType: 'ajax', - args: { - api: deleteApi, - data: { - '&': '$$' - } + api: deleteApi, + data: { + '&': '$$' } }, { actionType: 'search', + groupType: 'component', componentId: componentId } ] @@ -855,12 +871,11 @@ export class ApiDSBuilder extends DSBuilder< actions: [ { actionType: 'ajax', - args: { - api: bulkDeleteApi - } + api: bulkDeleteApi }, { actionType: 'search', + groupType: 'component', componentId: componentId } ] @@ -1265,18 +1280,22 @@ export class ApiDSBuilder extends DSBuilder< const actions = get(host, 'onEvent.click.actions', []); const actionSchema = actions.find( (action: any) => - action?.actionType === 'ajax' && action?.args?.api != null + action?.actionType === 'ajax' && + (action?.api != null || action?.args?.api != null) ); - bulkDeleteApi = get(actionSchema, 'args.api', ''); + bulkDeleteApi = + get(actionSchema, 'api', '') || get(actionSchema, 'args.api', ''); } else if (value === 'Delete') { feats.push('Delete'); const actions = get(host, 'onEvent.click.actions', []); const actionSchema = actions.find( (action: any) => - action?.actionType === 'ajax' && action?.args?.api != null + action?.actionType === 'ajax' && + (action?.api != null || action?.args?.api != null) ); - deleteApi = get(actionSchema, 'args.api', ''); + deleteApi = + get(actionSchema, 'api', '') || get(actionSchema, 'args.api', ''); } else if (Array.isArray(value) && value.includes('SimpleQuery')) { feats.push('SimpleQuery'); @@ -1384,13 +1403,14 @@ export class ApiDSBuilder extends DSBuilder< const {feat, scaffoldConfig} = options; const {initApi, __pristineSchema} = scaffoldConfig || {}; let formSchema: GenericSchema; + const id = __pristineSchema?.id ?? generateNodeId(); if (feat === 'Insert') { - formSchema = await this.buildInsertSchema<'form'>(options); + formSchema = await this.buildInsertSchema<'form'>(options, id); } else if (feat === 'Edit') { - formSchema = await this.buildEditSchema(options); + formSchema = await this.buildEditSchema(options, id); } else { - formSchema = await this.buildBulkEditSchema(options); + formSchema = await this.buildBulkEditSchema(options, id); } const baseSchema = { @@ -1400,8 +1420,6 @@ export class ApiDSBuilder extends DSBuilder< }; if (__pristineSchema && isObject(__pristineSchema)) { - const id = __pristineSchema.id ?? generateNodeId(); - return { ...__pristineSchema, ...baseSchema, diff --git a/packages/amis-editor/src/builder/DSBuilder.ts b/packages/amis-editor/src/builder/DSBuilder.ts index fdb63260c..aae22795e 100644 --- a/packages/amis-editor/src/builder/DSBuilder.ts +++ b/packages/amis-editor/src/builder/DSBuilder.ts @@ -51,12 +51,12 @@ export interface DSBuilderInterface< /** 是否为默认 */ isDefault?: boolean; - /** 是否默认隐藏 */ - defaultHidden?: boolean; - /** 实例获取数据源的key */ key: string; + /** 是否禁用 */ + disabledOn?: () => boolean; + /** 获取功能场景的value */ getFeatValueByKey(feat: DSFeatureType): string; @@ -185,7 +185,7 @@ export abstract class DSBuilder readonly order: number; /** 是否为默认 */ readonly isDefault?: boolean; - defaultHidden?: boolean; + features: DSFeatureType[]; constructor(readonly manager: EditorManager) {} diff --git a/packages/amis-editor/src/builder/DSBuilderManager.ts b/packages/amis-editor/src/builder/DSBuilderManager.ts index 75781b1f0..bcaaafd6b 100644 --- a/packages/amis-editor/src/builder/DSBuilderManager.ts +++ b/packages/amis-editor/src/builder/DSBuilderManager.ts @@ -43,7 +43,9 @@ export class DSBuilderManager { } getDefaultBuilderKey() { - const collections = Array.from(this.builders.entries()); + const collections = Array.from(this.builders.entries()).filter( + ([_, builder]) => builder?.disabledOn?.() !== true + ); const [defaultKey, _] = collections.find(([_, builder]) => builder.isDefault === true) ?? collections.sort((lhs, rhs) => { @@ -55,17 +57,22 @@ export class DSBuilderManager { } getDefaultBuilder() { + const collections = Array.from(this.builders.entries()).filter( + ([_, builder]) => builder?.disabledOn?.() !== true + ); const [_, defaultBuilder] = - Array.from(this.builders.entries()).find( - ([_, builder]) => builder.isDefault === true - ) ?? []; + collections.find(([_, builder]) => builder.isDefault === true) ?? + collections.sort((lhs, rhs) => { + return (lhs[1].order ?? 0) - (rhs[1].order ?? 0); + })?.[0] ?? + []; - return defaultBuilder!; + return defaultBuilder; } getAvailableBuilders() { return Array.from(this.builders.entries()) - .filter(item => item[1]?.defaultHidden !== true) + .filter(([_, builder]) => builder?.disabledOn?.() !== true) .sort((lhs, rhs) => { return (lhs[1].order ?? 0) - (rhs[1].order ?? 0); }); diff --git a/packages/amis-editor/src/plugin/CRUD2/BaseCRUD.tsx b/packages/amis-editor/src/plugin/CRUD2/BaseCRUD.tsx index e2c82f9c1..d5224b4ef 100644 --- a/packages/amis-editor/src/plugin/CRUD2/BaseCRUD.tsx +++ b/packages/amis-editor/src/plugin/CRUD2/BaseCRUD.tsx @@ -4,6 +4,7 @@ */ import React from 'react'; +import cx from 'classnames'; import isFunction from 'lodash/isFunction'; import flattenDeep from 'lodash/flattenDeep'; import cloneDeep from 'lodash/cloneDeep'; @@ -12,7 +13,7 @@ import uniqBy from 'lodash/uniqBy'; import get from 'lodash/get'; import uniq from 'lodash/uniq'; import sortBy from 'lodash/sortBy'; -import {toast, autobind, isObject} from 'amis'; +import {toast, autobind, isObject, Icon} from 'amis'; import { BasePlugin, ScaffoldForm, @@ -46,9 +47,8 @@ import type {CRUDScaffoldConfig} from '../../builder/type'; /** 需要动态控制的属性 */ export type CRUD2DynamicControls = Partial< Record< - 'columns' | 'toolbar' | 'filters', - | Record - | ((context: BuildPanelEventContext) => Record) + 'columns' | 'toolbar' | 'filters' | 'primaryField', + (context: BuildPanelEventContext) => any > >; export class BaseCRUDPlugin extends BasePlugin { @@ -214,7 +214,9 @@ export class BaseCRUDPlugin extends BasePlugin { }; } ), - getSchemaTpl('primaryField') + getSchemaTpl('primaryField', { + visibleOn: `!data.dsType || data.dsType !== '${ModelDSBuilderKey}'` + }) ] }, { @@ -332,7 +334,8 @@ export class BaseCRUDPlugin extends BasePlugin { return errors; } - const fieldErrors = FieldSetting.validator(form.data[fieldsKey]); + const fieldErrors = false; + // FieldSetting.validator(form.data[fieldsKey]); if (fieldErrors) { errors[fieldsKey] = fieldErrors; @@ -441,7 +444,9 @@ export class BaseCRUDPlugin extends BasePlugin { /** 工具栏配置 */ toolbar: context => this.renderToolbarCollapse(context), /** 搜索栏 */ - filters: context => this.renderFiltersCollapse(context) + filters: context => this.renderFiltersCollapse(context), + /** 主键 */ + primaryField: context => getSchemaTpl('primaryField') }; /** 需要动态控制的控件 */ @@ -494,7 +499,10 @@ export class BaseCRUDPlugin extends BasePlugin { /** 其他类别 */ this.renderOthersCollapse(context), /** 状态类别 */ - getSchemaTpl('status', {readonly: false}) + { + title: '状态', + body: [getSchemaTpl('hidden'), getSchemaTpl('visible')] + } ].filter(Boolean) ) ] @@ -503,6 +511,9 @@ export class BaseCRUDPlugin extends BasePlugin { /** 基础配置 */ renderBasicPropsCollapse(context: BuildPanelEventContext) { + /** 动态加载的配置集合 */ + const dc = this.dynamicControls; + return { title: '基本', order: 1, @@ -552,7 +563,7 @@ export class BaseCRUDPlugin extends BasePlugin { }; }), /** 主键配置,TODO:支持联合主键 */ - getSchemaTpl('primaryField'), + dc?.primaryField?.(context), { name: 'placeholder', pipeIn: defaultValue('暂无数据'), @@ -653,12 +664,15 @@ export class BaseCRUDPlugin extends BasePlugin { /** 分页类别 */ renderPaginationCollapse(context: BuildPanelEventContext) { + const isPagination = 'data.loadType === "pagination"'; + const isInfinity = 'data.loadType === "more"'; + return { order: 30, title: '分页设置', body: [ { - label: '更多模式', + label: '分页模式', type: 'select', name: 'loadType', options: [ @@ -725,20 +739,55 @@ export class BaseCRUDPlugin extends BasePlugin { getSchemaTpl('switch', { name: 'loadDataOnce', label: '前端分页', - visibleOn: 'data.loadType === "pagination"' + visibleOn: isPagination }), + getSchemaTpl('switch', { + name: 'loadDataOnceFetchOnFilter', + label: tipedLabel( + '过滤时刷新', + '在开启前端分页时,表头过滤后是否重新请求初始化 API' + ), + visibleOn: isPagination + ' && data.loadDataOnce' + }), + getSchemaTpl('switch', { + name: 'keepItemSelectionOnPageChange', + label: tipedLabel( + '保留选择项', + '默认切换页面、搜索后,用户选择项会被清空,开启此功能后会保留用户选择,可以实现跨页面批量操作。' + ), + pipeIn: defaultValue(false), + visibleOn: isPagination + }), + getSchemaTpl('switch', { + name: 'autoJumpToTopOnPagerChange', + label: tipedLabel('翻页后回到顶部', '当切分页的时候,是否自动跳顶部'), + pipeIn: defaultValue(true), + visibleOn: isPagination + }), + { + name: 'perPage', + type: 'input-number', + label: tipedLabel( + '每页数量', + '无限加载时,根据此项设置其每页加载数量,留空即不限制' + ), + clearValueOnEmpty: true, + clearable: true, + pipeIn: defaultValue(10), + visibleOn: isInfinity + }, { type: 'button', label: '点击编辑分页组件', block: true, - className: 'm-b', + className: 'mb-1', level: 'enhance', - // icon: 'fa fa-plus', visibleOn: 'data.loadType === "pagination"', onClick: () => { const findPage: any = findSchema( - context?.schema ?? context?.node?.schema ?? {}, - item => item.type === 'pagination', + context?.node?.schema ?? {}, + item => + item.type === 'pagination' || item.behavior === 'Pagination', 'headerToolbar', 'footerToolbar' ); @@ -749,26 +798,7 @@ export class BaseCRUDPlugin extends BasePlugin { } this.manager.setActiveId(findPage.$$id); } - }, - { - name: 'perPage', - type: 'input-number', - label: '每页数量', - visibleOn: 'data.loadType === "more"' - }, - getSchemaTpl('switch', { - name: 'keepItemSelectionOnPageChange', - label: tipedLabel( - '保留选择项', - '默认切换页面、搜索后,用户选择项会被清空,开启此功能后会保留用户选择,可以实现跨页面批量操作。' - ), - visibleOn: 'data.loadType === "pagination"' - }), - getSchemaTpl('switch', { - name: 'autoJumpToTopOnPagerChange', - label: tipedLabel('翻页后回到顶部', '当切分页的时候,是否自动跳顶部'), - visibleOn: 'data.loadType === "pagination"' - }) + } ] }; } @@ -779,27 +809,47 @@ export class BaseCRUDPlugin extends BasePlugin { order: 25, title: '其他', body: [ - getSchemaTpl('interval', { - formItems: [ - getSchemaTpl('switch', { - name: 'silentPolling', - label: '静默拉取', - pipeIn: defaultValue(false) - }) - ], - intervalConfig: { - control: { - type: 'input-number', - name: 'interval' - } - }, - switchMoreConfig: { - isChecked: (e: any) => { - return !!get(e.data, 'interval'); - }, - autoFocus: false, - trueValue: 10000 + { + type: 'ae-switch-more', + mode: 'normal', + formType: 'extend', + visibleOn: 'data.api', + label: tipedLabel( + '接口轮询', + '开启初始化接口轮询,开启后会按照设定的时间间隔轮询调用接口' + ), + autoFocus: false, + form: { + body: [ + { + type: 'input-number', + name: 'interval', + label: tipedLabel('轮询间隔', '定时刷新间隔,单位 ms'), + step: 10, + min: 1000 + }, + getSchemaTpl('tplFormulaControl', { + name: 'stopAutoRefreshWhen', + label: tipedLabel( + '停止条件', + '定时刷新停止表达式,条件满足后则停止定时刷新,否则会持续轮询调用初始化接口。' + ), + visibleOn: '!!data.interval' + }), + getSchemaTpl('switch', { + name: 'stopAutoRefreshWhenModalIsOpen', + label: tipedLabel( + '模态窗口期间停止', + '当页面中存在弹窗时停止接口轮询,避免中断操作' + ) + }) + ] } + }, + getSchemaTpl('switch', { + name: 'silentPolling', + label: tipedLabel('静默拉取', '刷新时是否隐藏加载动画'), + pipeIn: defaultValue(false) }) ] }; @@ -821,12 +871,12 @@ export class BaseCRUDPlugin extends BasePlugin { getSchemaTpl('className', { name: 'headerToolbarClassName', - label: '顶部外层' + label: '顶部工具栏' }), getSchemaTpl('className', { name: 'footerToolbarClassName', - label: '底部外层' + label: '底部工具栏' }) ] }) @@ -848,6 +898,30 @@ export class BaseCRUDPlugin extends BasePlugin { }; } + /** 重新构建 API */ + panelFormPipeOut = async (schema: any) => { + const entity = schema?.api?.entity; + + if (!entity || schema?.dsType !== ModelDSBuilderKey) { + return schema; + } + + const builder = this.dsManager.getBuilderBySchema(schema); + + try { + const updatedSchema = await builder.buildApiSchema({ + schema, + renderer: 'crud', + sourceKey: 'api' + }); + return updatedSchema; + } catch (e) { + console.error(e); + } + + return schema; + }; + emptyContainer = (align?: 'left' | 'right', body: any[] = []) => ({ type: 'container', body, diff --git a/packages/amis-editor/src/plugin/Form/Combo.tsx b/packages/amis-editor/src/plugin/Form/Combo.tsx index 0125dd4be..5211437d6 100644 --- a/packages/amis-editor/src/plugin/Form/Combo.tsx +++ b/packages/amis-editor/src/plugin/Form/Combo.tsx @@ -1,4 +1,4 @@ -import {setVariable} from 'amis-core'; +import {setVariable, someTree} from 'amis-core'; import { BaseEventContext, BasePlugin, diff --git a/packages/amis-editor/src/plugin/Form/Form.tsx b/packages/amis-editor/src/plugin/Form/Form.tsx index 1d382563b..f595bc389 100644 --- a/packages/amis-editor/src/plugin/Form/Form.tsx +++ b/packages/amis-editor/src/plugin/Form/Form.tsx @@ -1,7 +1,7 @@ import cx from 'classnames'; import flatten from 'lodash/flatten'; import cloneDeep from 'lodash/cloneDeep'; -import {isObject, someTree} from 'amis-core'; +import {isObject} from 'amis-core'; import { BasePlugin, tipedLabel, @@ -13,8 +13,6 @@ import { defaultValue, getSchemaTpl, jsonToJsonSchema, - BuildPanelEventContext, - BasicPanelItem, RendererPluginAction, RendererPluginEvent, EditorNodeType, @@ -34,12 +32,17 @@ import {getEventControlConfig} from '../../renderer/event-control/helper'; import {FieldSetting} from '../../renderer/FieldSetting'; import type {FormSchema} from 'amis/lib/Schema'; -import type {IFormStore, IFormItemStore, RendererConfig, Schema} from 'amis-core'; +import type { + IFormStore, + IFormItemStore, + Schema, + RendererConfig +} from 'amis-core'; import type {FormScaffoldConfig} from '../../builder'; export type FormPluginFeat = Extract< DSFeatureType, - 'Insert' | 'Edit' | 'BulkEdit' + 'Insert' | 'Edit' | 'BulkEdit' | 'View' >; export interface ExtendFormSchema extends FormSchema { @@ -70,7 +73,7 @@ export class FormPlugin extends BasePlugin { $schema = '/schemas/FormSchema.json'; - tags = ['功能', '数据容器']; + tags = ['数据容器']; order = -900; @@ -365,7 +368,7 @@ export class FormPlugin extends BasePlugin { }> = [ {label: '新增', value: DSFeatureEnum.Insert}, {label: '编辑', value: DSFeatureEnum.Edit}, - {label: '批量编辑', value: DSFeatureEnum.BulkEdit}, + {label: '批量编辑', value: DSFeatureEnum.BulkEdit, disabled: true}, {label: '查看', value: DSFeatureEnum.View, disabled: true} ]; @@ -380,6 +383,8 @@ export class FormPlugin extends BasePlugin { /** 表单脚手架 */ get scaffoldForm(): ScaffoldForm { + const features = this.Features.filter(f => !f.disabled); + return { title: '表单创建向导', mode: { @@ -396,7 +401,7 @@ export class FormPlugin extends BasePlugin { name: 'feat', label: '使用场景', value: DSFeatureEnum.Insert, - options: this.Features, + options: features, onChange: ( value: FormPluginFeat, oldValue: FormPluginFeat, @@ -462,7 +467,7 @@ export class FormPlugin extends BasePlugin { }), /** 数据源相关配置 */ ...flatten( - this.Features.map(feat => + features.map(feat => this.dsManager.buildCollectionFromBuilders( (builder, builderKey) => { return { @@ -944,6 +949,24 @@ export class FormPlugin extends BasePlugin { defaultValue: 'normal' }), getSchemaTpl('horizontal'), + { + name: 'labelAlign', + label: '标签对齐方式', + type: 'button-group-select', + size: 'sm', + visibleOn: "${mode === 'horizontal'}", + pipeIn: defaultValue('right', false), + options: [ + { + label: '左对齐', + value: 'left' + }, + { + label: '右对齐', + value: 'right' + } + ] + }, { label: '列数', name: 'columnCount', @@ -1033,6 +1056,31 @@ export class FormPlugin extends BasePlugin { ]; }; + /** 重新构建 API */ + panelFormPipeOut = async (schema: any) => { + const entity = schema?.api?.entity; + + if (!entity || schema?.dsType !== ModelDSBuilderKey) { + return schema; + } + + const builder = this.dsManager.getBuilderBySchema(schema); + + try { + const updatedSchema = await builder.buildApiSchema({ + schema, + renderer: 'form', + sourceKey: 'api', + feat: schema.feat ?? 'Insert' + }); + return updatedSchema; + } catch (e) { + console.error(e); + } + + return schema; + }; + afterUpdate(event: PluginEvent) { const context = event.context; diff --git a/packages/amis-editor/src/plugin/Form/InputTable.tsx b/packages/amis-editor/src/plugin/Form/InputTable.tsx index 14bfe40bb..86a4e5c77 100644 --- a/packages/amis-editor/src/plugin/Form/InputTable.tsx +++ b/packages/amis-editor/src/plugin/Form/InputTable.tsx @@ -1064,12 +1064,14 @@ export class TableControlPlugin extends BasePlugin { filterProps(props: any) { const arr = resolveArrayDatasource(props); + /** 可 */ if (!Array.isArray(arr) || !arr.length) { const mockedData: any = {}; if (Array.isArray(props.columns)) { props.columns.forEach((column: any) => { - if (column.name) { + /** 可编辑状态下不写入 Mock 数据,避免误导用户 */ + if (column.name && !props.editable) { setVariable(mockedData, column.name, mockValue(column)); } }); diff --git a/packages/amis-editor/src/plugin/Pagination.tsx b/packages/amis-editor/src/plugin/Pagination.tsx index 5597f1928..ca3854ead 100644 --- a/packages/amis-editor/src/plugin/Pagination.tsx +++ b/packages/amis-editor/src/plugin/Pagination.tsx @@ -7,6 +7,7 @@ import { getSchemaTpl, registerEditorPlugin } from 'amis-editor-core'; +import sortBy from 'lodash/sortBy'; import {getEventControlConfig} from '../renderer/event-control/helper'; export class PaginationPlugin extends BasePlugin { @@ -131,15 +132,29 @@ export class PaginationPlugin extends BasePlugin { } ], pipeIn: (value: any) => { - if (!value) { - value = this.lastLayoutSetting; - } else if (typeof value === 'string') { + if (typeof value === 'string') { value = (value as string).split(','); + } else if (!value || !Array.isArray(value)) { + value = this.lastLayoutSetting; } - return this.layoutOptions.map(v => ({ - ...v, - checked: value.includes(v.value) - })); + + return sortBy( + this.layoutOptions.map(op => ({ + ...op, + checked: value.includes(op.value) + })), + [ + item => { + const idx = value.findIndex(v => v === item.value); + return ~idx ? idx : Infinity; + } + ] + ); + + // return this.layoutOptions.map(v => ({ + // ...v, + // checked: value.includes(v.value) + // })); }, pipeOut: (value: any[]) => { this.lastLayoutSetting = value @@ -191,7 +206,7 @@ export class PaginationPlugin extends BasePlugin { }), { name: 'perPage', - type: 'input-text', + type: 'input-number', label: '默认每页条数', visibleOn: '(!data.mode || data.mode === "normal") && data.layout?.includes("perPage")' @@ -212,7 +227,11 @@ export class PaginationPlugin extends BasePlugin { }, { title: '状态', - body: [getSchemaTpl('disabled')] + body: [ + getSchemaTpl('disabled'), + getSchemaTpl('hidden'), + getSchemaTpl('visible') + ] } ]) }, diff --git a/packages/amis-editor/src/plugin/Service.tsx b/packages/amis-editor/src/plugin/Service.tsx index 1cc2e2227..eb4b82e30 100644 --- a/packages/amis-editor/src/plugin/Service.tsx +++ b/packages/amis-editor/src/plugin/Service.tsx @@ -328,6 +328,29 @@ export class ServicePlugin extends BasePlugin { ]); }; + panelFormPipeOut = async (schema: any) => { + const entity = schema?.api?.entity; + + if (!entity || schema?.dsType !== ModelDSBuilderKey) { + return schema; + } + + const builder = this.dsManager.getBuilderBySchema(schema); + + try { + const updatedSchema = await builder.buildApiSchema({ + schema, + renderer: 'service', + sourceKey: 'api' + }); + return updatedSchema; + } catch (e) { + console.error(e); + } + + return schema; + }; + async buildDataSchemas( node: EditorNodeType, region?: EditorNodeType, @@ -358,29 +381,6 @@ export class ServicePlugin extends BasePlugin { return jsonschema; } - panelFormPipeOut = async (schema: any) => { - const entity = schema?.api?.entity; - - if (!entity || schema?.dsType !== ModelDSBuilderKey) { - return schema; - } - - const builder = this.dsManager.getBuilderBySchema(schema); - - try { - const updatedSchema = await builder.buildApiSchema({ - schema, - renderer: 'service', - sourceKey: 'api' - }); - return updatedSchema; - } catch (e) { - console.error(e); - } - - return schema; - }; - rendererBeforeDispatchEvent(node: EditorNodeType, e: any, data: any) { if (e === 'fetchInited') { const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`); diff --git a/packages/amis-editor/src/plugin/Table2.tsx b/packages/amis-editor/src/plugin/Table2.tsx index 88a778dd1..abab52be0 100644 --- a/packages/amis-editor/src/plugin/Table2.tsx +++ b/packages/amis-editor/src/plugin/Table2.tsx @@ -297,6 +297,7 @@ export type Table2DynamicControls = Partial< | 'quickSaveItemApi' | 'draggable' | 'itemDraggableOn' + | 'saveOrderApi' | 'columnTogglable', (context: BaseEventContext) => any > @@ -654,48 +655,70 @@ export class Table2Plugin extends BasePlugin { } protected _dynamicControls: Table2DynamicControls = { - primaryField: () => { - return getSchemaTpl('primaryField'); + primaryField: context => { + return getSchemaTpl('primaryField', { + /** CRUD下,该项配置提升到CRUD中 */ + hiddenOn: `data.type && (data.type === "crud" || data.type === "crud2")` + }); }, - quickSaveApi: () => { + quickSaveApi: context => { return getSchemaTpl('apiControl', { - label: '快速保存', name: 'quickSaveApi', - renderLabel: true + renderLabel: false, + label: { + type: 'tpl', + tpl: '快速保存', + className: 'flex items-end' + } }); }, - quickSaveItemApi: () => { + quickSaveItemApi: context => { return getSchemaTpl('apiControl', { - label: '快速保存单条', name: 'quickSaveItemApi', - renderLabel: true + renderLabel: false, + label: { + type: 'tpl', + tpl: '快速保存单条', + className: 'flex items-end' + } }); }, - rowSelectionKeyField: () => { + rowSelectionKeyField: context => { return { type: 'input-text', name: 'rowSelection.keyField', label: '数据源key' }; }, - expandableKeyField: () => { + expandableKeyField: context => { return { type: 'input-text', name: 'rowSelection.keyField', label: '数据源key' }; }, - draggable: () => + draggable: context => getSchemaTpl('switch', { name: 'draggable', label: '可拖拽' }), - itemDraggableOn: () => + itemDraggableOn: context => getSchemaTpl('formulaControl', { label: '可拖拽条件', name: 'itemDraggableOn' }), - columnTogglable: () => false + saveOrderApi: context => { + return getSchemaTpl('apiControl', { + name: 'saveOrderApi', + renderLabel: false, + label: { + type: 'tpl', + tpl: '保存排序', + className: 'flex items-end' + } + }); + }, + columnTogglable: context => false }; /** 需要动态控制的控件 */ @@ -713,9 +736,12 @@ export class Table2Plugin extends BasePlugin { this._dynamicControls = {...this._dynamicControls, ...controls}; } + isCRUDContext(context: BaseEventContext) { + return context.schema.type === 'crud2' || context.schema.type === 'crud'; + } + panelBodyCreator = (context: BaseEventContext) => { - const isCRUDBody = - context.schema.type === 'crud2' || context.schema.type === 'crud'; + const isCRUDBody = this.isCRUDContext(context); const dc = this.dynamicControls; return getSchemaTpl('tabs', [ @@ -734,8 +760,8 @@ export class Table2Plugin extends BasePlugin { pipeIn: defaultValue('${items}') }), dc?.primaryField?.(context), - dc?.quickSaveApi?.(context), - dc?.quickSaveItemApi?.(context), + isCRUDBody ? null : dc?.quickSaveApi?.(context), + isCRUDBody ? null : dc?.quickSaveItemApi?.(context), getSchemaTpl('switch', { name: 'title', label: '显示标题', @@ -799,13 +825,14 @@ export class Table2Plugin extends BasePlugin { }), getSchemaTpl('tablePlaceholder', { hidden: isCRUDBody - }), - { - type: 'input-number', - name: 'combineNum', - label: '合并单元格' - } - ] + }) + // TODD: 组件功能没有支持,暂时隐藏 + // { + // type: 'input-number', + // name: 'combineNum', + // label: '合并单元格' + // } + ].filter(Boolean) }, { title: '列设置', @@ -1070,6 +1097,7 @@ export class Table2Plugin extends BasePlugin { }, dc?.draggable?.(context), dc?.itemDraggableOn?.(context), + dc?.saveOrderApi?.(context), { name: 'showBadge', label: '行角标', diff --git a/packages/amis-editor/src/renderer/FieldSetting.tsx b/packages/amis-editor/src/renderer/FieldSetting.tsx index 2f14a4b9c..0142441fe 100644 --- a/packages/amis-editor/src/renderer/FieldSetting.tsx +++ b/packages/amis-editor/src/renderer/FieldSetting.tsx @@ -32,7 +32,7 @@ interface FieldSettingProps extends FormControlProps { /** 脚手架渲染类型 */ renderer?: string; feat: DSFeatureType; - options: { + config: { showInputType?: boolean; showDisplayType?: boolean; }; @@ -50,7 +50,7 @@ export class FieldSetting extends React.Component< {loading: boolean} > { static defaultProps = { - options: { + config: { showInputType: true, showDisplayType: true } @@ -308,11 +308,11 @@ export class FieldSetting extends React.Component< defaultValue: formDefaultValue, env, renderer, - options, + config, data: ctx, feat } = this.props; - const {showDisplayType, showInputType} = options || {}; + const {showDisplayType, showInputType} = config || {}; const isForm = renderer === 'form'; const defaultValue = Array.isArray(formDefaultValue) ? {items: formDefaultValue} diff --git a/packages/amis-editor/src/renderer/TableColumnWidthControl.tsx b/packages/amis-editor/src/renderer/TableColumnWidthControl.tsx index 7f8327603..029707e50 100644 --- a/packages/amis-editor/src/renderer/TableColumnWidthControl.tsx +++ b/packages/amis-editor/src/renderer/TableColumnWidthControl.tsx @@ -168,11 +168,15 @@ export default class TableColumnWidthControl extends React.Component< control: { type: 'input-number', min: 0, - value, - onChange: (val: number) => this.handleChange('fixed', val) + value + // onChange: (val: number) => this.handleChange('fixed', val) }, - unit: 'px' - }) + unit: 'px', + className: 'mt-3' + }), + { + onChange: (val: number) => this.handleChange('fixed', val) + } ); } diff --git a/packages/amis-editor/src/renderer/crud2-control/CRUDColumnControl.tsx b/packages/amis-editor/src/renderer/crud2-control/CRUDColumnControl.tsx index 666990ca9..12f4bb2c3 100644 --- a/packages/amis-editor/src/renderer/crud2-control/CRUDColumnControl.tsx +++ b/packages/amis-editor/src/renderer/crud2-control/CRUDColumnControl.tsx @@ -6,8 +6,8 @@ import React from 'react'; import {findDOMNode} from 'react-dom'; import Sortable from 'sortablejs'; -import get from 'lodash/get'; import {FormItem, Button, Icon, toast, Tag, Spinner, autobind} from 'amis'; +import {TooltipWrapper} from 'amis-ui'; import {JSONPipeIn} from 'amis-editor-core'; import AddColumnModal from './AddColumnModal'; @@ -208,14 +208,17 @@ export class CRUDColumnControl extends React.Component< @autobind handleEdit(item: Option) { - const {manager} = this.props; + const {manager, node} = this.props; + const columns = node?.schema?.columns ?? []; + const idx = columns.findIndex(c => c.id === item.pristine.id); - if (!item.nodeId) { + if (!~idx) { toast.warning(`未找到对应列「${item.label}」`); return; } - manager.setActiveId(item.nodeId); + // FIXME: 理论上用item.nodeId就可以,不知道为何会重新构建一次导致store中node.id更新 + manager.setActiveId(columns[idx]?.$$id); } /** 添加列 */ @@ -322,16 +325,33 @@ export class CRUDColumnControl extends React.Component< @autobind renderOption(item: Option, index: number) { - const {classnames: cx, data: ctx, render} = this.props; + const { + classnames: cx, + data: ctx, + render, + popOverContainer, + env + } = this.props; return (
  • -
    - {item.label} -
    + +
    + {item.label} +
    +
    {item.hidden || !item?.context?.isCascadingField ? null : ( diff --git a/packages/amis-editor/src/renderer/crud2-control/CRUDFiltersControl.tsx b/packages/amis-editor/src/renderer/crud2-control/CRUDFiltersControl.tsx index 68ae927c2..5be37593e 100644 --- a/packages/amis-editor/src/renderer/crud2-control/CRUDFiltersControl.tsx +++ b/packages/amis-editor/src/renderer/crud2-control/CRUDFiltersControl.tsx @@ -17,6 +17,7 @@ import { Tag, autobind } from 'amis'; +import {TooltipWrapper} from 'amis-ui'; import {DSFeatureEnum} from '../../builder/constants'; import {traverseSchemaDeep} from '../../builder/utils'; import {deepRemove} from '../../plugin/CRUD2/utils'; @@ -103,7 +104,7 @@ export class CRUDFiltersControl extends React.Component< ? (option.label as any).tpl /** 处理 SchemaObject 的场景 */ : option.name, value: option.name ?? (option as any).key, - /** 使用$$id用于定位 */ + /** 使用id用于定位 */ nodeId: option.$$id, pristine: option }; @@ -455,7 +456,7 @@ export class CRUDFiltersControl extends React.Component< value.filter( (item: any) => item?.behavior !== DSFeatureEnum.AdvancedQuery && - item.type === 'condition-builder' + item.type !== 'condition-builder' ) ]; } @@ -674,13 +675,24 @@ export class CRUDFiltersControl extends React.Component< @autobind renderOption(item: Option, index: number) { - const {classnames: cx, feat} = this.props; + const {classnames: cx, feat, popOverContainer, env} = this.props; return (
  • -
    - {item.label} -
    + +
    + {item.label} +
    +
    {item?.context?.isCascadingField ? ( @@ -688,8 +700,8 @@ export class CRUDFiltersControl extends React.Component< label={item?.context?.modelLabel} displayMode="normal" className={cx( - 'CRUDConfigControl-list-item-tag', - 'CRUDConfigControl-list-item-tag--cascading' + 'ae-CRUDConfigControl-list-item-tag', + 'ae-CRUDConfigControl-list-item-tag--cascading' )} /> ) : null} diff --git a/packages/amis-editor/src/renderer/crud2-control/CRUDToolbarControl.tsx b/packages/amis-editor/src/renderer/crud2-control/CRUDToolbarControl.tsx index 50eb5dc5d..81ff4eb1a 100644 --- a/packages/amis-editor/src/renderer/crud2-control/CRUDToolbarControl.tsx +++ b/packages/amis-editor/src/renderer/crud2-control/CRUDToolbarControl.tsx @@ -7,6 +7,7 @@ import React from 'react'; import {findDOMNode} from 'react-dom'; import cloneDeep from 'lodash/cloneDeep'; import {FormItem, Button, Icon, toast, Spinner, autobind} from 'amis'; +import {TooltipWrapper} from 'amis-ui'; import {findTreeAll} from 'amis-core'; import {JSONPipeIn} from 'amis-editor-core'; import {DSFeature, DSFeatureType, DSFeatureEnum} from '../../builder'; @@ -17,7 +18,9 @@ import type {EditorNodeType} from 'amis-editor-core'; import type {ColumnSchema} from 'amis/lib/renderers/Table2'; import type {DSBuilderInterface} from '../../builder'; -type ActionValue = Extract; +type ActionValue = + | Extract + | 'custom'; interface Option { label: string; @@ -81,7 +84,12 @@ export class CRUDToolbarControl extends React.Component< const store = manager.store; const node: EditorNodeType = store.getNodeById(nodeId); const actions = findTreeAll(node.children, item => - ['Insert', 'BulkEdit', 'BulkDelete'].includes(item.schema.behavior) + [ + DSFeatureEnum.Insert, + DSFeatureEnum.BulkEdit, + DSFeatureEnum.BulkDelete, + 'custom' + ].includes(item.schema.behavior) ) as unknown as EditorNodeType[]; return actions; @@ -98,7 +106,7 @@ export class CRUDToolbarControl extends React.Component< const behavior = schema.behavior as ActionValue; return { - label: DSFeature[behavior].label, + label: this.getOptionLabel(schema, behavior), value: behavior, nodeId: schema.$$id, node: node, @@ -109,6 +117,10 @@ export class CRUDToolbarControl extends React.Component< this.setState({options}); } + getOptionLabel(schema: any, behavior: ActionValue) { + return behavior === 'custom' ? schema.label : DSFeature[behavior].label; + } + @autobind handleEdit(item: Option) { const {manager} = this.props; @@ -188,6 +200,18 @@ export class CRUDToolbarControl extends React.Component< CRUDSchemaID ); break; + default: + scaffold = { + type: 'button', + label: '按钮', + behavior: 'custom', + className: 'm-r-xs', + onEvent: { + click: { + actions: [] + } + } + }; } if (!scaffold) { @@ -199,7 +223,7 @@ export class CRUDToolbarControl extends React.Component< const actionSchema = JSONPipeIn({...scaffold}); options.push({ - label: DSFeature[type].label, + label: this.getOptionLabel(actionSchema, type), value: type, nodeId: actionSchema.$$id, pristine: actionSchema @@ -245,13 +269,24 @@ export class CRUDToolbarControl extends React.Component< @autobind renderOption(item: Option, index: number) { - const {classnames: cx} = this.props; + const {classnames: cx, popOverContainer, env} = this.props; return (
  • -
    - {item.label} -
    + +
    + {item.label} +
    +