feat: CRUD & Form 脚手架构建优化, 列表可视化设计优化; chore: DSBuilder迁移至amis-editor (#8003)

* feat: CRUD & Form 脚手架构建优化; chore: DSBuilder迁移至amis-editor (#7531)

* tpl最大显示行数支持

* 编辑器组件通用假数据支持

* each循环次数支持

* 列表可视化设计优化

* fix: 数据源构造器 & Form & CRUD2 & Service相关问题修复 (#8002)

* chore: 修复正则报错

* chore: 修复正则报错-02

* chore: 更新 CRUD 组件 Card 模式单测快照

* chore: 优化假数据merge

---------

Co-authored-by: zhangtao07 <zhang.tao.1006@163.com>
This commit is contained in:
RUNZE LU 2023-09-06 21:30:21 +08:00 committed by GitHub
parent 415ad0135d
commit d27948f0f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
107 changed files with 11880 additions and 5153 deletions

View File

@ -4,7 +4,7 @@ title: 可视化编辑器
目前 amis 可视化编辑器也作为单独的 npm 包发布了出来,可以通过 npm 安装使用。
在线体验https://aisuda.github.io/amis-editor-demo
在线体验https://aisuda.github.io/amis-editor-demo
示例代码https://github.com/aisuda/amis-editor-demo
## 使用
@ -46,7 +46,7 @@ render() {
- `className?: string` 额外加个 css 类名,辅助样式定义。
- `schemas?: JSONSchemaObject` 用来定义有哪些全局变量,辅助编辑器格式化绑定全局数据。
- `theme?: string` amis 主题
- `schemaFilter?: (schema: any) => any` 配置过滤器。可以用来实现 api proxy比如原始配置中请求地址是 `http://baidu.com` 如果直接给编辑器预览请求,很可能会报跨域,可以自动转成 `/api/proxy?_url=xxxx`,走 proxy 解决。
- `schemaFilter?: (schema: any, isPreview?: boolean) => any` 配置过滤器。可以用来实现 api proxy比如原始配置中请求地址是 `http://baidu.com` 如果直接给编辑器预览请求,很可能会报跨域,可以自动转成 `/api/proxy?_url=xxxx`,走 proxy 解决。
- `amisEnv?: any` 这是是给 amis 的 Env 对象,具体请前往 [env 说明](../start/getting-started#env)
- `disableBultinPlugin?: boolean` 是否禁用内置插件
- `disablePluginList?: Array<string> | (id: string, plugin: PluginClass) => boolean` 禁用插件列表

View File

@ -807,13 +807,13 @@ export const components = [
import('../../docs/zh-CN/components/table.md').then(wrapDoc)
)
},
// {
// label: 'Table2 表格',
// path: '/zh-CN/components/table2',
// component: React.lazy(() =>
// import('../../docs/zh-CN/components/table2.md').then(wrapDoc)
// )
// },
{
label: 'Table2 表格',
path: '/zh-CN/components/table2',
component: React.lazy(() =>
import('../../docs/zh-CN/components/table2.md').then(wrapDoc)
)
},
{
label: 'Table View 表格视图',
path: '/zh-CN/components/table-view',

View File

@ -5,5 +5,5 @@
"packages/amis-ui",
"packages/amis"
],
"version": "3.4.0"
"version": "3.4.1-alpha.0"
}

View File

@ -1,6 +1,6 @@
{
"name": "amis-core",
"version": "3.4.0",
"version": "3.4.1-alpha.0",
"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.1-alpha.0",
"classnames": "2.3.2",
"file-saver": "^2.0.2",
"hoist-non-react-statics": "^3.3.2",

View File

@ -661,6 +661,7 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
return createObject(superData, {
total: self.total,
page: self.page,
perPage: self.perPage,
items: self.items.concat(),
selectedItems: self.selectedItems.concat(),
unSelectedItems: self.unSelectedItems.concat()

View File

@ -3,7 +3,7 @@
*
* @param string
*/
export const keyToPath = (string: string) => {
export const keyToPath = (string: string = '') => {
const result = [];
if (string.charCodeAt(0) === '.'.charCodeAt(0)) {

View File

@ -1,6 +1,6 @@
{
"name": "amis-editor-core",
"version": "5.5.0",
"version": "5.5.2-alpha.0",
"description": "amis 可视化编辑器",
"main": "lib/index.js",
"module": "esm/index.js",

View File

@ -4,3 +4,28 @@
border: 1px solid rgba(#23b7e5, 1);
z-index: 999999;
}
.ae-Editor-list {
.ae-Editor-listItem,
.ae-Editor-eachItem {
position: relative !important;
&::after {
position: absolute;
content: '';
width: 100%;
height: 100%;
pointer-events: all;
background: rgba(22, 40, 60, 0.2) url(../static/indication.png) repeat;
z-index: 100;
top: 0;
left: 0;
cursor: not-allowed;
}
}
.ae-Editor-eachItem:first-child::after,
.cards-items > div:first-child > div::after {
display: none;
}
}

View File

@ -17,6 +17,7 @@
&-content {
@include flexBox();
align-items: stretch;
.ae-ApiControl-input {
background: var(--Form-input-bg);
@ -44,6 +45,10 @@
width: 100%;
height: calc(var(--Form-input-lineHeight) * var(--Form-input-fontSize));
}
.ae-ApiControl-setting-button {
height: unset;
}
}
}

View File

@ -0,0 +1,163 @@
/**
* @file crud2-control.scss
* @desc CRUD2相关控件及样式
*/
.ae-CRUDConfigControl {
margin-bottom: var(--Form-item-gap);
&-list {
margin: 0;
padding: 0;
border-radius: 4px;
border: 1px solid #e8e9eb;
padding-top: 3px;
padding-bottom: 3px;
&-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 30px;
padding: 0 var(--gap-sm);
&.is-draggable:hover {
background-color: #f9f9f9;
cursor: move;
}
&-dragger {
cursor: move;
margin: 0 var(--gap-sm) 0 0;
color: rgba(232, 233, 235, 1);
}
&-info {
display: flex;
align-items: center;
justify-content: flex-start;
max-width: 140px;
& > span {
max-width: 100%;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 500;
color: #151b26;
}
}
&-actions {
display: flex;
align-items: center;
justify-content: space-between;
& > button {
color: #151b26;
margin: 0;
padding-left: 0;
padding-right: 0;
&:not(:last-child) {
margin-right: var(--gap-sm);
}
& > svg {
width: px2rem(16px);
height: px2rem(16px);
&.icon-share-link {
width: px2rem(14px);
height: px2rem(14px);
}
}
}
}
&-tag {
cursor: auto;
background-color: transparent;
border: 1px solid #2468f2;
color: #2468f2;
border-radius: 2px;
line-height: #{px2rem(18px)};
height: #{px2rem(20px)};
margin-right: #{px2rem(8px)};
scale: 0.9;
max-width: 80px;
&--cascading {
color: #531dab;
border-color: #531dab;
}
}
}
}
&-placeholder {
color: #b4b6ba;
padding-top: px2rem(10px);
text-align: center;
vertical-align: middle;
}
&-header {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
& > span:nth-child(1) {
margin-bottom: 0;
}
&-actions {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
&-switch {
margin-right: var(--gap-sm);
}
&-divider {
width: 1px;
height: 16px;
margin: 0 4px;
background-color: #dfdfdf;
}
}
}
&-dropdown {
/* margin-bottom: var(--Form-mode-default-labelGap); */
& > button {
color: #4c5664;
margin: 0;
padding-left: 0;
padding-right: 0;
font-weight: bold;
& > svg {
width: px2rem(14px);
height: px2rem(14px);
margin-right: var(--gap-xs);
}
}
}
&-footer {
display: flex;
flex-flow: row-reverse nowrap;
}
}
.ae-CRUDConfigControl-modal {
&-btn-loading {
--Spinner-color: #fff;
}
}

View File

@ -8,13 +8,23 @@
display: flex;
height: 30px;
margin-bottom: 12px;
position: relative;
align-items: center;
:not(:last-child) {
margin-right: px2rem(8px);
}
&-go {
&-content {
flex-grow: 1;
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
& > .ae-FeatureControlItem-go {
width: 100%;
}
}
&-label {
@ -42,6 +52,27 @@
}
}
}
&-dragBar {
position: absolute;
z-index: 2;
top: 50%;
transform: translateY(-50%);
left: 3px;
cursor: move;
& > svg {
fill: #e7e7e7;
color: #e7e7e7;
}
}
&:hover &-dragBar {
& > svg {
color: transparent;
fill: transparent;
}
}
}
&-action {
@ -55,24 +86,9 @@
}
&--menus {
width: calc(100% - 12px);
margin-left: 6px;
/* width: calc(100% - 12px);
margin-left: 6px; */
text-align: center;
}
}
&-dragBar {
position: absolute;
display: none;
z-index: 2;
top: 50%;
transform: translateY(-50%);
left: 3px;
cursor: pointer;
}
&:hover &-dragBar {
display: block;
color: #fff;
}
}

View File

@ -15,9 +15,9 @@
position: absolute;
top: 0;
left: 0;
background-color: rgba($color: #000000, $alpha: 0.4);
color: #fff;
cursor: pointer;
color: #fff;
background-color: rgba($color: #000000, $alpha: 0.55);
}
&:hover {

View File

@ -0,0 +1,13 @@
/**
* @file table-column-width-control.scss
* @desc 表格列宽控件
*/
.ae-columnWidthControl {
&-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
}
}

View File

@ -73,6 +73,11 @@
> li {
margin-right: 10px;
cursor: pointer;
&.is-loading {
display: flex;
cursor: unset;
}
}
&--fullscreen {
> a {

View File

@ -41,6 +41,8 @@
@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';
/* 样式控件 */
@import './style-control/box-model';
@ -1233,7 +1235,7 @@
[data-region] {
position: relative;
min-height: 34px;
min-height: 10px;
&:empty {
min-width: 20px;
@ -1768,15 +1770,27 @@ div.ae-DragImage {
width: px2rem(700px);
@include panel-sm-content();
--radio-default-default-fontSize: #{$Editor-right-panel-font-size};
--Table-thead-fontSize: #{$Editor-right-panel-font-size};
--button-size-default-fontSize: #{$Editor-right-panel-font-size};
--checkbox-checkbox-default-fontSize: #{$Editor-right-panel-font-size};
--Tabs--vertical-fontSize: #{$Editor-right-panel-font-size};
--Tabs--vertical-active-fontSize: #{$Editor-right-panel-font-size};
--Tabs--vertical-hover-fontSize: #{$Editor-right-panel-font-size};
.ae-Steps {
margin: auto;
max-width: px2rem(350px);
--Steps-title-fontsize: #{px2rem(14px)};
&-Icon {
display: flex !important;
width: px2rem(22px) !important;
height: px2rem(22px) !important;
margin-top: px2rem(5px);
font-size: px2rem(12px) !important;
align-items: center;
justify-content: center;
}
}

View File

@ -1,602 +0,0 @@
/**
* API数据源处理器
*/
import {toast} from 'amis';
import {
DSBuilder,
DSFeature,
DSFeatureType,
DSGrain,
registerDSBuilder
} from './DSBuilder';
import cloneDeep from 'lodash/cloneDeep';
import {getEnv} from 'mobx-state-tree';
import type {ButtonSchema} from 'amis';
import type {FormSchema, SchemaCollection, SchemaObject} from 'amis';
import type {DSSourceSettingFormConfig} from './DSBuilder';
import {getSchemaTpl, tipedLabel} from '../tpl';
import {EditorNodeType} from '../store/node';
class APIBuilder extends DSBuilder {
static type = 'api';
static accessable = (controlType: string, propKey: string) => {
return true;
};
name = '接口';
order = 0;
public match = (value: any, schema?: SchemaObject) => {
// https://aisuda.bce.baidu.com/amis/zh-CN/docs/types/api
if (
(typeof value === 'string' &&
/^(get|post|put|delete|option):/.test(value)) ||
(typeof value === 'object' && value.url)
) {
return true;
}
return false;
};
public features: Array<DSFeatureType> = [
'List',
'Insert',
'View',
'Edit',
'Delete',
'BulkEdit',
'BulkDelete',
'Import',
'Export',
'SimpleQuery',
'FuzzyQuery'
];
public makeSourceSettingForm(
config: DSSourceSettingFormConfig
): SchemaObject[] {
let {name, label, feat, inCrud, inScaffold} = config;
if (['Import', 'Export', 'SimpleQuery', 'FuzzyQuery'].includes(feat)) {
return [];
}
label =
label ??
(inCrud && feat !== 'List' ? DSFeature[feat].label + '接口' : '接口');
name = name ?? (inScaffold ? DSFeature[feat].value + 'Api' : 'api');
let sampleBuilder = null;
let apiDesc = null;
switch (feat) {
case 'Insert':
(label as any) = tipedLabel(
label,
`用来保存数据, 表单提交后将数据传入此接口。 <br/>
(data中有数据)<br/>
${JSON.stringify({status: 0, msg: '', data: {}}, null, '<br/>')}`
);
break;
case 'List':
(label as any) = tipedLabel(
label,
`接口响应体要求:<br/>
${JSON.stringify(
{status: 0, msg: '', items: {}, page: 0, total: 0},
null,
'<br/>'
)}`
);
break;
}
return [
getSchemaTpl('apiControl', {
label,
name,
sampleBuilder,
apiDesc
})
]
.concat(
feat === 'Edit' && !inCrud
? getSchemaTpl('apiControl', {
label: tipedLabel(
'初始化接口',
`接口响应体要求:<br/>
${JSON.stringify({status: 0, msg: '', data: {}}, null, '<br/>')}`
),
name: 'initApi'
})
: null
)
.concat(
feat === 'List' && inCrud && inScaffold
? this.makeFieldsSettingForm({
feat,
setting: true
})
: null
)
.filter(Boolean);
}
public async getContextFileds(config: {
schema: any;
sourceKey: string;
feat: DSFeatureType;
}) {
return config.schema.__fields;
}
public async getAvailableContextFileds(
config: {
schema: any;
sourceKey: string;
feat: DSFeatureType;
},
target: EditorNodeType
) {
// API类目前没有增加API中心的出参入参后可以在这里提供绑定字段
// return {
// type: 'ae-SimpleDataBindingPanel',
// fields: [
// {
// label: '可用字段',
// children: [
// {label: '名称', value: 'name'},
// {label: '年级', value: 'grade'}
// ]
// }
// ]
// } as any;
}
onFieldsInit(value: any, form: any) {
this.features.forEach(feat => {
const key = `${DSFeature[feat].value}Fields`;
const currentData = form.getValueByName(key);
const result = cloneDeep(value || []).map((field: any) => {
const exist = currentData?.find((f: any) => f.name === field.name);
return {
...field,
checked: exist ? exist.checked : true
};
});
form.setValueByName(key, result);
});
}
public makeFieldsSettingForm(config: {
sourceKey?: string;
feat: DSFeatureType;
inCrud?: boolean;
setting?: boolean;
inScaffold?: boolean;
}) {
let {sourceKey, feat, inCrud, setting, inScaffold} = config;
if (
inScaffold === false ||
['Import', 'Export', 'FuzzyQuery'].includes(feat)
) {
return [];
}
sourceKey = sourceKey ?? `${DSFeature[feat].value}Api`;
const key = setting ? '__fields' : `${DSFeature[feat].value}Fields`;
const hasInputType =
['Edit', 'Insert'].includes(feat) || (inCrud && feat === 'List');
const hasType = ['View', 'List'].includes(feat);
return ([] as any)
.concat(
inCrud && feat !== 'List'
? this.makeSourceSettingForm({
feat,
inScaffold,
inCrud
})
: null
)
.concat([
{
type: 'combo',
className: 'mb-0 ae-Fields-Setting',
joinValues: false,
name: key,
label: inCrud ? `${DSFeature[feat].label}字段` : '字段',
multiple: true,
draggable: true,
addable: false,
removable: false,
itemClassName: 'ae-Fields-Setting-Item',
// CRUD的脚手架面板基于现有字段进行选择
hidden: setting || !inCrud || ['Delete', 'BulkDelete'].includes(feat),
items: {
type: 'container',
body: [
{
name: 'checked',
label: false,
mode: 'inline',
className: 'm-0 ml-1',
type: 'checkbox'
},
{
type: 'tpl',
className: 'ae-Fields-Setting-Item-label',
tpl: '${label}'
}
]
}
},
{
type: 'input-table',
label: '字段',
className: 'mb-0',
name: key,
// 非crud都是定义字段的模式只有crud有统一定义字段因此是选择字段
visible: setting ?? !inCrud,
removable: true,
columnsTogglable: false,
needConfirm: false,
onChange: (value: any, oldValue: any, model: any, form: any) =>
this.onFieldsInit(value, form),
columns: [
{
type: 'switch',
name: 'checked',
value: true,
label: '隐藏,默认选中',
visible: false
},
{
type: 'input-text',
name: 'label',
label: '标题'
},
{
type: 'input-text',
name: 'name',
label: '绑定字段'
},
{
type: 'select',
name: 'type',
label: '类型',
visible: hasType,
value: 'tpl',
options: [
{
value: 'tpl',
label: '文本',
typeKey: 'tpl'
},
{
value: 'image',
label: '图片',
typeKey: 'src'
},
{
value: 'date',
label: '日期',
typeKey: 'value'
},
{
value: 'progress',
label: '进度',
typeKey: 'value'
},
{
value: 'status',
label: '状态',
typeKey: 'value'
},
{
value: 'mapping',
label: '映射',
typeKey: 'value'
}
],
autoFill: {
typeKey: '${typeKey}'
}
},
{
type: 'select',
name: 'inputType',
label: '输入类型',
visible: hasInputType,
value: 'input-text',
options: [
{
label: '输入框',
value: 'input-text'
},
{
label: '多行文本',
value: 'textarea'
},
{
label: '数字输入',
value: 'input-number'
},
{
label: '单选框',
value: 'radios'
},
{
label: '勾选框',
value: 'checkbox'
},
{
label: '复选框',
value: 'checkboxes'
},
{
label: '下拉框',
value: 'select'
},
{
label: '开关',
value: 'switch'
},
{
label: '日期',
value: 'input-date'
},
{
label: '表格',
value: 'input-table'
},
{
label: '文件上传',
value: 'input-file'
},
{
label: '图片上传',
value: 'input-image'
},
{
label: '富文本编辑器',
value: 'input-rich-text'
}
]
}
]
},
{
type: 'group',
visible: setting ?? !inCrud,
label: '',
body: [
{
type: 'grid',
columns: [
{
body: [
{
type: 'button',
label: '添加字段',
target: key,
className: 'ae-Button--link',
level: 'link',
icon: 'plus',
actionType: 'add'
}
]
},
{
columnClassName: 'text-right',
body: [
{
type: 'button',
label: '基于接口自动生成字段',
visible: feat === 'Edit' || feat === 'List',
className: 'ae-Button--link',
level: 'link',
// className: 'm-t-xs m-b-xs',
// 列表 或者 不在CRUD中的查看接口等
onClick: async (e: Event, props: any) => {
const data = props.data;
const schemaFilter = getEnv(
(window as any).editorStore
).schemaFilter;
const apiKey =
feat === 'Edit' && !inCrud ? 'initApi' : sourceKey;
let api: any = data[apiKey!];
// 主要是给爱速搭中替换 url
if (schemaFilter) {
api = schemaFilter({
api
}).api;
}
if (!api) {
toast.warning('请先填写接口');
}
const result = await props.env.fetcher(api, data);
let autoFillKeyValues: Array<any> = [];
let itemExample;
if (feat === 'List') {
const items = result.data?.rows || result.data?.items;
itemExample = items?.[0];
} else {
itemExample = result.data;
}
if (itemExample) {
Object.entries(itemExample).forEach(
([key, value]) => {
autoFillKeyValues.push({
label: key,
type: 'tpl',
inputType:
typeof value === 'number'
? 'input-number'
: 'input-text',
name: key
});
}
);
props.formStore.setValues({
[key]: autoFillKeyValues
});
this.onFieldsInit(autoFillKeyValues, props.formStore);
} else {
toast.warning(
'API返回格式不正确请查看接口响应格式要求'
);
}
}
}
]
}
]
}
]
}
]) as SchemaObject[];
}
public async makeFieldFilterSetting(config: {
/** 数据源字段名 */
sourceKey: string;
schema: any;
fieldName: string;
}) {
return [];
}
public resolveSourceSchema(config: {
schema: SchemaObject;
setting: any;
name?: string;
feat?: DSFeatureType;
inCrud?: boolean;
}): void {
let {name, setting, schema, feat} = config;
name = name ?? 'api';
// @ts-ignore
schema[name] = setting[feat ? `${DSFeature[feat].value}Api` : 'api'];
// form中需要初始化接口和编辑接口
if (feat === 'Edit') {
(schema as FormSchema).initApi = setting.initApi;
}
}
public resolveViewSchema(config: {
setting: any;
feat?: DSFeatureType;
}): SchemaObject[] {
let {setting, feat = 'Edit'} = config;
const fields = setting[`${DSFeature[feat].value}Fields`] || [];
return fields
.filter((i: any) => i.checked)
.map((field: any) => ({
type: field.type,
[field.typeKey || 'value']: '${' + field.name + '}'
}));
}
public resolveTableSchema(config: {schema: any; setting: any}): void {
let {schema, setting} = config;
const fields = setting.listFields.filter((i: any) => i.checked) || [];
schema.columns = this.makeTableColumnsByFields(fields);
}
public makeTableColumnsByFields(fields: any[]) {
return fields.map((field: any) => ({
type: field.type,
title: field.label,
name: field.name,
[field.typeKey || 'value']: '${' + field.name + '}'
}));
}
public resolveCreateSchema(config: {
schema: FormSchema;
setting: any;
feat: 'Insert' | 'Edit' | 'BulkEdit';
name?: string;
inCrud?: boolean;
inScaffold?: boolean;
}): void {
let {schema, setting, feat, name} = config;
const fields = setting[`${DSFeature[feat].value}Fields`] || [];
// @ts-ignore
schema[name ?? 'api'] = setting[DSFeature[feat].value + 'Api'];
schema.initApi = setting['initApi'];
schema.body = fields
.filter((i: any) => i.checked)
.map((field: any) => ({
type: field.inputType,
name: field.name,
label: field.label
}));
}
public resolveDeleteSchema(config: {
schema: ButtonSchema;
setting: any;
feat: 'BulkDelete' | 'Delete';
name?: string | undefined;
}) {
const {schema, setting, feat} = config;
schema.onEvent = Object.assign(schema.onEvent ?? {}, {
click: {
actions: []
}
});
const api = {
...(setting[`${DSFeature[feat].value}Api`] || {})
};
if (feat === 'Delete') {
api.data = {
id: '${item.id}'
};
} else {
api.data = {
ids: '${ARRAYMAP(selectedItems, item=> item.id)}'
};
}
schema.onEvent.click.actions.push({
actionType: 'ajax',
args: {api}
});
}
public resolveSimpleFilterSchema(config: {
setting: any;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'full';
}) {
const {setting, size} = config;
const fields = setting.simpleQueryFields || [];
return fields
.filter((i: any) => i.checked)
.map((field: any) => ({
type: field.inputType,
name: field.name,
label: field.label,
size
}));
}
public resolveAdvancedFilterSchema(config: {setting: any}) {
return;
}
}
registerDSBuilder(APIBuilder);

View File

@ -1,381 +0,0 @@
/**
* amis中的扩展数据源
*/
import type {ButtonSchema} from 'amis';
import type {CRUD2Schema} from 'amis';
import type {FormSchema, SchemaCollection, SchemaObject} from 'amis';
import {EditorNodeType} from '../store/node';
/**
* schema从后端来
*/
export enum DSBehavior {
create = 'create',
view = 'view',
update = 'update',
table = 'table',
filter = 'filter'
}
export interface DSField {
value: string;
label: string;
[propKey: string]: any;
}
export interface DSFieldGroup {
value: string;
label: string;
children: DSField[];
[propKey: string]: any;
}
/**
*
*/
export enum DSGrain {
entity = 'entity',
list = 'list',
piece = 'piece'
}
export const DSFeature = {
List: {
value: 'list',
label: '列表'
},
Insert: {
value: 'insert',
label: '新增'
},
View: {
value: 'view',
label: '详情'
},
Edit: {
value: 'edit',
label: '编辑'
},
Delete: {
value: 'delete',
label: '删除'
},
BulkEdit: {
value: 'bulkEdit',
label: '批量编辑'
},
BulkDelete: {
value: 'bulkDelete',
label: '批量删除'
},
Import: {
value: 'import',
label: '导入'
},
Export: {
value: 'export',
label: '导出'
},
SimpleQuery: {
value: 'simpleQuery',
label: '简单查询'
},
FuzzyQuery: {
value: 'fuzzyQuery',
label: '模糊查询'
},
AdvancedQuery: {
value: 'advancedQuery',
label: '高级查询'
}
};
export type DSFeatureType = keyof typeof DSFeature;
export interface DSSourceSettingFormConfig {
/** 数据源字段名 */
name?: string;
/** 数据源字段标题 */
label?: string;
/** 所需要配置的数据粒度 */
grain?: DSGrain;
/** 数据源所被使用的功能场景 */
feat: DSFeatureType;
/** 渲染器类型 */
renderer?: string;
/**
* @deprecated 使renderer字段代替
* CRUD场景下CRUD中可以统一设置
* */
inCrud?: boolean;
/** 是否在脚手架中 */
inScaffold?: boolean;
}
/**
*
*/
export abstract class DSBuilder {
/**
*
*/
public static type: string;
public name: string;
// 数字越小排序越靠前
public order: number;
/**
* schema运行前转换
*/
public static schemaFilter?: (schema: any) => any;
/**
* 使
*/
public static accessable: (controlType: string, propKey: string) => boolean;
public features: Array<keyof typeof DSFeature>;
/**
* schema配置状态
*/
public abstract match(value: any, schema?: SchemaObject): boolean;
/**
*
*/
public abstract makeSourceSettingForm(
config: DSSourceSettingFormConfig
): SchemaObject[];
public abstract makeFieldsSettingForm(config: {
/** 数据源字段名 */
sourceKey?: string;
feat: DSFeatureType;
inCrud?: boolean;
inScaffold?: boolean;
/** 初次设置字段还是选择字段 */
setting?: boolean;
}): SchemaObject[];
/**
*
*/
public abstract makeFieldFilterSetting(config: {
/** 数据源字段名 */
sourceKey: string;
schema: any;
fieldName: string;
}): Promise<SchemaObject[]>;
/**
* schema生成
*/
abstract resolveSourceSchema(config: {
/** schema */
schema: SchemaObject;
/** 数据源配置结果 */
setting: any;
/** 数据源字段名 */
name?: string;
feat?: DSFeatureType;
/** 是否是在CRUD场景下有的数据源在CRUD中可以统一设置 */
inCrud?: boolean;
inScaffold?: boolean;
}): void;
/**
* schema生成
*/
abstract resolveDeleteSchema(config: {
schema: ButtonSchema;
setting: any;
feat: 'BulkDelete' | 'Delete';
name?: string;
}): any;
/**
* schema
*/
abstract resolveCreateSchema(config: {
/** schema */
schema: FormSchema;
/** 脚手架配置数据 */
setting: any;
feat: 'Insert' | 'Edit' | 'BulkEdit';
/** 数据源字段名 */
name?: string;
/** 是否是在CRUD场景下有的数据源在CRUD中可以统一设置 */
inCrud?: boolean;
}): void;
/**
*
*/
abstract resolveTableSchema(config: {
/** schema */
schema: CRUD2Schema;
/** 脚手架配置数据 */
setting: any;
/** 数据源字段名 */
name?: string;
/** 是否是在CRUD场景下有的数据源在CRUD中可以统一设置 */
inCrud?: boolean;
}): void;
/**
*
*/
abstract resolveViewSchema(config: {
/** 脚手架配置数据 */
setting: any;
feat?: DSFeatureType;
}): SchemaObject[];
abstract resolveSimpleFilterSchema(config: {
setting: any;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'full';
}): SchemaObject[];
abstract resolveAdvancedFilterSchema(config: {
setting: any;
}): SchemaObject | void;
abstract makeTableColumnsByFields(fields: any[]): SchemaObject[];
/**
* 使
*/
abstract getContextFileds(config: {
schema: any;
sourceKey: string;
feat: DSFeatureType;
}): Promise<DSField[] | void>;
/**
* 使
*/
abstract getAvailableContextFileds(
config: {
schema: any;
sourceKey: string;
feat: DSFeatureType;
scopeNode?: EditorNodeType;
},
target: EditorNodeType
): Promise<SchemaCollection | void>;
}
/**
*
*/
const __builders: {
[key: string]: any;
} = {};
export const registerDSBuilder = (builderKClass: any) => {
__builders[builderKClass.type] = builderKClass;
};
/**
* 便
*/
export class DSBuilderManager {
/** 所有可用的数据源构造器实例 */
builders: {
[key: string]: DSBuilder;
} = {};
get builderNum() {
return Object.keys(this.builders).length;
}
constructor(type: string, propKey: string) {
Object.values(__builders)
.filter(builder => builder.accessable?.(type, propKey) ?? true)
.forEach(Builder => {
this.builders[Builder.type] = new Builder();
});
}
resolveBuilderBySetting(setting: any) {
return this.builders[setting.dsType] || Object.values(this.builders)[0];
}
resolveBuilderBySchema(schema: any, propKey: string) {
const builders = Object.values(this.builders);
return (
builders.find(builder => builder.match(schema[propKey])) || builders[0]
);
}
getDefaultBuilderName() {
// 先返回第一个之后可以加一些order之类的
const builderOptions = Object.entries(this.builders)
.map(([key, builder]) => {
return {
value: key,
order: builder.order
};
})
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return builderOptions[0].value;
}
getDSSwitch(setting: any = {}) {
const multiSource = this.builderNum > 1;
const builderOptions = Object.entries(this.builders).map(
([key, builder]) => ({
label: builder.name,
value: key,
order: builder.order
})
);
builderOptions.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return {
type: 'radios',
label: '数据来源',
name: 'dsType',
visible: multiSource,
selectFirst: true,
options: builderOptions,
...setting
};
}
// getDSSwitchFormForPanel(
// propKey: string,
// label: string
// ) {
// return Object.keys(this.builders).length > 1 ? {
// type: Object.keys(this.builders).length > 3 ? 'select' : 'button-group-select',
// options: Object.keys(this.builders).map(name => ({
// label: name,
// value: name
// })),
// name: propKey,
// label: label,
// pipeIn: (value: string) => {
// const builders = Object.entries(this.builders);
// return (builders.find(([, builder]) => {
// return builder.match(value);
// }) || builders[0])[0];
// },
// pipeOut: (value: string) => {
// return this.builders[value].defaultSchema || {};
// }
// } : null;
// }
collectFromBuilders(
callee: (builder: DSBuilder, builderName: string) => any
) {
return Object.entries(this.builders).map(([name, builder]) => {
return callee(builder, name);
});
}
}

View File

@ -39,6 +39,8 @@ export interface EditorProps extends PluginEventListener {
amisDocHost?: string;
superEditorData?: any;
withSuperDataSchema?: boolean;
/** 当前 Editor 为 SubEditor 时触发的宿主节点 */
hostNode?: EditorNodeType;
dataBindingChange?: (
value: string,
data: any,
@ -49,7 +51,7 @@ export interface EditorProps extends PluginEventListener {
* Preview
* api地址替换成 proxy
*/
schemaFilter?: (schema: any, preview?: boolean) => any;
schemaFilter?: (schema: any, isPreview?: boolean) => any;
amisEnv?: RenderOptions;
/**
@ -126,6 +128,8 @@ export interface EditorProps extends PluginEventListener {
) => Promise<void | boolean>;
getHostNodeDataSchema?: () => Promise<any>;
getAvaiableContextFields?: (node: EditorNodeType) => Promise<any>;
}
export default class Editor extends Component<EditorProps> {

View File

@ -1,11 +1,12 @@
import {RendererProps} from 'amis-core';
import {RendererProps, isObject} from 'amis-core';
import {observer} from 'mobx-react';
import {isAlive} from 'mobx-state-tree';
import React from 'react';
import {findDOMNode} from 'react-dom';
import merge from 'lodash/merge';
import {RendererInfo} from '../plugin';
import {EditorNodeType} from '../store/node';
import {autobind} from '../util';
import {autobind, isEmpty} from '../util';
export interface NodeWrapperProps extends RendererProps {
$$editor: RendererInfo; // 当前节点信息info
@ -71,6 +72,14 @@ export class NodeWrapper extends React.Component<NodeWrapperProps> {
rest = $$editor.filterProps.call($$editor.plugin, rest, $$node);
}
// 自动合并假数据
if (
isObject(rest.editorSetting?.mock) &&
!isEmpty(rest.editorSetting.mock)
) {
rest = merge({}, rest, rest.editorSetting.mock);
}
if ($$editor.renderRenderer) {
return $$editor.renderRenderer.call(
$$editor.plugin,

View File

@ -10,7 +10,7 @@ import {EditorNodeContext, EditorNodeType} from '../store/node';
export interface RegionWrapperProps {
name: string;
label: string;
placeholder?: string;
placeholder?: string | JSX.Element;
preferTag?: string;
wrapperResolve?: (dom: HTMLElement) => HTMLElement;
editorStore: EditorStoreType;

View File

@ -45,7 +45,7 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
body = [
{
type: 'steps',
name: '__steps',
name: '__step',
className: 'ae-Steps',
steps: body.map((step, index) => ({
title: step.title,
@ -133,7 +133,6 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
@autobind
goToNextStep() {
// 不能更新props的data控制amis不重新渲染否则数据会重新初始化
const store = this.props.store;
const form = this.amisScope?.getComponents()[0].props.store;
const step = store.scaffoldFormStep + 1;
@ -178,13 +177,14 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
true
);
this.handleConfirm([values]);
await this.handleConfirm([values]);
} catch (e) {
console.log(e.stack);
store.setScaffoldError(e.message);
} finally {
store.setScaffoldBuzy(false);
}
store.setScaffoldBuzy(false);
store.setScaffoldStep(0);
}
@autobind
@ -197,8 +197,8 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
const {store, theme, manager} = this.props;
const scaffoldFormContext = store.scaffoldForm;
const cx = getTheme(theme || 'cxd').classnames;
const isStepBody = !!scaffoldFormContext?.stepsBody;
const canSkip = !!scaffoldFormContext?.canSkip;
const isLastStep =
isStepBody &&
store.scaffoldFormStep === scaffoldFormContext!.body.length - 1;
@ -210,7 +210,7 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
size={scaffoldFormContext?.size || 'md'}
contentClassName={scaffoldFormContext?.className}
show={!!scaffoldFormContext}
onHide={store.closeScaffoldForm}
onHide={this.handleCancelClick}
className="ae-scaffoldForm-Modal"
closeOnEsc={!store.scaffoldFormBuzy}
>
@ -218,7 +218,7 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
{!store.scaffoldFormBuzy ? (
<a
data-position="left"
onClick={store.closeScaffoldForm}
onClick={this.handleCancelClick}
className={cx('Modal-close')}
>
<Icon icon="close" className="icon" />
@ -233,7 +233,8 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
{
data: store.scaffoldData,
onValidate: scaffoldFormContext.validate,
scopeRef: this.scopeRef
scopeRef: this.scopeRef,
manager
},
{
...manager.env,
@ -257,13 +258,29 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
) : null}
</div>
) : null}
{isStepBody && canSkip && isFirstStep && (
<Button
onClick={this.handleConfirmClick}
disabled={store.scaffoldFormBuzy}
>
</Button>
)}
{isStepBody && !isFirstStep && (
<Button level="primary" onClick={this.goToPrevStep}>
<Button
level="primary"
onClick={this.goToPrevStep}
disabled={store.scaffoldFormBuzy}
>
</Button>
)}
{isStepBody && !isLastStep && (
<Button level="primary" onClick={this.goToNextStep}>
<Button
level="primary"
onClick={this.goToNextStep}
disabled={store.scaffoldFormBuzy}
>
</Button>
)}

View File

@ -146,6 +146,7 @@ export class SubEditor extends React.Component<SubEditorProps> {
ref={store.subEditorRef}
onChange={onChange}
data={store.subEditorContext?.data}
hostNode={store.subEditorContext?.hostNode}
superEditorData={superEditorData}
schemaFilter={manager.config.schemaFilter}
theme={manager.env.theme}
@ -184,6 +185,9 @@ export class SubEditor extends React.Component<SubEditorProps> {
getHostNodeDataSchema={() =>
manager.getContextSchemas(manager.store.activeId)
}
getAvaiableContextFields={node =>
manager.getAvailableContextFields(node)
}
/>
)
}

View File

@ -73,7 +73,7 @@ export function makeWrapper(
// 查找父数据域,将当前组件数据域追加上去,使其形成父子关系
if (
rendererConfig.storeType &&
(rendererConfig.storeType || info.isListComponent) &&
!manager.dataSchema.hasScope(`${info.id}-${info.type}`)
) {
let from = parent;

View File

@ -22,8 +22,6 @@ export * from './manager';
export * from './plugin';
export * from './icons/index';
export * from './mocker';
export * from './builder/DSBuilder';
import './builder/ApiBuilder';
import {BasicEditor, RendererEditor} from './compat';
import MiniEditor from './component/MiniEditor';
import CodeEditor from './component/Panel/AMisCodeEditor';

View File

@ -3,12 +3,13 @@
* UI 西
*/
import {reaction} from 'mobx';
import {isAlive} from 'mobx-state-tree';
import {parse, stringify} from 'json-ast-comments';
import debounce from 'lodash/debounce';
import findIndex from 'lodash/findIndex';
import omit from 'lodash/omit';
import {openContextMenus, toast, alert, DataScope, DataSchema} from 'amis';
import {getRenderers, RenderOptions, mapTree} from 'amis-core';
import {getRenderers, RenderOptions, mapTree, isEmpty} from 'amis-core';
import {
PluginInterface,
BasicPanelItem,
@ -229,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 => {
@ -986,6 +1018,29 @@ export class EditorManager {
}
}
/**
*
*/
canAppendSiblings() {
const store = this.store;
const id = store.activeId;
const node = store.getNodeById(id)!; // 当前选中节点
if (!node) {
return false;
}
const regionNode = node.parent as EditorNodeType; // 父级节点
if (
regionNode &&
!regionNode.region &&
!regionNode.schema.body &&
regionNode.schema?.type !== 'flex'
) {
return false;
}
return true;
}
/**
* schema
* &
@ -1701,7 +1756,7 @@ export class EditorManager {
patchList(node.uniqueChildren);
}
if (!node.isRegion) {
if (isAlive(node) && !node.isRegion) {
node.patch(this.store, force);
}
});
@ -1845,7 +1900,6 @@ export class EditorManager {
return;
}
const plugin = node.info.plugin!;
const store = this.store;
const context: PopOverFormContext = {
node,
@ -1918,42 +1972,74 @@ export class EditorManager {
}
let nearestScope;
let listScope = [];
// 更新组件树中的所有上下文数据声明为最新数据
while (scope) {
const [id, type] = scope.id.split('-');
const node = this.store.getNodeById(id, type);
const [nodeId, type] = scope.id.split('-');
const scopeNode = this.store.getNodeById(nodeId, type);
// 拿非重复组件id的父组件作为主数据域展示如CRUD不展示表格只展示增删改查信息避免变量面板出现两份数据
if (!nearestScope && node && !node.isSecondFactor) {
if (!nearestScope && scopeNode && !scopeNode.isSecondFactor) {
nearestScope = scope;
}
const jsonschema = await node?.info?.plugin?.buildDataSchemas?.(
node,
region,
trigger,
node
);
const jsonschema = await scopeNode?.info?.plugin?.buildDataSchemas?.(
scopeNode,
region,
trigger
);
if (jsonschema) {
scope.removeSchema(jsonschema.$id);
scope.addSchema(jsonschema);
}
// 记录each列表等组件顺序
if (scopeNode?.info?.isListComponent) {
listScope.unshift(scope);
// 如果当前节点是list类型节点当前scope从父节点上取
if (nodeId === id) {
nearestScope = scope.parent;
}
}
scope = withoutSuper ? undefined : scope.parent;
}
// each列表类型嵌套时需要从上到下获取数据重新执行一遍
if (listScope.length > 1) {
for (let scope of listScope) {
const [id, type] = scope.id.split('-');
const node = this.store.getNodeById(id, type);
const jsonschema = await node?.info?.plugin?.buildDataSchemas?.(
node,
region,
trigger
);
if (jsonschema) {
scope.removeSchema(jsonschema.$id);
scope.addSchema(jsonschema);
}
}
}
// 存在当前行时找到最底层todo暂不考虑table套service+table的场景
const nearestScopeId = Object.keys(this.dataSchema.idMap).find(
key =>
/\-currentRow$/.test(key) &&
!this.dataSchema.idMap[key].children?.length
);
const nearestScopeId =
Object.keys(this.dataSchema.idMap).find(
key =>
/\-currentRow$/.test(key) &&
!this.dataSchema.idMap[key].children?.length
) || nearestScope?.id;
if (nearestScopeId) {
this.dataSchema.switchTo(nearestScopeId);
} else if (nearestScope?.id) {
this.dataSchema.switchTo(nearestScope.id);
}
// 如果当前容器是list非数据组件scope从父scope开始
if (node.info.isListComponent) {
let lastScope = listScope[listScope.length - 1];
this.dataSchema.switchTo(lastScope.parent!);
}
return withoutSuper
@ -1964,7 +2050,7 @@ export class EditorManager {
/**
*
*/
async getAvailableContextFields(node: EditorNodeType) {
async getAvailableContextFields(node: EditorNodeType): Promise<any> {
if (!node) {
return;
}
@ -1993,6 +2079,10 @@ export class EditorManager {
}
if (!scope) {
/** 如果在子编辑器中,继续去上层编辑器查找,不过这里可能受限于当前层的数据映射 */
if (!from && this.store.isSubEditor) {
return this.config?.getAvaiableContextFields?.(node);
}
return from?.info.plugin.getAvailableContextFields?.(from, node);
}
@ -2000,7 +2090,7 @@ export class EditorManager {
const [id, type] = scope.id.split('-');
const scopeNode = this.store.getNodeById(id, type);
if (scopeNode) {
if (scopeNode && !scopeNode.info?.isListComponent) {
return scopeNode?.info.plugin.getAvailableContextFields?.(
scopeNode,
node

View File

@ -26,6 +26,8 @@ export function mockValue(schema: any) {
return placeholderImage;
} else if (schema.type === 'images' || schema.type === 'static-images') {
return [placeholderImage];
} else if (schema.type === 'number' || schema.type === 'input-number') {
return (Math.random() * 1000).toFixed(schema.precision ?? 0);
}
return '假数据';

View File

@ -36,7 +36,7 @@ export interface RegionConfig {
/**
*
*/
placeholder?: string;
placeholder?: string | JSX.Element;
/**
*
@ -200,6 +200,12 @@ export interface RendererInfo extends RendererScaffoldInfo {
isBaseComponent?: boolean;
/**
*
* listeachcards
*/
isListComponent?: boolean;
disabledRendererPlugin?: boolean;
/**
@ -342,7 +348,8 @@ export interface ScaffoldForm extends PopOverForm {
* value
*/
validate?: (
values: any
values: any,
formStore: any
) =>
| void
| {[propName: string]: string}
@ -820,6 +827,9 @@ export interface PluginInterface
region?: EditorNodeType
) => Promise<SchemaCollection | void>;
/** 配置面板表单的 pipeOut function */
panelFormPipeOut?: (value: any) => any;
/**
* @deprecated panelBodyCreator
*/
@ -1035,6 +1045,7 @@ export abstract class BasePlugin implements PluginInterface {
scaffoldForm: plugin.scaffoldForm,
disabledRendererPlugin: plugin.disabledRendererPlugin,
isBaseComponent: plugin.isBaseComponent,
isListComponent: plugin.isListComponent,
rendererName: plugin.rendererName
};
}
@ -1082,6 +1093,18 @@ export abstract class BasePlugin implements PluginInterface {
plugin
});
const baseProps = {
definitions: plugin.panelDefinitions,
submitOnChange: plugin.panelSubmitOnChange,
api: plugin.panelApi,
controls: plugin.panelControlsCreator
? plugin.panelControlsCreator(context)
: plugin.panelControls!,
justify: plugin.panelJustify,
panelById: store.activeId,
pipeOut: plugin.panelFormPipeOut?.bind?.(plugin)
};
panels.push({
key: 'config',
icon: plugin.panelIcon || plugin.icon || 'fa fa-cog',
@ -1092,27 +1115,13 @@ export abstract class BasePlugin implements PluginInterface {
const panelBody = await (body as Promise<SchemaCollection>);
return this.manager.makeSchemaFormRender({
definitions: plugin.panelDefinitions,
submitOnChange: plugin.panelSubmitOnChange,
api: plugin.panelApi,
body: panelBody,
controls: plugin.panelControlsCreator
? plugin.panelControlsCreator(context)
: plugin.panelControls!,
justify: plugin.panelJustify,
panelById: store.activeId
...baseProps,
body: panelBody
});
}, omit(plugin.async, 'enable'))
: this.manager.makeSchemaFormRender({
definitions: plugin.panelDefinitions,
submitOnChange: plugin.panelSubmitOnChange,
api: plugin.panelApi,
body: body as SchemaCollection,
controls: plugin.panelControlsCreator
? plugin.panelControlsCreator(context)
: plugin.panelControls!,
justify: plugin.panelJustify,
panelById: store.activeId
...baseProps,
body: body as SchemaCollection
})
});
} else if (

View File

@ -233,7 +233,7 @@ export const MainStore = types
// 给预览状态时的
get filteredSchemaForPreview() {
const schema = JSONPipeOut(self.schema);
return getEnv(self).schemaFilter?.(schema) ?? schema;
return getEnv(self).schemaFilter?.(schema, true) ?? schema;
},
// 判断当前元素是否是根节点

View File

@ -544,6 +544,7 @@ export const EditorNode = types
if (node.id === 'root') {
return;
}
node = node.parent;
}
}
@ -564,6 +565,10 @@ export const EditorNode = types
break;
}
if (cursor.id === 'root') {
return cursor;
}
cursor = cursor.parent;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

@ -1,6 +1,6 @@
{
"name": "amis-editor",
"version": "5.5.0",
"version": "5.5.2-alpha.0",
"description": "amis 可视化编辑器",
"main": "lib/index.js",
"module": "esm/index.js",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,286 @@
/**
* @file DSBuilder.ts
* @desc
*/
import {EditorManager} from 'amis-editor-core';
import {getFeatValueByKey, getFeatLabelByKey} from './utils';
import type {EditorNodeType} from 'amis-editor-core';
import type {
DSFeatureType,
GenericSchema,
CRUDScaffoldConfig,
FormScaffoldConfig
} from './type';
export interface DSBuilderBaseOptions {
/** 渲染器类型 */
renderer: string;
/** Form应用场景 */
feat?: DSFeatureType;
/** CRUD应用场景 */
feats?: DSFeatureType[];
/** 当前组件的 Schema */
schema?: GenericSchema;
/** 数据源字段名 */
sourceKey?: string;
/** 是否在脚手架环境中 */
inScaffold?: boolean;
/** 如果为列表类容器,则会返回对应的节点 */
scopeNode?: EditorNodeType;
/** 数据源控件配置项 */
sourceSettings?: Record<string, any>;
/** 字段控件配置项 */
fieldSettings?: Record<string, any>;
[propName: string]: any;
}
export interface DSBuilderInterface<
T extends DSBuilderBaseOptions = DSBuilderBaseOptions
> {
/** 数据源中文名称,主要用于前端展示 */
readonly name: string;
/** 构造器排序权重,数字越小排序越靠前,支持负数 */
readonly order: number;
/** 数据源支持的功能场景 */
readonly features: DSFeatureType[];
/** 是否为默认 */
isDefault?: boolean;
/** 实例获取数据源的key */
key: string;
/** 是否禁用 */
disabledOn?: () => boolean;
/** 获取功能场景的value */
getFeatValueByKey(feat: DSFeatureType): string;
/** 获取功能场景的label */
getFeatLabelByKey(feat: DSFeatureType): string;
/** 按照功能场景过滤 */
filterByFeat(feat: any): boolean;
/** 根据schema判断是否匹配当前数据源 */
match(schema?: any): boolean;
/** 当前上下文中使用的字段 */
getContextFields(options: T): Promise<any>;
/** 当前上下文可以使用的字段 */
getAvailableContextFields(
options: Omit<T, 'renderer'>,
target: EditorNodeType
): Promise<any>;
/** 获取CRUD列表字段 */
getCRUDListFields?: <F extends Record<string, any>>(
options: T
) => Promise<F[]>;
/** 获取CRUD简单查询字段 */
getCRUDSimpleQueryFields?: <F extends Record<string, any>>(
options: T
) => Promise<F[]>;
/** 构建简单查询表单项 */
buildSimpleQueryCollectionSchema?: (
options: T
) => Promise<GenericSchema[] | undefined>;
/** 获取CRUD高级查询字段 */
getCRUDAdvancedQueryFields?: <F extends Record<string, any>>(
options: T
) => Promise<F[]>;
/** 构建高级查询 */
buildAdvancedQuerySchema?: (options: T) => Promise<GenericSchema | undefined>;
/** 获取CRUD模糊查询字段 */
getCRUDFuzzyQueryFields?: <F extends Record<string, any>>(
options: T
) => Promise<F[]>;
/** 构建模糊查询 */
buildFuzzyQuerySchema?: (options: T) => Promise<GenericSchema | undefined>;
/** 构造数据源的可视化配置表单 */
makeSourceSettingForm(options: T): any[];
/** 构造数据源字段的可视化配置表单 */
makeFieldsSettingForm(options: T): any[];
/** 新建数据 */
buildInsertSchema(options: T, componentId?: string): Promise<any>;
/** 编辑数据 */
buildEditSchema(options: T, componentId?: string): Promise<any>;
/** 批量编辑数据 */
buildBulkEditSchema(options: T, componentId?: string): Promise<any>;
/** 查看详情数据 */
buildViewSchema(options: T, componentId?: string): Promise<any>;
/** 删除数据 */
buildCRUDDeleteSchema(options: T, componentId?: string): Promise<any>;
/** 批量删除数据 */
buildCRUDBulkDeleteSchema(options: T, componentId?: string): Promise<any>;
/** 构建 CRUD 的顶部工具栏 */
buildCRUDHeaderToolbar?: (
options: T,
componentId?: string
) => Promise<GenericSchema>;
/** 表格的表头查询 */
buildCRUDFilterSchema(options: T, componentId?: string): Promise<any>;
/** 表格单列 */
buildCRUDColumn?: (
field: Record<string, any>,
options: T,
componentId?: string
) => Promise<any>;
/** 表格操作列 */
buildCRUDOpColumn?: (options: T, componentId?: string) => Promise<any>;
/** 表格列 */
buildCRUDColumnsSchema(options: T, componentId?: string): Promise<any>;
/** 表格构建 */
buildCRUDSchema(options: T): Promise<any>;
/** 表单构建 */
buildFormSchema(options: T): Promise<any>;
/** 基于 schema 还原CRUD脚手架配置 */
guessCRUDScaffoldConfig<T extends CRUDScaffoldConfig<any, any>>(options: {
schema: GenericSchema;
[propName: string]: any;
}): Promise<T> | T;
/** 基于 schema 还原Form脚手架配置 */
guessFormScaffoldConfig<T extends FormScaffoldConfig<any, any>>(options: {
schema: GenericSchema;
[propName: string]: any;
}): Promise<T> | T;
/** 重新构建 API 配置 */
buildApiSchema(options: T): Promise<any>;
}
export abstract class DSBuilder<T extends DSBuilderBaseOptions>
implements DSBuilderInterface<T>
{
static key: string;
readonly name: string;
readonly order: number;
/** 是否为默认 */
readonly isDefault?: boolean;
features: DSFeatureType[];
constructor(readonly manager: EditorManager) {}
/** 实例获取数据源的key */
get key() {
return (this.constructor as typeof DSBuilder<T>).key;
}
/** 获取功能场景的value */
getFeatValueByKey(feat: DSFeatureType) {
return getFeatValueByKey(feat);
}
/** 获取功能场景的label */
getFeatLabelByKey(feat: DSFeatureType) {
return getFeatLabelByKey(feat);
}
filterByFeat(feat: any) {
return feat && this.features.includes(feat);
}
abstract match(schema?: any): boolean;
abstract getContextFields(options: T): Promise<any>;
abstract getAvailableContextFields(
options: Omit<T, 'renderer'>,
target: EditorNodeType
): Promise<any>;
abstract makeSourceSettingForm(options: T): any[];
abstract makeFieldsSettingForm(options: T): any[];
/** 新建数据 */
abstract buildInsertSchema(options: T): Promise<any>;
/** 查看详情数据 */
abstract buildViewSchema(options: T): Promise<any>;
/** 编辑数据 */
abstract buildEditSchema(options: T): Promise<any>;
/** 批量编辑数据 */
abstract buildBulkEditSchema(options: T): Promise<any>;
/** 删除数据 */
abstract buildCRUDDeleteSchema(options: T): Promise<any>;
/** 批量删除数据 */
abstract buildCRUDBulkDeleteSchema(options: T): Promise<any>;
/** 表格的表头查询 */
abstract buildCRUDFilterSchema(options: T): Promise<any>;
/** 表格列 */
abstract buildCRUDColumnsSchema(options: T): Promise<any>;
/** 表格 */
abstract buildCRUDSchema(options: T): Promise<any>;
/** 表单 */
abstract buildFormSchema(options: T): Promise<any>;
/** 基于 schema 还原CRUD脚手架配置 */
abstract guessCRUDScaffoldConfig<
T extends CRUDScaffoldConfig<any, any>
>(options: {schema: GenericSchema; [propName: string]: any}): Promise<T> | T;
/** 基于 schema 还原Form脚手架配置 */
abstract guessFormScaffoldConfig<
T extends FormScaffoldConfig<any, any>
>(options: {schema: GenericSchema; [propName: string]: any}): Promise<T> | T;
abstract buildApiSchema(options: T): Promise<any>;
}
export interface DSBuilderClass {
new (manager: EditorManager): DSBuilderInterface;
/** 数据源类型,使用英文,可以覆盖同名 */
key: string;
}
export const builderFactory = new Map<string, DSBuilderClass>();
/** 注册数据源构造器 */
export const registerDSBuilder = (klass: DSBuilderClass) => {
if (builderFactory.has(klass.key)) {
console.warn(
`[amis-editor][DSBuilder] duplicate DSBuilder「${klass.key}`
);
}
/** 重名覆盖 */
builderFactory.set(klass.key, klass);
};

View File

@ -0,0 +1,115 @@
/**
* @file DSBuilderManager
* @desc
*/
import {builderFactory, DSBuilderInterface} from './DSBuilder';
import {EditorManager} from 'amis-editor-core';
export class DSBuilderManager {
private builders: Map<string, DSBuilderInterface>;
constructor(manager: EditorManager) {
this.builders = new Map();
builderFactory.forEach((Builder, key) => {
this.builders.set(key, new Builder(manager));
});
}
get size() {
return this.builders.size;
}
getBuilderByKey(key: string) {
return this.builders.get(key);
}
getBuilderByScaffoldSetting(scaffoldConfig: any) {
return this.builders.get(scaffoldConfig.dsType);
}
getBuilderBySchema(schema: any) {
let builder: DSBuilderInterface | undefined;
for (let [key, value] of Array.from(this.builders.entries())) {
if (value.match(schema)) {
builder = value;
break;
}
}
return builder ? builder : this.getDefaultBuilder();
}
getDefaultBuilderKey() {
const collections = Array.from(this.builders.entries()).filter(
([_, builder]) => builder?.disabledOn?.() !== true
);
const [defaultKey, _] =
collections.find(([_, builder]) => builder.isDefault === true) ??
collections.sort((lhs, rhs) => {
return (lhs[1].order ?? 0) - (rhs[1].order ?? 0);
})?.[0] ??
[];
return defaultKey;
}
getDefaultBuilder() {
const collections = Array.from(this.builders.entries()).filter(
([_, builder]) => builder?.disabledOn?.() !== true
);
const [_, defaultBuilder] =
collections.find(([_, builder]) => builder.isDefault === true) ??
collections.sort((lhs, rhs) => {
return (lhs[1].order ?? 0) - (rhs[1].order ?? 0);
})?.[0] ??
[];
return defaultBuilder;
}
getAvailableBuilders() {
return Array.from(this.builders.entries())
.filter(([_, builder]) => builder?.disabledOn?.() !== true)
.sort((lhs, rhs) => {
return (lhs[1].order ?? 0) - (rhs[1].order ?? 0);
});
}
getDSSelectorSchema(patch: Record<string, any>) {
const builders = this.getAvailableBuilders();
const options = builders.map(([key, builder]) => ({
label: builder.name,
value: key
}));
return {
type: 'radios',
label: '数据来源',
name: 'dsType',
visible: options.length > 0,
selectFirst: true,
options: options,
...patch
};
}
buildCollectionFromBuilders(
callback: (
builder: DSBuilderInterface,
builderKey: string,
index: number
) => any
) {
const builders = this.getAvailableBuilders();
const collection = builders
.map(([key, builder], index) => {
return callback(builder, key, index);
})
.filter(Boolean);
return collection;
}
}

View File

@ -0,0 +1,130 @@
/**
* @file constants.ts
* @desc builder
*/
import {FormOperatorValue, FormOperator} from './type';
/**
* schema从后端来
*/
export enum DSBehavior {
/** 创建操作 */
create = 'create',
/** 查询操作 */
view = 'view',
/** 更新操作 */
update = 'update',
table = 'table',
filter = 'filter'
}
/** 数据粒度 */
export enum DSGrain {
/** 实体 */
entity = 'entity',
/** 多条数据 */
list = 'list',
/** 单条数据 */
piece = 'piece'
}
/** 数据源所使用的功能场景 */
export const DSFeature = {
List: {
value: 'list',
label: '列表'
},
Insert: {
value: 'insert',
label: '新增'
},
View: {
value: 'view',
label: '详情'
},
Edit: {
value: 'edit',
label: '编辑'
},
Delete: {
value: 'delete',
label: '删除'
},
BulkEdit: {
value: 'bulkEdit',
label: '批量编辑'
},
BulkDelete: {
value: 'bulkDelete',
label: '批量删除'
},
Import: {
value: 'import',
label: '导入'
},
Export: {
value: 'export',
label: '导出'
},
SimpleQuery: {
value: 'simpleQuery',
label: '简单查询'
},
FuzzyQuery: {
value: 'fuzzyQuery',
label: '模糊查询'
},
AdvancedQuery: {
value: 'advancedQuery',
label: '高级查询'
}
};
export enum DSFeatureEnum {
List = 'List',
Insert = 'Insert',
View = 'View',
Edit = 'Edit',
Delete = 'Delete',
BulkEdit = 'BulkEdit',
BulkDelete = 'BulkDelete',
Import = 'Import',
Export = 'Export',
SimpleQuery = 'SimpleQuery',
FuzzyQuery = 'FuzzyQuery',
AdvancedQuery = 'AdvancedQuery'
}
export const DSFeatureList = Object.keys(
DSFeature
) as (keyof typeof DSFeature)[];
export const FormOperatorMap: Record<FormOperatorValue, FormOperator> = {
cancel: {
label: '取消',
value: 'cancel',
order: 0,
schema: {
level: 'default'
}
},
reset: {
label: '重置',
value: 'reset',
order: 1,
schema: {
level: 'default'
}
},
submit: {
label: '提交',
value: 'submit',
order: 2,
schema: {
level: 'primary'
}
}
};
export const ModelDSBuilderKey = 'model-entity';

View File

@ -0,0 +1,7 @@
export * from './type';
export * from './constants';
export * from './utils';
export * from './DSBuilder';
export * from './DSBuilderManager';
import './ApiDSBuilder';

View File

@ -0,0 +1,122 @@
/**
* @file type.ts
* @desc builder
*/
import {DSFeature} from './constants';
import type {BaseApiObject} from 'amis-core';
export interface DSField {
value: string;
label: string;
[propKey: string]: any;
}
/** 数据源字段集合 */
export interface DSFieldGroup {
value: string;
label: string;
children: DSField[];
[propKey: string]: any;
}
export type DSFeatureType = keyof typeof DSFeature;
export type GenericSchema = Record<string, any>;
export type DSRendererType = 'form' | 'crud' | 'service';
export interface ScaffoldField {
/** 标题 */
label: string;
/** 字段名 */
name: string;
/** 展示控件类型 */
displayType: string;
/** 输入控件类型 */
inputType: string;
typeKey?: string;
/** 是否启用 */
checked?: boolean;
}
/** 表单操作 */
export type ApiConfig = string | BaseApiObject;
/** 表单操作 */
export type FormOperatorValue = 'cancel' | 'reset' | 'submit';
/** 表单操作按钮 */
export interface FormOperator {
label: string;
value: FormOperatorValue;
order: number;
schema: Record<string, any>;
}
export interface ScaffoldConfigBase {
/** 数据源类型 */
dsType: string;
/** 重新构建时用户的原始 Schema */
__pristineSchema?: Record<string, any>;
[propName: string]: any;
}
export interface FormScaffoldConfig<
Fields extends Record<string, any> = ScaffoldField,
API extends any = ApiConfig
> extends ScaffoldConfigBase {
/** Form功能场景 */
feat?: DSFeatureType;
/** 表单初始化接口 */
initApi?: API;
insertApi?: API;
editApi?: API;
bulkEditApi?: API;
insertFields?: Fields[];
editFields?: Fields[];
bulkEditFields?: Fields[];
operators?: FormOperator[];
}
export interface CRUDScaffoldConfig<
Fields extends Record<string, any> = ScaffoldField,
API extends any = ApiConfig
> extends ScaffoldConfigBase {
/** 工具栏 */
tools?: Extract<DSFeatureType, 'Insert' | 'BulkDelete' | 'BulkEdit'>[];
/** 数据操作 */
operators?: Extract<DSFeatureType, 'View' | 'Edit' | 'Delete'>[];
/** 条件查询 */
filters?: Extract<
DSFeatureType,
'FuzzyQuery' | 'SimpleQuery' | 'AdvancedQuery'
>[];
/** 表格 list 接口 */
listApi?: API;
viewApi?: API;
editApi?: API;
/** 编辑表单的初始化接口 */
initApi?: API;
bulkEditApi?: API;
deleteApi?: API;
bulkDeleteApi?: API;
insertApi?: API;
listFields?: Fields[];
insertFields?: Fields[];
viewFields?: Fields[];
editFields?: Fields[];
bulkEditFields?: Fields[];
fuzzyQueryFields?: Fields[];
simpleQueryFields?: Fields[];
advancedQueryFields?: Fields[];
importFields?: Fields[];
exportFields?: Fields[];
/** 表格脚手架时的主键 */
primaryField?: string;
}
export type ScaffoldConfig<
Fields extends Record<string, any> = ScaffoldField,
API extends any = ApiConfig
> = FormScaffoldConfig<Fields, API> | CRUDScaffoldConfig<Fields, API>;

View File

@ -0,0 +1,95 @@
/**
* @file utils
* @desc builder用到的 utils
*/
import isObjectLike from 'lodash/isObjectLike';
import {DSFeature} from './constants';
import type {DSFeatureType} from './type';
export const getFeatValueByKey = (feat: DSFeatureType) => {
return `${DSFeature?.[feat]?.value}`;
};
export const getFeatLabelByKey = (feat: DSFeatureType) => {
return `${DSFeature?.[feat]?.label}`;
};
const _traverseSchemaDeep = (
schema: Record<string, any>,
mapper: (originKey: string, originValue: any, origin: any) => any[],
cache = new WeakMap()
) => {
const target: Record<string, any> = {};
if (cache.has(schema)) {
return cache.get(schema);
}
cache.set(schema, target);
const mapArray = (arr: any[]): any =>
arr.map((item: any) => {
return isObjectLike(item)
? _traverseSchemaDeep(item, mapper, cache)
: item;
});
if (Array.isArray(schema)) {
return mapArray(schema);
}
for (const [key, value] of Object.entries(schema)) {
const result = mapper(key, value, schema);
let [updatedKey, updatedValue] = result;
if (updatedKey === '__proto__') {
continue;
}
if (isObjectLike(updatedValue)) {
updatedValue = Array.isArray(updatedValue)
? mapArray(updatedValue)
: _traverseSchemaDeep(updatedValue, mapper, cache);
}
target[updatedKey] = updatedValue;
}
return target;
};
export const traverseSchemaDeep = (
schema: Record<string, any>,
mapper: (originKey: string, originValue: any, origin: any) => any[]
) => {
if (!isObjectLike(schema)) {
return schema;
}
if (Array.isArray(schema)) {
return schema;
}
return _traverseSchemaDeep(schema, mapper);
};
/** CRUD列类型转 Form 表单类型 */
export const displayType2inputType = (inputType: string): string => {
if (!inputType || typeof inputType !== 'string') {
return inputType;
}
const map: Record<string, string> = {
tpl: 'input-text',
image: 'input-image',
date: 'input-date',
progress: 'input-number',
status: 'tag',
mapping: 'tag',
list: 'input-table'
};
return map.hasOwnProperty(inputType) ? map[inputType] : inputType;
};

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class=""><path d="M2.66699 8L13.3337 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><path d="M8 2.66699L8 13.3337" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path></svg>

After

Width:  |  Height:  |  Size: 315 B

View File

@ -0,0 +1 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M6.19561 6.20513L14.5925 14.4728M5.83984 14.3673L14.2367 6.09961" stroke="currentColor" stroke-width="1.6"></path></svg>

After

Width:  |  Height:  |  Size: 190 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class=""><path d="M3 6.5C2.17157 6.5 1.5 7.17157 1.5 8C1.5 8.82843 2.17157 9.5 3 9.5C3.82843 9.5 4.5 8.82843 4.5 8C4.5 7.17157 3.82843 6.5 3 6.5Z" fill="currentColor"></path><path d="M6.5 8C6.5 7.17157 7.17157 6.5 8 6.5C8.82843 6.5 9.5 7.17157 9.5 8C9.5 8.82843 8.82843 9.5 8 9.5C7.17157 9.5 6.5 8.82843 6.5 8Z" fill="currentColor"></path><path d="M13 6.5C12.1716 6.5 11.5 7.17157 11.5 8C11.5 8.82843 12.1716 9.5 13 9.5C13.8284 9.5 14.5 8.82843 14.5 8C14.5 7.17157 13.8284 6.5 13 6.5Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@ -0,0 +1,2 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2532" width="64" height="64"><path d="M938.666667 469.333333c-23.466667 0-42.666667 19.2-42.666667 42.666667l0 85.333333 0 320c0 12.8-8.533333 21.333333-21.333333 21.333333L106.666667 938.666667c-12.8 0-21.333333-8.533333-21.333333-21.333333l0-106.666667L85.333333 234.666667c0-12.8 8.533333-21.333333 21.333333-21.333333l362.666667 0c23.466667 0 42.666667-19.2 42.666667-42.666667 0-23.466667-19.2-42.666667-42.666667-42.666667L85.333333 128c-46.933333 0-85.333333 38.4-85.333333 85.333333l0 341.333333 0 256 0 128c0 46.933333 38.4 85.333333 85.333333 85.333333l810.666667 0c46.933333 0 85.333333-38.4 85.333333-85.333333l0-128L981.333333 597.333333l0-85.333333C981.333333 488.533333 962.133333 469.333333 938.666667 469.333333zM1011.2 162.133333l-149.333333-149.333333C855.466667 4.266667 844.8 0 832 0c-23.466667 0-42.666667 19.2-42.666667 42.666667 0 12.8 4.266667 23.466667 12.8 29.866667l83.2 83.2C554.666667 202.666667 298.666667 488.533333 298.666667 832c0 23.466667 19.2 42.666667 42.666667 42.666667s42.666667-19.2 42.666667-42.666667c0-290.133333 206.933333-533.333333 484.266667-586.666667l-66.133333 66.133333C793.6 317.866667 789.333333 328.533333 789.333333 341.333333c0 23.466667 19.2 42.666667 42.666667 42.666667 12.8 0 23.466667-4.266667 29.866667-12.8l149.333333-149.333333C1019.733333 215.466667 1024 204.8 1024 192 1024 179.2 1019.733333 168.533333 1011.2 162.133333z" p-id="2533"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -183,6 +183,12 @@ import jSpaceAround from './display/jSpaceAround.svg';
// 主题
import themeCss from './theme/css.svg';
// CRUD相关
import ColumnSetting from './crud/column-setting.svg';
import ColumnDelete from './crud/column-delete.svg';
import ColumnAdd from './crud/column-add.svg';
import ShareLink from './crud/share-link.svg';
// 功能类组件 icon x 11
registerIcon('audio-plugin', audio);
registerIcon('custom-plugin', custom);
@ -351,4 +357,10 @@ registerIcon('jSpaceAround', jSpaceAround);
// 主题
registerIcon('theme-css', themeCss);
// CRUD相关
registerIcon('column-setting', ColumnSetting);
registerIcon('column-delete', ColumnDelete);
registerIcon('column-add', ColumnAdd);
registerIcon('share-link', ShareLink);
export {Icon};

View File

@ -1,6 +1,7 @@
import 'amis';
import './locale/index';
export * from 'amis-editor-core';
export * from './builder';
import './tpl/index';
export * from './plugin';
@ -40,6 +41,11 @@ import './renderer/TransferTableControl';
import './renderer/style-control/ThemeCssCode';
import './renderer/ButtonGroupControl';
import './renderer/FlexSettingControl';
import './renderer/FieldSetting';
import './renderer/TableColumnWidthControl';
import './renderer/crud2-control/CRUDColumnControl';
import './renderer/crud2-control/CRUDToolbarControl';
import './renderer/crud2-control/CRUDFiltersControl';
import 'amis-theme-editor/lib/locale/zh-CN';
import 'amis-theme-editor/lib/locale/en-US';
import 'amis-theme-editor/lib/renderers/Border';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,149 @@
/**
* @file CRUDCards.tsx
* @desc CRUD2
*/
import React from 'react';
import {autobind} from 'amis';
import {
EditorManager,
JSONPipeIn,
BuildPanelEventContext,
registerEditorPlugin
} from 'amis-editor-core';
import {DSBuilderManager, DSFeatureEnum} from '../../builder';
import {Table2RenderereEvent, Table2RendererAction} from '../Table2';
import {BaseCRUDPlugin} from './BaseCRUD';
export class CRUDCardsPlugin extends BaseCRUDPlugin {
static id = 'CardsCRUDPlugin';
disabledRendererPlugin = true;
name = '卡片列表';
panelTitle: '卡片列表';
icon = 'fa fa-window-maximize';
panelIcon = 'fa fa-table';
subPanelIcon = 'fa fa-table';
pluginIcon = 'cards-plugin';
panelJustify = true;
multifactor = true;
isBaseComponent = true;
description =
'围绕卡片列表的数据增删改查. 负责数据的拉取,分页,单条操作,批量操作,排序,快速编辑等等功能,集成查询条件。';
order = -1000;
$schema = '/schemas/CRUD2CardsSchema.json';
docLink = '/amis/zh-CN/components/crud2';
previewSchema: Record<string, any> = this.generatePreviewSchema('cards');
scaffold: any = this.generateScaffold('cards');
constructor(manager: EditorManager) {
super(manager, Table2RenderereEvent, Table2RendererAction);
this.dsManager = new DSBuilderManager(manager);
}
/** 非实体数据源走默认构建 */
panelBodyCreator = (context: BuildPanelEventContext) => {
/** 先写入动态控件 */
this.dynamicControls = {
/** 列配置 */
columns: context => this.renderColumnsControl(context),
/** 工具栏配置 */
toolbar: context => this.renderToolbarCollapse(context),
/** 搜索栏 */
filters: context => this.renderFiltersCollapse(context)
};
return this.baseCRUDPanelBody(context);
};
@autobind
renderColumnsControl(context: BuildPanelEventContext) {
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
return {
title: '列设置',
order: 5,
body: [
{
type: 'ae-crud-column-control',
name: 'columns',
nodeId: context.id,
builder
}
]
};
}
@autobind
renderToolbarCollapse(context: BuildPanelEventContext) {
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
return {
order: 20,
title: '工具栏',
body: [
{
type: 'ae-crud-toolbar-control',
name: 'headerToolbar',
nodeId: context.id,
builder
}
]
};
}
@autobind
renderFiltersCollapse(context: BuildPanelEventContext) {
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
const collection: any[] = [];
builder.features.forEach(feat => {
if (/Query$/.test(feat)) {
collection.push({
type: 'ae-crud-filters-control',
name:
feat === DSFeatureEnum.SimpleQuery ||
feat === DSFeatureEnum.AdvancedQuery
? 'filter'
: feat === DSFeatureEnum.FuzzyQuery
? 'headerToolbar'
: undefined,
label:
feat === DSFeatureEnum.SimpleQuery
? '简单查询'
: feat === DSFeatureEnum.AdvancedQuery
? '高级查询'
: '模糊查询',
nodeId: context.id,
feat: feat,
builder
});
}
});
return collection.length > 0
? {
order: 10,
title: '搜索设置',
body: collection
}
: undefined;
}
}
// registerEditorPlugin(CRUDCardsPlugin);

View File

@ -0,0 +1,149 @@
/**
* @file CRUDList.tsx
* @desc CRUD2
*/
import React from 'react';
import {autobind} from 'amis';
import {
EditorManager,
JSONPipeIn,
BuildPanelEventContext,
registerEditorPlugin
} from 'amis-editor-core';
import {DSBuilderManager, DSFeatureEnum} from '../../builder';
import {Table2RenderereEvent, Table2RendererAction} from '../Table2';
import {BaseCRUDPlugin} from './BaseCRUD';
export class CRUDListPlugin extends BaseCRUDPlugin {
static id = 'ListCRUDPlugin';
disabledRendererPlugin = true;
name = '列表';
panelTitle: '列表';
icon = 'fa fa-list';
panelIcon = 'fa fa-list';
subPanelIcon = 'fa fa-list';
pluginIcon = 'list-plugin';
panelJustify = true;
multifactor = true;
isBaseComponent = true;
description =
'围绕列表的数据增删改查. 负责数据的拉取,分页,单条操作,批量操作,排序,快速编辑等等功能,集成查询条件。';
order = -1000;
$schema = '/schemas/CRUD2ListSchema.json';
docLink = '/amis/zh-CN/components/crud2';
previewSchema: Record<string, any> = this.generatePreviewSchema('list');
scaffold: any = this.generateScaffold('list');
constructor(manager: EditorManager) {
super(manager, Table2RenderereEvent, Table2RendererAction);
this.dsManager = new DSBuilderManager(manager);
}
/** 非实体数据源走默认构建 */
panelBodyCreator = (context: BuildPanelEventContext) => {
/** 先写入动态控件 */
this.dynamicControls = {
/** 列配置 */
columns: context => this.renderColumnsControl(context),
/** 工具栏配置 */
toolbar: context => this.renderToolbarCollapse(context),
/** 搜索栏 */
filters: context => this.renderFiltersCollapse(context)
};
return this.baseCRUDPanelBody(context);
};
@autobind
renderColumnsControl(context: BuildPanelEventContext) {
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
return {
title: '列设置',
order: 5,
body: [
{
type: 'ae-crud-column-control',
name: 'columns',
nodeId: context.id,
builder
}
]
};
}
@autobind
renderToolbarCollapse(context: BuildPanelEventContext) {
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
return {
order: 20,
title: '工具栏',
body: [
{
type: 'ae-crud-toolbar-control',
name: 'headerToolbar',
nodeId: context.id,
builder
}
]
};
}
@autobind
renderFiltersCollapse(context: BuildPanelEventContext) {
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
const collection: any[] = [];
builder.features.forEach(feat => {
if (/Query$/.test(feat)) {
collection.push({
type: 'ae-crud-filters-control',
name:
feat === DSFeatureEnum.SimpleQuery ||
feat === DSFeatureEnum.AdvancedQuery
? 'filter'
: feat === DSFeatureEnum.FuzzyQuery
? 'headerToolbar'
: undefined,
label:
feat === DSFeatureEnum.SimpleQuery
? '简单查询'
: feat === DSFeatureEnum.AdvancedQuery
? '高级查询'
: '模糊查询',
nodeId: context.id,
feat: feat,
builder
});
}
});
return collection.length > 0
? {
order: 10,
title: '搜索设置',
body: collection
}
: undefined;
}
}
// registerEditorPlugin(CRUDListPlugin);

View File

@ -0,0 +1,58 @@
/**
* @file CRUDTable.tsx
* @desc CRUD2
*/
import React from 'react';
import sortBy from 'lodash/sortBy';
import {autobind} from 'amis';
import {
EditorManager,
JSONPipeIn,
BuildPanelEventContext,
EditorNodeType,
registerEditorPlugin
} from 'amis-editor-core';
import {
DSBuilder,
DSBuilderManager,
DSFeatureEnum,
DSFeatureType
} from '../../builder';
import {Table2RenderereEvent, Table2RendererAction} from '../Table2';
import {BaseCRUDPlugin} from './BaseCRUD';
export class CRUDTablePlugin extends BaseCRUDPlugin {
static id = 'TableCRUDPlugin';
panelJustify = true;
multifactor = true;
isBaseComponent = true;
description =
'用来实现对数据的增删改查,用来展示表格数据,可以配置列信息,然后关联数据便能完成展示。支持嵌套、超级表头、列固定、表头固顶、合并单元格等等。';
order = -950;
$schema = '/schemas/CRUD2TableSchema.json';
docLink = '/amis/zh-CN/components/crud2';
previewSchema: Record<string, any> = this.generatePreviewSchema('table2');
scaffold: any = this.generateScaffold('table2');
constructor(manager: EditorManager) {
super(manager, Table2RenderereEvent, Table2RendererAction);
this.dsManager = new DSBuilderManager(manager);
}
/** 非实体数据源走默认构建 */
panelBodyCreator = (context: BuildPanelEventContext) => {
return this.baseCRUDPanelBody(context);
};
}
registerEditorPlugin(CRUDTablePlugin);

View File

@ -0,0 +1,51 @@
/**
* @file constants.ts
* @desc CRUD
*/
import {DSFeatureEnum} from '../../builder/constants';
export const ToolsConfig = {
groupName: 'tools',
options: [
{
label: '新增记录',
value: 'Insert',
align: 'left',
icon: 'fa fa-layer-group',
order: 10
},
{
label: '批量编辑',
value: 'BulkEdit',
align: 'left',
icon: 'fa fa-layer-group',
order: 20
},
{
label: '批量删除',
value: 'BulkDelete',
align: 'left',
icon: 'fa fa-layer-group',
order: 30
}
]
};
export const FiltersConfig = {
groupName: 'filters',
options: [
{label: '模糊查询', value: 'FuzzyQuery', icon: 'fa fa-search', order: 10},
{label: '简单查询', value: 'SimpleQuery', icon: 'fa fa-search', order: 20},
{label: '高级查询', value: 'AdvancedQuery', icon: 'fa fa-search', order: 30}
]
};
export const OperatorsConfig = {
groupName: 'operators',
options: [
{label: '查看详情', value: 'View', icon: 'fa fa-database', order: 10},
{label: '编辑记录', value: 'Edit', icon: 'fa fa-database', order: 20},
{label: '删除记录', value: 'Delete', icon: 'fa fa-database', order: 30}
]
};

View File

@ -0,0 +1,194 @@
import isEmpty from 'lodash/isEmpty';
import isObject from 'lodash/isObject';
import remove from 'lodash/remove';
import pickBy from 'lodash/pickBy';
import cloneDeep from 'lodash/cloneDeep';
export function findAndUpdate<T = any>(
arr: T[],
compareFn: (item: T) => boolean,
target?: T
) {
if (!target) {
return arr;
}
const result = cloneDeep(arr);
const idx = result.findIndex(item => compareFn(item));
if (~idx) {
result.splice(idx, 1, target);
}
return result;
}
/** 深度删除 */
export const deepRemove = (
obj: any,
predicate: (obj: any) => boolean,
checkAll: boolean = false
): any => {
const waitProcess = [obj];
let find = false;
while (waitProcess.length) {
if (find) {
break;
}
let item: any = waitProcess.pop();
if (Array.isArray(item)) {
remove(item, (val: any) => {
const res = predicate(val);
if (res && !checkAll) {
find = true;
}
return res;
});
waitProcess.push(...item);
continue;
}
if (!isObject(item)) {
continue;
}
Object.entries(item).forEach(([key, value]) => {
if (isObject(value) && predicate(value)) {
delete item[key];
checkAll || (find = true);
}
waitProcess.push(value);
});
}
return find;
};
export const findObj = (
obj: any,
predicate: (obj: any) => boolean,
stop?: (obj: any) => boolean
): any | void => {
const waitProcess = [obj];
while (waitProcess.length) {
let item: any = waitProcess.shift();
if (Array.isArray(item)) {
waitProcess.push(...item);
continue;
}
if (!isObject(item) || (stop && stop(item))) {
continue;
}
if (predicate(item)) {
return item;
}
waitProcess.push(
...Object.values(
pickBy(item, (val: any, key: string) => !String(key).startsWith('__'))
)
);
}
};
/** schema 中查找 */
export const findSchema = (
schema: any,
predicate: (obj: any) => boolean,
...scope: string[]
) => {
if (scope.length === 0) {
return findObj(schema, predicate);
}
let region = null;
while ((region = scope.shift())) {
const res = findObj(schema[region], predicate);
if (res) {
return res;
}
}
return null;
};
/** headerToolbar 和 footerToolbar 布局换成 flex 包裹 container */
export const addSchema2Toolbar = (
schema: any,
content: any,
position: 'header' | 'footer',
align: 'left' | 'right'
) => {
const region = `${position}Toolbar`;
const buildFlex = (items: any[] = []) => ({
type: 'flex',
items,
style: {
position: 'static'
},
direction: 'row',
justify: 'flex-start',
alignItems: 'stretch'
});
const buildContainer = (align?: 'left' | 'right', body: any[] = []) => ({
type: 'container',
body,
wrapperBody: false,
style: {
flexGrow: 1,
flex: '1 1 auto',
position: 'static',
display: 'flex',
flexBasis: 'auto',
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'stretch',
...(align
? {
justifyContent: align === 'left' ? 'flex-start' : 'flex-end'
}
: {})
}
});
if (
!schema[region] ||
isEmpty(schema[region]) ||
!Array.isArray(schema[region])
) {
const isArr = Array.isArray(schema[region]);
const newSchema = buildFlex([
buildContainer('left', isArr || !schema[region] ? [] : [schema[region]]),
buildContainer('right')
]);
(isArr && schema[region].push(newSchema)) || (schema[region] = [newSchema]);
}
// 尝试放到左面第一个,否则只能放外头了
try {
// 优先判断没有右边列的情况避免都走到catch里造成嵌套层数过多的问题
if (align === 'right' && schema[region][0].items.length < 2) {
schema[region][0].items.push(buildContainer('right'));
}
schema[region][0].items[
align === 'left' ? 0 : schema[region][0].items.length - 1
].body.push(content);
} catch (e) {
const olds = [...schema[region]];
schema[region].length = 0;
schema[region].push(
buildFlex([
buildContainer('left', olds),
buildContainer('right', content)
])
);
}
};

View File

@ -1,6 +1,6 @@
import {Button} from 'amis';
import {Button, JSONValueMap, isObject} from 'amis';
import React from 'react';
import {registerEditorPlugin} from 'amis-editor-core';
import {EditorNodeType, registerEditorPlugin} from 'amis-editor-core';
import {
BaseEventContext,
BasePlugin,
@ -16,7 +16,8 @@ import {
} from 'amis-editor-core';
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
import {diff, JSONPipeOut, repeatArray} from 'amis-editor-core';
import {resolveArrayDatasource} from '../util';
import set from 'lodash/set';
import {escapeFormula, resolveArrayDatasource} from '../util';
export class CardsPlugin extends BasePlugin {
static id = 'CardsPlugin';
@ -28,6 +29,7 @@ export class CardsPlugin extends BasePlugin {
// 组件名称
name = '卡片列表';
isBaseComponent = true;
isListComponent = true;
description =
'功能类似于表格,但是用一个个小卡片来展示数据。当前组件需要配置数据源,不自带数据拉取,请优先使用 「CRUD」 组件。';
docLink = '/amis/zh-CN/components/cards';
@ -36,36 +38,330 @@ export class CardsPlugin extends BasePlugin {
pluginIcon = 'cards-plugin';
scaffold = {
type: 'cards',
data: {
items: [
{a: 1, b: 2},
{a: 3, b: 4}
]
},
columnsCount: 2,
columnsCount: 4,
card: {
type: 'card',
className: 'm-b-none',
header: {
title: '标题',
subTitle: '副标题'
},
type: 'container',
body: [
{
name: 'a',
label: 'A'
},
{
name: 'b',
label: 'B'
type: 'container',
body: [
{
type: 'container',
body: [
{
type: 'icon',
icon: 'fa fa-check',
vendor: '',
themeCss: {
className: {
'font': {
color: 'var(--colors-brand-6)',
fontSize: '20px'
},
'padding-and-margin:default': {
marginRight: '10px'
}
}
}
},
{
type: 'tpl',
tpl: '流水线任务实例 ',
inline: true,
wrapperComponent: '',
editorSetting: {
mock: {}
},
style: {
fontSize: 'var(--fonts-size-6)',
color: 'var(--colors-neutral-text-2)',
fontWeight: 'var(--fonts-weight-3)'
}
}
],
style: {
position: 'static',
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
marginBottom: '15px'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false,
size: 'none'
},
{
type: 'flex',
className: 'p-1',
items: [
{
type: 'container',
body: [
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '12/',
inline: true,
wrapperComponent: '',
style: {
fontSize: 'var(--fonts-size-6)',
color: 'var(--colors-neutral-text-2)',
fontWeight: 'var(--fonts-weight-3)'
}
},
{
type: 'tpl',
tpl: '19',
inline: true,
wrapperComponent: '',
style: {
color: 'var(--colors-neutral-text-6)',
fontSize: 'var(--fonts-size-6)'
}
}
],
style: {
position: 'static',
display: 'block',
flex: '0 0 auto',
marginTop: 'var(--sizes-size-0)',
marginRight: 'var(--sizes-size-0)',
marginBottom: 'var(--sizes-size-0)',
marginLeft: 'var(--sizes-size-0)'
},
wrapperBody: false,
isFixedWidth: false,
size: 'none'
},
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '单元测试',
inline: true,
wrapperComponent: '',
style: {
color: 'var(--colors-neutral-text-5)'
}
}
],
style: {
position: 'static',
display: 'flex',
flexWrap: 'nowrap',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
flex: '0 0 auto'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false,
size: 'none'
}
],
size: 'xs',
style: {
position: 'static',
display: 'flex',
flex: '1 1 auto',
flexGrow: 1,
flexBasis: 'auto',
flexWrap: 'nowrap',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false
},
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '100%',
inline: true,
wrapperComponent: '',
style: {
fontSize: 'var(--fonts-size-6)',
color: 'var(--colors-neutral-text-2)',
fontWeight: 'var(--fonts-weight-3)'
}
},
{
type: 'tpl',
tpl: '通过率',
inline: true,
wrapperComponent: '',
style: {
color: 'var(--colors-neutral-text-5)'
}
}
],
size: 'xs',
style: {
position: 'static',
display: 'flex',
flex: '1 1 auto',
flexGrow: 1,
flexBasis: 'auto',
flexWrap: 'nowrap',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'center'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false
},
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '99.9%',
inline: true,
wrapperComponent: '',
style: {
fontSize: 'var(--fonts-size-6)',
color: 'var(--colors-neutral-text-2)',
fontWeight: 'var(--fonts-weight-3)'
}
},
{
type: 'tpl',
tpl: '任务实例',
inline: true,
wrapperComponent: '',
style: {
color: 'var(--colors-neutral-text-5)'
}
}
],
size: 'xs',
style: {
position: 'static',
display: 'flex',
flex: '1 1 auto',
flexGrow: 1,
flexBasis: 'auto',
flexWrap: 'nowrap',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false
}
],
style: {
position: 'relative'
}
},
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '报告',
inline: true,
wrapperComponent: '',
style: {
fontSize: '14px',
color: 'var(--colors-neutral-text-5)'
}
},
{
type: 'tpl',
tpl: '2023-01-01 12:00',
inline: true,
wrapperComponent: '',
style: {
fontSize: '12px',
color: 'var(--colors-neutral-text-6)'
}
}
],
style: {
position: 'static',
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'space-between',
marginTop: '20px'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false
}
],
size: 'none',
style: {
'position': 'static',
'display': 'block',
'overflowY': 'auto',
'overflowX': 'auto',
'paddingTop': '10px',
'paddingRight': '10px',
'paddingBottom': '10px',
'paddingLeft': '10px',
'radius': {
'top-left-border-radius': '6px',
'top-right-border-radius': '6px',
'bottom-left-border-radius': '6px',
'bottom-right-border-radius': '6px'
},
'top-border-width': 'var(--borders-width-4)',
'left-border-width': 'var(--borders-width-2)',
'right-border-width': 'var(--borders-width-2)',
'bottom-border-width': 'var(--borders-width-2)',
'top-border-style': 'var(--borders-style-2)',
'left-border-style': 'var(--borders-style-2)',
'right-border-style': 'var(--borders-style-2)',
'bottom-border-style': 'var(--borders-style-2)',
'top-border-color': 'var(--colors-brand-6)',
'left-border-color': 'var(--colors-brand-10)',
'right-border-color': 'var(--colors-brand-10)',
'bottom-border-color': 'var(--colors-brand-10)',
'flex': '0 0 150px',
'marginRight': '15px',
'flexBasis': '100%'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: true,
onEvent: {
click: {
weight: 0,
actions: []
}
}
}
],
actions: [
{
label: '详情',
type: 'button'
}
]
style: {
position: 'static',
display: 'flex',
width: '1000%',
overflowX: 'auto',
margin: '0',
flexWrap: 'nowrap',
justifyContent: 'space-between'
},
isFixedHeight: false,
isFixedWidth: true,
wrapperBody: false
},
placeholder: '',
name: '',
style: {
gutterX: 15,
gutterY: 15
}
};
previewSchema = {
@ -74,6 +370,7 @@ export class CardsPlugin extends BasePlugin {
};
panelTitle = '卡片集';
panelJustify = true;
panelBodyCreator = (context: BaseEventContext) => {
const isCRUDBody = context.schema.type === 'crud';
const curPosition = context?.schema?.style?.position;
@ -82,200 +379,119 @@ export class CardsPlugin extends BasePlugin {
return [
getSchemaTpl('tabs', [
{
title: '常规',
body: [
getSchemaTpl('layout:originPosition', {
visibleOn: isAbsolute ? isAbsolute : undefined,
value: 'left-top'
}),
title: '属性',
body: getSchemaTpl('collapseGroup', [
{
children: (
<div className="m-b">
<Button
level="primary"
size="sm"
block
onClick={this.editDetail.bind(this, context.id)}
>
</Button>
</div>
)
},
{
type: 'divider'
},
getSchemaTpl('title'),
isCRUDBody
? null
: {
name: 'source',
title: '基本',
body: [
{
type: 'input-text',
label: '数据源',
pipeIn: defaultValue('${items}'),
description: '绑定当前环境变量',
test: !isCRUDBody
label: '组件名称',
name: 'editorSetting.displayName'
},
getSchemaTpl('cardsPlaceholder')
]
isCRUDBody
? null
: getSchemaTpl('formItemName', {
label: '绑定字段名'
}),
getSchemaTpl('cardsPlaceholder')
]
},
getSchemaTpl('status')
])
},
{
title: '外观',
body: [
getSchemaTpl('switch', {
name: 'showHeader',
label: '是否显示头部',
pipeIn: defaultValue(true)
}),
getSchemaTpl('switch', {
name: 'showFooter',
label: '是否显示底部',
pipeIn: defaultValue(true)
}),
getSchemaTpl('className', {
label: 'CSS 类名'
}),
getSchemaTpl('className', {
name: 'headerClassName',
label: '头部 CSS 类名'
}),
getSchemaTpl('className', {
name: 'footerClassName',
label: '底部 CSS 类名'
}),
getSchemaTpl('className', {
name: 'itemsClassName',
label: '内容 CSS 类名'
}),
getSchemaTpl('className', {
pipeIn: defaultValue('Grid-col--sm6 Grid-col--md4 Grid-col--lg3'),
name: 'itemClassName',
label: '卡片 CSS 类名'
}),
body: getSchemaTpl('collapseGroup', [
{
name: 'columnsCount',
type: 'input-range',
visibleOn: '!this.leftFixed',
min: 0,
max: 12,
step: 1,
label: '每行显示个数',
description: '不设置时,由卡片 CSS 类名决定'
title: '组件',
body: [
{
name: 'columnsCount',
type: 'input-range',
visibleOn: '!this.leftFixed',
min: 0,
max: 12,
step: 1,
label: '每行个数',
description: '不设置时,由卡片 CSS 类名决定'
},
{
type: 'input-number',
label: '左右间距',
name: 'style.gutterX'
},
{
type: 'input-number',
label: '上下间距',
name: 'style.gutterY'
},
getSchemaTpl('switch', {
name: 'masonryLayout',
label: '启用瀑布流'
}),
getSchemaTpl('layout:originPosition', {
visibleOn: isAbsolute ? isAbsolute : undefined,
value: 'left-top'
})
]
},
getSchemaTpl('switch', {
name: 'masonryLayout',
label: '启用瀑布流'
})
]
},
{
title: '显隐',
body: [getSchemaTpl('ref'), getSchemaTpl('visible')]
...getSchemaTpl('theme:common', {exclude: ['layout']})
])
}
])
];
};
editDetail(id: string) {
const manager = this.manager;
const store = manager.store;
const node = store.getNodeById(id);
const value = store.getValueOf(id);
buildDataSchemas(node: EditorNodeType, region: EditorNodeType) {
let dataSchema: any = {
$id: 'cards',
type: 'object',
title: '当前列表项',
properties: {}
};
node &&
value &&
this.manager.openSubEditor({
title: '配置成员渲染器',
value: {
type: 'card',
...value.card
},
slot: {
type: 'container',
body: '$$'
},
typeMutable: false,
onChange: newValue => {
newValue = {...value, card: newValue};
manager.panelChangeValue(newValue, diff(value, newValue));
},
data: {
item: 'mocked data',
index: 0
}
});
}
let match =
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
let field = node.schema.name || match?.[1];
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
const schema = scope?.parent?.getSchemaByPath(field);
if (isObject(schema?.items)) {
dataSchema = {
...dataSchema,
...(schema!.items as any)
};
buildEditorToolbar(
{id, info, schema}: BaseEventContext,
toolbars: Array<BasicToolbarItem>
) {
if (
info.renderer.name === 'cards' ||
(info.renderer.name === 'crud' && schema.mode === 'cards')
) {
toolbars.push({
icon: 'fa fa-expand',
order: 100,
tooltip: '配置成员渲染器',
onClick: this.editDetail.bind(this, id)
// 列表添加序号方便处理
set(dataSchema, 'properties.index', {
type: 'number',
title: '索引'
});
}
}
buildEditorContextMenu(
{id, schema, region, info, selections}: ContextMenuEventContext,
menus: Array<ContextMenuItem>
) {
if (selections.length || info?.plugin !== this) {
return;
}
if (
info.renderer.name === 'cards' ||
(info.renderer.name === 'crud' && schema.mode === 'cards')
) {
menus.push('|', {
label: '配置成员渲染器',
onSelect: this.editDetail.bind(this, id)
});
}
return dataSchema;
}
filterProps(props: any) {
const data = {
...props.defaultData,
...props.data
};
const arr = resolveArrayDatasource({
value: props.value,
data,
source: props.source
});
if (!Array.isArray(arr) || !arr.length) {
const mockedData: any = {
id: 666,
title: '假数据',
description: '假数据',
a: '假数据',
b: '假数据'
};
props.value = repeatArray(mockedData, 1).map((item, index) => ({
// 编辑时显示两行假数据
const count = (props.columnsCount || 3) * 2;
props.value = repeatArray({}, count).map((item, index) => {
return {
...item,
id: index + 1
}));
};
});
props.className = `${props.className || ''} ae-Editor-list`;
props.itemsClassName = `${props.itemsClassName || ''} cards-items`;
if (props.card && !props.card.className?.includes('listItem')) {
props.card.className = `${props.card.className || ''} ae-Editor-listItem`;
}
const {$schema, ...rest} = props;
// 列表类型内的文本元素显示原始公式
props = escapeFormula(props);
return {
...JSONPipeOut(rest),
$schema
};
return props;
}
getRendererInfo(

View File

@ -1,3 +1,4 @@
import update from 'lodash/update';
import {
BaseEventContext,
BasePlugin,
@ -10,24 +11,47 @@ import {
export class ColumnToggler extends BasePlugin {
static id = 'ColumnToggler';
// 关联渲染器名字
rendererName = 'column-toggler';
$schema = '/schemas/ColumnToggler.json';
// 组件名称
name = '自定义显示列';
isBaseComponent = true;
disabledRendererPlugin = true;
description = '用来展示表格的自定义显示列按钮,你可以配置不同的展示样式。';
tags = ['自定义显示列'];
icon = 'fa fa-square';
panelTitle = '自定义显示列';
icon = 'fa fa-square';
tags = ['自定义显示列'];
$schema = '/schemas/ColumnTogglerSchema.json';
description = '用来展示表格的自定义显示列按钮,你可以配置不同的展示样式。';
panelJustify = true;
isBaseComponent = true;
disabledRendererPlugin = true;
crudInfo: {id: any; columns: any[]; schema: any};
panelBodyCreator = (context: BaseEventContext) => {
const crud = context?.node?.getClosestParentByType('crud2');
if (crud) {
this.crudInfo = {
id: crud.id,
columns: crud.schema.columns || [],
schema: crud.schema
};
}
const columns = (this.crudInfo?.schema?.columns ?? []).map(
(item: any, index: number) => ({
label: item.title,
value: index
})
);
return getSchemaTpl('tabs', [
{
title: '属性',
@ -53,6 +77,64 @@ export class ColumnToggler extends BasePlugin {
label: '按钮图标'
})
]
},
{
title: '列默认显示',
body: [
{
name: `__toggled`,
value: '',
type: 'checkboxes',
// className: 'b-a p-sm',
label: false,
inline: false,
joinValues: false,
extractValue: true,
options: columns,
// style: {
// maxHeight: '200px',
// overflow: 'auto'
// },
pipeIn: (value: any, form: any) => {
const showColumnIndex: number[] = [];
this.crudInfo?.schema?.columns?.forEach(
(item: any, index: number) => {
if (item.toggled !== false) {
showColumnIndex.push(index);
}
}
);
return showColumnIndex;
},
onChange: (value: number[]) => {
if (!this.crudInfo) {
return;
}
let newColumns = this.crudInfo.schema.columns;
newColumns = newColumns.map((item: any, index: number) => ({
...item,
toggled: value.includes(index) ? undefined : false
}));
const updatedSchema = update(
this.crudInfo.schema,
'columns',
(origin: any) => {
return newColumns;
}
);
this.manager.store.changeValueById(
this.crudInfo.id,
updatedSchema
);
this.crudInfo.schema = updatedSchema;
}
}
]
}
])
},

View File

@ -1,16 +1,10 @@
import {Button} from 'amis';
import {isObject} from 'amis';
import React from 'react';
import {registerEditorPlugin} from 'amis-editor-core';
import {
BaseEventContext,
BasePlugin,
BasicToolbarItem,
ContextMenuEventContext,
ContextMenuItem
} from 'amis-editor-core';
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
import {diff, JSONPipeOut} from 'amis-editor-core';
import {schemaToArray} from '../util';
import {EditorNodeType, registerEditorPlugin} from 'amis-editor-core';
import {BaseEventContext, BasePlugin} from 'amis-editor-core';
import {getSchemaTpl} from 'amis-editor-core';
import {escapeFormula} from '../util';
import {set} from 'lodash';
export class EachPlugin extends BasePlugin {
static id = 'EachPlugin';
@ -22,132 +16,355 @@ export class EachPlugin extends BasePlugin {
// 组件名称
name = '循环 Each';
isBaseComponent = true;
isListComponent = true;
description = '功能渲染器,可以基于现有变量循环输出渲染器。';
tags = ['功能'];
icon = 'fa fa-repeat';
pluginIcon = 'each-plugin';
scaffold = {
type: 'each',
name: 'arr',
name: '',
items: {
type: 'tpl',
tpl: '<%= data.index + 1 %>. 内容:<%= data.item %>',
wrapperComponent: '',
inline: false
}
type: 'container',
body: [
{
type: 'container',
body: [
{
type: 'icon',
icon: 'fa fa-plane',
vendor: '',
themeCss: {
className: {
'padding-and-margin:default': {
marginRight: '4px'
},
'font': {
color: '#2856ad',
fontSize: '20px'
}
}
}
},
{
type: 'tpl',
style: {
fontWeight: 'var(--fonts-weight-3)',
fontSize: '16px',
color: 'var(--colors-brand-6)'
},
tpl: '回访数量TOP1',
inline: true,
wrapperComponent: ''
}
],
style: {
position: 'static',
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'flex-start',
alignItems: 'center',
marginBottom: '6px'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false
},
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '北京分公司',
inline: true,
wrapperComponent: '',
style: {
'fontSize': 'var(--fonts-size-4)',
'color': 'var(--colors-neutral-text-2)',
'fontWeight': 'var(--fonts-weight-3)',
'font-family': '-apple-system'
}
}
],
style: {
position: 'static',
display: 'block'
},
wrapperBody: false
}
],
size: 'none',
style: {
position: 'static',
display: 'block',
flex: '0 0 150px',
marginRight: '20px',
paddingTop: '20px',
paddingRight: '15px',
paddingBottom: '20px',
paddingLeft: '15px',
flexBasis: '250px',
overflowX: 'auto',
overflowY: 'auto',
boxShadow: ' 0px 0px 8px 0px rgba(3, 3, 3, 0.1)',
radius: {
'top-left-border-radius': 'var(--borders-radius-3)',
'top-right-border-radius': 'var(--borders-radius-3)',
'bottom-left-border-radius': 'var(--borders-radius-3)',
'bottom-right-border-radius': 'var(--borders-radius-3)'
}
},
wrapperBody: false,
isFixedHeight: false
},
placeholder: '',
style: {
position: 'static',
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'flex-start',
alignItems: 'center',
marginTop: '10px',
marginBottom: '10px'
},
isFixedHeight: false,
isFixedWidth: false,
size: 'none'
};
previewSchema = {
...this.scaffold,
value: ['a', 'b', 'c']
};
panelTitle = '循环';
panelJustify = true;
panelBodyCreator = (context: BaseEventContext) => {
return [
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
{
type: 'input-text',
name: 'name',
label: '关联字段',
placeholder: 'varname',
description:
'如果所在容器有下发 value 则不需要配置如果没有请配置变量名支持多层级如a.b表示关联a对象下的b属性。目标变量可以是数组也可以是对象。'
},
const curRendererSchema = context?.schema;
const isFreeContainer = curRendererSchema?.isFreeContainer || false;
const isFlexItem = this.manager?.isFlexItem(context?.id);
const isFlexColumnItem = this.manager?.isFlexColumnItem(context?.id);
{
children: (
<Button
size="sm"
level="primary"
className="m-b"
block
onClick={this.editDetail.bind(this, context.id)}
>
</Button>
)
},
const displayTpl = [
getSchemaTpl('layout:display'),
getSchemaTpl('placeholder', {
label: '占位符',
pipeIn: defaultValue('暂无内容'),
description:
'当没有关联变量,或者目标变量不是数组或者对象时显示此占位信息'
getSchemaTpl('layout:flex-setting', {
visibleOn:
'data.style && (data.style.display === "flex" || data.style.display === "inline-flex")',
direction: curRendererSchema.direction,
justify: curRendererSchema.justify,
alignItems: curRendererSchema.alignItems
}),
getSchemaTpl('className')
getSchemaTpl('layout:flex-wrap', {
visibleOn:
'data.style && (data.style.display === "flex" || data.style.display === "inline-flex")'
})
];
return getSchemaTpl('tabs', [
{
title: '属性',
body: getSchemaTpl('collapseGroup', [
{
title: '基本',
body: [
{
type: 'input-text',
label: '组件名称',
name: 'editorSetting.displayName'
},
getSchemaTpl('formItemName', {
label: '绑定字段名',
paramType: 'output'
}),
getSchemaTpl('valueFormula', {
rendererSchema: {
type: 'input-number',
min: 1
},
name: 'maxLength',
label: '最大显示个数',
valueType: 'number'
}),
getSchemaTpl('valueFormula', {
rendererSchema: {
type: 'input-text'
},
name: 'placeholder',
label: '空数据提示'
})
]
},
getSchemaTpl('status')
])
},
{
title: '外观',
body: getSchemaTpl('collapseGroup', [
{
title: '布局',
body: [
getSchemaTpl('layout:padding'),
getSchemaTpl('layout:position', {
visibleOn: '!data.stickyStatus'
}),
getSchemaTpl('layout:originPosition'),
getSchemaTpl('layout:inset', {
mode: 'vertical'
}),
// 自由容器不需要 display 相关配置项
...(!isFreeContainer ? displayTpl : []),
isFlexItem
? getSchemaTpl('layout:flex', {
isFlexColumnItem,
label: isFlexColumnItem ? '高度设置' : '宽度设置',
visibleOn:
'data.style && (data.style.position === "static" || data.style.position === "relative")'
})
: null,
isFlexItem
? getSchemaTpl('layout:flex-grow', {
visibleOn:
'data.style && data.style.flex === "1 1 auto" && (data.style.position === "static" || data.style.position === "relative")'
})
: null,
isFlexItem
? getSchemaTpl('layout:flex-basis', {
label: isFlexColumnItem ? '弹性高度' : '弹性宽度',
visibleOn:
'data.style && (data.style.position === "static" || data.style.position === "relative") && data.style.flex === "1 1 auto"'
})
: null,
isFlexItem
? getSchemaTpl('layout:flex-basis', {
label: isFlexColumnItem ? '固定高度' : '固定宽度',
visibleOn:
'data.style && (data.style.position === "static" || data.style.position === "relative") && data.style.flex === "0 0 150px"'
})
: null,
getSchemaTpl('layout:overflow-x', {
visibleOn: `${
isFlexItem && !isFlexColumnItem
} && data.style.flex === '0 0 150px'`
}),
getSchemaTpl('layout:isFixedHeight', {
visibleOn: `${!isFlexItem || !isFlexColumnItem}`,
onChange: (value: boolean) => {
context?.node.setHeightMutable(value);
}
}),
getSchemaTpl('layout:height', {
visibleOn: `${!isFlexItem || !isFlexColumnItem}`
}),
getSchemaTpl('layout:max-height', {
visibleOn: `${!isFlexItem || !isFlexColumnItem}`
}),
getSchemaTpl('layout:min-height', {
visibleOn: `${!isFlexItem || !isFlexColumnItem}`
}),
getSchemaTpl('layout:overflow-y', {
visibleOn: `${
!isFlexItem || !isFlexColumnItem
} && (data.isFixedHeight || data.style && data.style.maxHeight) || (${
isFlexItem && isFlexColumnItem
} && data.style.flex === '0 0 150px')`
}),
getSchemaTpl('layout:isFixedWidth', {
visibleOn: `${!isFlexItem || isFlexColumnItem}`,
onChange: (value: boolean) => {
context?.node.setWidthMutable(value);
}
}),
getSchemaTpl('layout:width', {
visibleOn: `${!isFlexItem || isFlexColumnItem}`
}),
getSchemaTpl('layout:max-width', {
visibleOn: `${!isFlexItem || isFlexColumnItem}`
}),
getSchemaTpl('layout:min-width', {
visibleOn: `${!isFlexItem || isFlexColumnItem}`
}),
getSchemaTpl('layout:overflow-x', {
visibleOn: `${
!isFlexItem || isFlexColumnItem
} && (data.isFixedWidth || data.style && data.style.maxWidth)`
}),
!isFlexItem ? getSchemaTpl('layout:margin-center') : null,
!isFlexItem && !isFreeContainer
? getSchemaTpl('layout:textAlign', {
name: 'style.textAlign',
label: '内部对齐方式',
visibleOn:
'data.style && data.style.display !== "flex" && data.style.display !== "inline-flex"'
})
: null,
getSchemaTpl('layout:z-index'),
getSchemaTpl('layout:sticky', {
visibleOn:
'data.style && (data.style.position !== "fixed" && data.style.position !== "absolute")'
}),
getSchemaTpl('layout:stickyPosition')
]
},
...getSchemaTpl('theme:common', {exclude: ['layout']})
])
}
]);
};
filterProps(props: any) {
props = JSONPipeOut(props);
// 列表类型内的文本元素显示{{公式}}或者自定义展位,不显示实际值
props = escapeFormula(props);
// 循环编辑态显示2个元素
props.value = [{}, {}];
// 至少显示一个成员,否则啥都不显示。
if (!props.value) {
props.value = [
{
item: 'mocked data'
}
];
props.className = `${props.className || ''} ae-Editor-list`;
if (props.items && !props.items.className?.includes('listItem')) {
props.items.className = `${
props.items.className || ''
} ae-Editor-eachItem`;
}
return props;
}
buildEditorToolbar(
{id, info}: BaseEventContext,
toolbars: Array<BasicToolbarItem>
) {
if (info.renderer.name === 'each') {
toolbars.push({
icon: 'fa fa-expand',
order: 100,
tooltip: '配置成员渲染器',
onClick: this.editDetail.bind(this, id)
buildDataSchemas(node: EditorNodeType, region?: EditorNodeType) {
let dataSchema: any = {
$id: 'each',
type: 'object',
title: '当前循环项',
properties: {}
};
let match =
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
let field = node.schema.name || match?.[1];
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
const schema = scope?.parent?.getSchemaByPath(field);
if (isObject(schema?.items)) {
dataSchema = {
...dataSchema,
...(schema!.items as any)
};
// 循环添加索引方便渲染序号
set(dataSchema, 'properties.index', {
type: 'number',
title: '索引'
});
}
}
buildEditorContextMenu(
{id, schema, region, info, selections}: ContextMenuEventContext,
menus: Array<ContextMenuItem>
) {
if (selections.length || info?.plugin !== this) {
return;
}
if (info.renderer.name === 'each') {
menus.push('|', {
label: '配置成员渲染器',
onSelect: this.editDetail.bind(this, id)
});
}
}
editDetail(id: string) {
const manager = this.manager;
const store = manager.store;
const node = store.getNodeById(id);
const value = store.getValueOf(id);
node &&
value &&
this.manager.openSubEditor({
title: '配置成员渲染器',
value: schemaToArray(value.items),
slot: {
type: 'container',
body: '$$'
},
typeMutable: true,
onChange: newValue => {
newValue = {...value, items: newValue};
manager.panelChangeValue(newValue, diff(value, newValue));
},
data: {
item: 'mocked data',
index: 0
}
});
return dataSchema;
}
}

View File

@ -1,3 +1,4 @@
import {setVariable, someTree} from 'amis-core';
import {
BaseEventContext,
BasePlugin,
@ -11,11 +12,9 @@ import {
RegionConfig,
getI18nEnabled,
EditorNodeType,
EditorManager,
DSBuilderManager
EditorManager
} from 'amis-editor-core';
import {setVariable, someTree} from 'amis-core';
import {DSBuilderManager} from '../../builder/DSBuilderManager';
import {ValidatorTag} from '../../validator';
import {
getArgsWrapper,
@ -240,11 +239,11 @@ export class ComboControlPlugin extends BasePlugin {
panelJustify = true;
dsBuilderManager: DSBuilderManager;
dsManager: DSBuilderManager;
constructor(manager: EditorManager) {
super(manager);
this.dsBuilderManager = new DSBuilderManager('combo', 'api');
this.dsManager = new DSBuilderManager(manager);
}
panelBodyCreator = (context: BaseEventContext) => {
@ -750,14 +749,11 @@ export class ComboControlPlugin extends BasePlugin {
(target.parent.isRegion && target.parent.region === 'items')
) {
scope = scopeNode.parent.parent;
builder = this.dsBuilderManager.resolveBuilderBySchema(
scope.schema,
'api'
);
builder = this.dsManager.getBuilderBySchema(scope.schema);
}
if (builder && scope.schema.api) {
return builder.getAvailableContextFileds(
return builder.getAvailableContextFields(
{
schema: scope.schema,
sourceKey: 'api',

File diff suppressed because it is too large Load Diff

View File

@ -15,10 +15,10 @@ import {
repeatArray,
mockValue,
EditorNodeType,
EditorManager,
DSBuilderManager
EditorManager
} from 'amis-editor-core';
import {getTreeAncestors, setVariable, someTree} from 'amis-core';
import {setVariable, someTree} from 'amis-core';
import {DSBuilderManager} from '../../builder/DSBuilderManager';
import {ValidatorTag} from '../../validator';
import {
getEventControlConfig,
@ -814,11 +814,11 @@ export class TableControlPlugin extends BasePlugin {
}
];
dsBuilderManager: DSBuilderManager;
dsManager: DSBuilderManager;
constructor(manager: EditorManager) {
super(manager);
this.dsBuilderManager = new DSBuilderManager('input-table', 'api');
this.dsManager = new DSBuilderManager(manager);
}
panelBodyCreator = (context: BaseEventContext) => {
@ -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));
}
});
@ -1197,14 +1199,11 @@ export class TableControlPlugin extends BasePlugin {
(target.parent.isRegion && target.parent.region === 'columns')
) {
scope = scopeNode.parent.parent;
builder = this.dsBuilderManager.resolveBuilderBySchema(
scope.schema,
'api'
);
builder = this.dsManager.getBuilderBySchema(scope.schema);
}
if (builder && scope.schema.api) {
return builder.getAvailableContextFileds(
return builder.getAvailableContextFields(
{
schema: scope.schema,
sourceKey: 'api',

View File

@ -90,6 +90,14 @@ export class ImagePlugin extends BasePlugin {
label: '缩略图地址',
description: '如果已绑定字段名,可以不用设置,支持用变量。'
}),
getSchemaTpl('backgroundImageUrl', {
name: 'editorSetting.mock.src',
label: tipedLabel(
'假数据图片',
'只在编辑区显示的模拟图片,运行时将显示图片实际内容'
)
}),
{
type: 'ae-switch-more',
mode: 'normal',

View File

@ -232,6 +232,7 @@ export class FlexPluginBase extends LayoutBasePlugin {
const isFlexItem = this.manager?.isFlexItem(id);
const isFlexColumnItem = this.manager?.isFlexColumnItem(id);
const newColumnSchema = defaultFlexColumnSchema('新的一列');
const canAppendSiblings = this.manager?.canAppendSiblings();
const toolbarsTooltips: any = {};
toolbars.forEach(toolbar => {
@ -245,7 +246,8 @@ export class FlexPluginBase extends LayoutBasePlugin {
(info.renderer?.name === 'flex' || info.renderer?.name === 'container') &&
!isFlexItem && // 备注:如果是列级元素就不需要显示了
!draggableContainer &&
!schema?.isFreeContainer
!schema?.isFreeContainer &&
canAppendSiblings
) {
// 非特殊布局元素fixed、absolute支持前后插入追加布局元素功能icon
if (!toolbarsTooltips['上方插入布局容器']) {
@ -294,7 +296,7 @@ export class FlexPluginBase extends LayoutBasePlugin {
}
}
if (isFlexItem && !draggableContainer) {
if (isFlexItem && !draggableContainer && canAppendSiblings) {
if (
!toolbarsTooltips[`${isFlexColumnItem ? '上方' : '左侧'}插入列级容器`]
) {

View File

@ -1,7 +1,9 @@
import {registerEditorPlugin} from 'amis-editor-core';
import {BasePlugin, RegionConfig, RendererInfo} from 'amis-editor-core';
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
import {tipedLabel} from 'amis-editor-core';
import {
registerEditorPlugin,
BasePlugin,
getSchemaTpl,
tipedLabel
} from 'amis-editor-core';
export class LinkPlugin extends BasePlugin {
static id = 'LinkPlugin';
@ -37,15 +39,23 @@ export class LinkPlugin extends BasePlugin {
title: '基本',
body: [
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
{
getSchemaTpl('valueFormula', {
name: 'href',
type: 'input-text',
label: tipedLabel(
'目标地址',
'支持取变量,如果已绑定字段名,可以不用设置'
)
),
rendererSchema: {
type: 'input-text'
}
}),
{
label: tipedLabel('内容', '不填写时,自动使用目标地址值'),
type: 'ae-textareaFormulaControl',
mode: 'normal',
pipeIn: (value: any, data: any) => value || (data && data.html),
name: 'body'
},
getSchemaTpl('inputBody'),
getSchemaTpl('switch', {
name: 'blank',
label: '在新窗口打开'

View File

@ -1,6 +1,10 @@
import {Button} from 'amis';
import {Button, isObject} from 'amis';
import React from 'react';
import {getI18nEnabled, registerEditorPlugin} from 'amis-editor-core';
import {
EditorNodeType,
getI18nEnabled,
registerEditorPlugin
} from 'amis-editor-core';
import {
BaseEventContext,
BasePlugin,
@ -13,6 +17,7 @@ import {
} from 'amis-editor-core';
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
import {diff, JSONPipeOut, repeatArray} from 'amis-editor-core';
import set from 'lodash/set';
import {
schemaArrayFormat,
resolveArrayDatasource,
@ -28,6 +33,8 @@ export class ListPlugin extends BasePlugin {
// 组件名称
name = '列表';
isBaseComponent = true;
isListComponent = true;
disabledRendererPlugin = true;
description =
'展示一个列表,可以自定标题、副标题,内容及按钮组部分。当前组件需要配置数据源,不自带数据拉取,请优先使用 「CRUD」 组件。';
docLink = '/amis/zh-CN/components/list';
@ -64,7 +71,7 @@ export class ListPlugin extends BasePlugin {
panelTitle = '列表';
panelJustify = true;
panelBodyCreator = (context: BaseEventContext) => {
const isCRUDBody = context.schema.type === 'crud';
const isCRUDBody = ['crud', 'crud2'].includes(context.schema.type);
const i18nEnabled = getI18nEnabled();
return getSchemaTpl('tabs', [
{
@ -73,21 +80,21 @@ export class ListPlugin extends BasePlugin {
{
title: '基本',
body: [
{
children: (
<Button
level="primary"
size="sm"
block
onClick={this.editDetail.bind(this, context.id)}
>
</Button>
)
},
{
type: 'divider'
},
// {
// children: (
// <Button
// level="primary"
// size="sm"
// block
// onClick={this.editDetail.bind(this, context.id)}
// >
// 配置成员详情
// </Button>
// )
// },
// {
// type: 'divider'
// },
{
name: 'title',
type: i18nEnabled ? 'input-text-i18n' : 'input-text',
@ -95,8 +102,8 @@ export class ListPlugin extends BasePlugin {
},
isCRUDBody
? null
: getSchemaTpl('sourceBindControl', {
label: '数据源'
: getSchemaTpl('formItemName', {
label: '绑定字段名'
}),
{
name: 'placeholder',
@ -210,7 +217,8 @@ export class ListPlugin extends BasePlugin {
const {$schema, ...rest} = props;
return {
...JSONPipeOut(rest),
// ...JSONPipeOut(rest),
...rest,
$schema
};
}
@ -326,6 +334,36 @@ export class ListPlugin extends BasePlugin {
}
}
buildDataSchemas(node: EditorNodeType, region?: EditorNodeType) {
let dataSchema: any = {
$id: 'each',
type: 'object',
title: '当前循环项',
properties: {}
};
let match =
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
let field = node.schema.name || match?.[1];
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
const schema = scope?.parent?.getSchemaByPath(field);
if (isObject(schema?.items)) {
dataSchema = {
...dataSchema,
...(schema!.items as any)
};
// 循环添加序号方便处理
set(dataSchema, 'properties.index', {
type: 'number',
title: '序号'
});
}
return dataSchema;
}
buildEditorContextMenu(
{id, schema, region, info, selections}: ContextMenuEventContext,
menus: Array<ContextMenuItem>
@ -352,7 +390,7 @@ export class ListPlugin extends BasePlugin {
const {renderer, schema} = context;
if (
!schema.$$id &&
schema.$$editor?.renderer.name === 'crud' &&
['crud', 'crud2'].includes(schema.$$editor?.renderer.name) &&
renderer.name === 'list'
) {
return {

View File

@ -0,0 +1,459 @@
import {Button, JSONValueMap, isObject} from 'amis';
import React from 'react';
import {EditorNodeType, registerEditorPlugin} from 'amis-editor-core';
import {
BaseEventContext,
BasePlugin,
BasicRendererInfo,
PluginInterface,
RendererInfoResolveEventContext
} from 'amis-editor-core';
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
import {repeatArray} from 'amis-editor-core';
import set from 'lodash/set';
import {escapeFormula, resolveArrayDatasource} from '../util';
export class List2Plugin extends BasePlugin {
static id = 'List2Plugin';
static scene = ['layout'];
// 关联渲染器名字
rendererName = 'cards';
$schema = '/schemas/CardsSchema.json';
// 组件名称
name = '列表';
isBaseComponent = true;
isListComponent = true;
description =
'功能类似于表格,但是用一个个小卡片来展示数据。当前组件需要配置数据源,不自带数据拉取,请优先使用 「CRUD」 组件。';
docLink = '/amis/zh-CN/components/cards';
tags = ['展示'];
icon = 'fa fa-window-maximize';
pluginIcon = 'cards-plugin';
scaffold = {
type: 'cards',
columnsCount: 1,
card: {
type: 'container',
body: [
{
type: 'container',
body: [
{
type: 'flex',
items: [
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '01',
inline: true,
wrapperComponent: '',
style: {
color: 'var(--colors-neutral-text-2)',
fontSize: 'var(--fonts-size-3)',
fontWeight: 'var(--fonts-weight-5)',
marginRight: '10px'
}
},
{
type: 'tpl',
tpl: '/',
inline: true,
wrapperComponent: '',
style: {
marginRight: '10px',
fontSize: 'var(--fonts-size-3)',
color: '#cccccc'
},
id: 'u:95d2a3ac3e70'
},
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '3月',
inline: true,
wrapperComponent: '',
style: {
fontSize: 'var(--fonts-size-6)'
}
},
{
type: 'tpl',
tpl: '2023',
inline: true,
wrapperComponent: '',
style: {
fontSize: 'var(--fonts-size-6)'
}
}
],
style: {
position: 'static',
display: 'flex',
flexWrap: 'nowrap',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false
}
],
size: 'none',
style: {
'position': 'static',
'display': 'flex',
'flex': '1 1 auto',
'flexGrow': 0,
'flexBasis': 'auto',
'flexWrap': 'nowrap',
'justifyContent': 'flex-start',
'alignItems': 'center',
'paddingLeft': '20px',
'paddingRight': '40px',
'right-border-width': 'var(--borders-width-2)',
'right-border-style': 'var(--borders-style-2)',
'right-border-color': '#ececec',
'marginRight': '40px'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false
},
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '列表标题',
inline: true,
wrapperComponent: '',
style: {
fontSize: 'var(--fonts-size-5)',
color: 'var(--colors-neutral-text-4)',
fontWeight: 'var(--fonts-weight-4)',
marginBottom: '10px'
},
maxLine: 1,
id: 'u:105ca9cda3ef'
},
{
type: 'tpl',
tpl: '这是内容简介,可以设置显示行数',
inline: true,
wrapperComponent: '',
maxLine: 1,
style: {
fontSize: '13px',
color: 'var(--colors-neutral-text-5)'
}
}
],
size: 'none',
style: {
position: 'static',
display: 'flex',
flex: '1 1 auto',
flexGrow: 1,
flexBasis: 'auto',
flexWrap: 'nowrap',
flexDirection: 'column',
alignItems: 'flex-start'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false
},
{
type: 'container',
body: [
{
type: 'button',
label: '查看详情',
onEvent: {
click: {
actions: []
}
},
level: 'default',
size: 'default',
editorState: 'default',
themeCss: {
className: {
'border:default': {
'top-border-width': 'var(--borders-width-2)',
'left-border-width': 'var(--borders-width-2)',
'right-border-width': 'var(--borders-width-2)',
'bottom-border-width': 'var(--borders-width-2)',
'top-border-style': 'var(--borders-style-2)',
'left-border-style': 'var(--borders-style-2)',
'right-border-style': 'var(--borders-style-2)',
'bottom-border-style': 'var(--borders-style-2)',
'top-border-color': 'var(--colors-brand-6)',
'left-border-color': 'var(--colors-brand-6)',
'right-border-color': 'var(--colors-brand-6)',
'bottom-border-color': 'var(--colors-brand-6)'
},
'padding-and-margin:default': {
paddingLeft: '20px',
paddingRight: '20px'
},
'radius:default': {
'top-left-border-radius': '20px',
'top-right-border-radius': '20px',
'bottom-left-border-radius': '20px',
'bottom-right-border-radius': '20px'
},
'font:default': {
color: 'var(--colors-brand-6)'
}
}
}
}
],
size: 'xs',
style: {
position: 'static',
display: 'flex',
flex: '1 1 auto',
flexGrow: 0,
flexBasis: 'auto',
flexWrap: 'nowrap',
flexDirection: 'column',
justifyContent: 'center'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false,
id: 'u:77cb3edb2288'
}
],
style: {
position: 'relative'
}
}
],
size: 'none',
style: {
'position': 'static',
'display': 'block',
'overflowY': 'auto',
'overflowX': 'auto',
'paddingTop': '10px',
'paddingRight': '10px',
'paddingBottom': '10px',
'paddingLeft': '10px',
'radius': {
'top-left-border-radius': '6px',
'top-right-border-radius': '6px',
'bottom-left-border-radius': '6px',
'bottom-right-border-radius': '6px'
},
'top-border-width': 'var(--borders-width-1)',
'left-border-width': 'var(--borders-width-1)',
'right-border-width': 'var(--borders-width-1)',
'bottom-border-width': 'var(--borders-width-1)',
'top-border-style': 'var(--borders-style-1)',
'left-border-style': 'var(--borders-style-1)',
'right-border-style': 'var(--borders-style-1)',
'bottom-border-style': 'var(--borders-style-1)',
'top-border-color': '#3be157',
'left-border-color': '#3be157',
'right-border-color': '#3be157',
'bottom-border-color': '#3be157',
'flex': '0 0 150px',
'marginRight': '15px',
'flexBasis': '100%',
'boxShadow': ' 0px 0px 10px 0px var(--colors-neutral-line-8)'
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: true,
onEvent: {
click: {
weight: 0,
actions: []
}
}
}
],
style: {
position: 'static',
display: 'flex',
width: '100%',
overflowX: 'visible',
margin: '0',
flexWrap: 'nowrap',
justifyContent: 'space-between'
},
isFixedHeight: false,
isFixedWidth: true,
wrapperBody: false
},
placeholder: '',
name: 'items',
style: {
gutterY: 10
}
};
previewSchema = {
...this.scaffold,
className: 'text-left '
};
panelTitle = '列表';
panelJustify = true;
panelBodyCreator = (context: BaseEventContext) => {
const isCRUDBody = context.schema.type === 'crud';
const curPosition = context?.schema?.style?.position;
const isAbsolute = curPosition === 'fixed' || curPosition === 'absolute';
return [
getSchemaTpl('tabs', [
{
title: '属性',
body: getSchemaTpl('collapseGroup', [
{
title: '基本',
body: [
{
type: 'input-text',
label: '组件名称',
name: 'editorSetting.displayName'
},
isCRUDBody
? null
: getSchemaTpl('formItemName', {
label: '绑定字段名'
}),
getSchemaTpl('cardsPlaceholder')
]
},
getSchemaTpl('status')
])
},
{
title: '外观',
body: getSchemaTpl('collapseGroup', [
{
title: '组件',
body: [
{
name: 'columnsCount',
type: 'input-range',
visibleOn: '!this.leftFixed',
min: 1,
max: 12,
step: 1,
label: '每行个数'
},
{
type: 'input-number',
label: '左右间距',
name: 'style.gutterX',
visibleOn: 'this.columnsCount > 1'
},
{
type: 'input-number',
label: '上下间距',
name: 'style.gutterY'
},
getSchemaTpl('layout:originPosition', {
visibleOn: isAbsolute ? isAbsolute : undefined,
value: 'left-top'
})
]
},
...getSchemaTpl('theme:common', {exclude: ['layout']})
])
}
])
];
};
buildDataSchemas(node: EditorNodeType, region: EditorNodeType) {
let dataSchema: any = {
$id: 'cards',
type: 'object',
title: '当前列表项',
properties: {}
};
let match =
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
let field = node.schema.name || match?.[1];
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
const schema = scope?.parent?.getSchemaByPath(field);
if (isObject(schema?.items)) {
dataSchema = {
...dataSchema,
...(schema!.items as any)
};
// 列表添加序号方便处理
set(dataSchema, 'properties.index', {
type: 'number',
title: '索引'
});
}
return dataSchema;
}
filterProps(props: any) {
// 编辑时显示两行假数据
const count = (props.columnsCount || 3) * 2;
props.value = repeatArray({}, count).map((item, index) => {
return {
...item,
id: index + 1
};
});
props.className = `${props.className || ''} ae-Editor-list`;
props.itemsClassName = `${props.itemsClassName || ''} cards-items`;
if (props.card && !props.card.className?.includes('listItem')) {
props.card.className = `${props.card.className || ''} ae-Editor-listItem`;
}
// 列表类型内的文本元素显示原始公式
props = escapeFormula(props);
return props;
}
getRendererInfo(
context: RendererInfoResolveEventContext
): BasicRendererInfo | void {
const plugin: PluginInterface = this;
const {renderer, schema} = context;
if (
!schema.$$id &&
schema.$$editor?.renderer.name === 'crud' &&
renderer.name === 'cards'
) {
return {
...({id: schema.$$editor.id} as any),
name: plugin.name!,
regions: plugin.regions,
patchContainers: plugin.patchContainers,
vRendererConfig: plugin.vRendererConfig,
wrapperProps: plugin.wrapperProps,
wrapperResolve: plugin.wrapperResolve,
filterProps: plugin.filterProps,
$schema: plugin.$schema,
renderRenderer: plugin.renderRenderer
};
}
return super.getRendererInfo(context);
}
}
registerEditorPlugin(List2Plugin);

View File

@ -58,7 +58,6 @@ export class OperationPlugin extends BasePlugin {
{
children: (
<Button
size="sm"
block
className="m-b-sm ae-Button--enhance"
onClick={() => {

View File

@ -1,15 +1,14 @@
import {registerEditorPlugin} from 'amis-editor-core';
import {
BasePlugin,
RegionConfig,
BaseEventContext,
tipedLabel
tipedLabel,
defaultValue,
getSchemaTpl,
registerEditorPlugin
} from 'amis-editor-core';
import {ValidatorTag} from '../validator';
import sortBy from 'lodash/sortBy';
import {getEventControlConfig} from '../renderer/event-control/helper';
import {RendererPluginEvent} from 'amis-editor-core';
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
export class PaginationPlugin extends BasePlugin {
static id = 'PaginationPlugin';
@ -20,7 +19,6 @@ export class PaginationPlugin extends BasePlugin {
// 组件名称
name = '分页组件';
isBaseComponent = true;
disabledRendererPlugin = true;
description = '分页组件,可以对列表进行分页展示,提高页面性能';
tags = ['容器'];
icon = 'fa fa-window-minimize';
@ -109,7 +107,7 @@ export class PaginationPlugin extends BasePlugin {
'启用功能',
'选中表示启用该项,可以拖拽排序调整功能的顺序'
),
visibleOn: 'data.mode === "normal"',
visibleOn: '!data.mode || data.mode === "normal"',
mode: 'normal',
multiple: true,
multiLine: false,
@ -124,7 +122,8 @@ export class PaginationPlugin extends BasePlugin {
{
type: 'checkbox',
name: 'checked',
className: 'm-t-n-xxs'
className: 'm-t-n-xxs',
inputClassName: 'p-t-none'
},
{
type: 'tpl',
@ -133,15 +132,31 @@ 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: string) => v === item.value
);
return ~idx ? idx : Infinity;
}
]
);
// return this.layoutOptions.map(v => ({
// ...v,
// checked: value.includes(v.value)
// }));
},
pipeOut: (value: any[]) => {
this.lastLayoutSetting = value
@ -163,7 +178,7 @@ export class PaginationPlugin extends BasePlugin {
type: 'combo',
label: '每页条数选项',
visibleOn:
'data.mode === "normal" && data.layout && data.layout.includes("perPage")',
'(!data.mode || data.mode === "normal") && data.layout && data.layout.includes("perPage")',
mode: 'normal',
multiple: true,
multiLine: false,
@ -185,15 +200,18 @@ export class PaginationPlugin extends BasePlugin {
return value?.map(v => ({value: v})) || [10];
},
pipeOut: (value: any[]) => {
return value.map(v => v.value);
const pages = value.map(v => v.value);
return pages.map(
page => page || Math.max(...pages.filter(Boolean)) + 5
);
}
}),
{
name: 'perPage',
type: 'input-text',
type: 'input-number',
label: '默认每页条数',
visibleOn:
'data.mode === "normal" && data.layout?.includes("perPage")'
'(!data.mode || data.mode === "normal") && data.layout?.includes("perPage")'
},
{
name: 'maxButtons',
@ -205,13 +223,17 @@ export class PaginationPlugin extends BasePlugin {
min: 5,
max: 20,
pipeOut: (value: any) => value || 5,
visibleOn: 'data.mode === "normal"'
visibleOn: '!data.mode || data.mode === "normal"'
}
]
},
{
title: '状态',
body: [getSchemaTpl('disabled')]
body: [
getSchemaTpl('disabled'),
getSchemaTpl('hidden'),
getSchemaTpl('visible')
]
}
])
},

View File

@ -1,9 +1,10 @@
import React from 'react';
import {registerEditorPlugin} from 'amis-editor-core';
import {
registerEditorPlugin,
BaseEventContext,
BasePlugin,
RendererPluginEvent
RendererPluginEvent,
RendererPluginAction
} from 'amis-editor-core';
import {getSchemaTpl} from 'amis-editor-core';
import {getEventControlConfig} from '../renderer/event-control/helper';
@ -18,6 +19,7 @@ export class SearchBoxPlugin extends BasePlugin {
// 组件名称
name = '搜索框';
searchKeywords = '搜索框、searchbox';
isBaseComponent = true;
description =
'用于展示一个简单搜索框,通常需要搭配其他组件使用。比如 page 配置 initApi 后可以用来实现简单数据过滤查找name keywords 会作为参数传递给 page 的 initApi。';
@ -28,6 +30,7 @@ export class SearchBoxPlugin extends BasePlugin {
scaffold: Schema = {
type: 'search-box',
name: 'keyword',
body: {
type: 'tpl',
tpl: '搜索框',
@ -137,6 +140,19 @@ export class SearchBoxPlugin extends BasePlugin {
}
];
actions: RendererPluginAction[] = [
{
actionType: 'clear',
actionLabel: '清空',
description: '清空输入框'
},
{
actionType: 'setValue',
actionLabel: '更新数据',
description: '更新数据'
}
];
notRenderFormZone = true;
panelTitle = '搜索框';
panelJustify = true;

View File

@ -1,53 +1,81 @@
import {Button} from 'amis';
import React from 'react';
import {render as amisRender} from 'amis';
import flattenDeep from 'lodash/flattenDeep';
import {
EditorNodeType,
JSONPipeOut,
jsonToJsonSchema,
registerEditorPlugin
registerEditorPlugin,
BaseEventContext,
BasePlugin,
RegionConfig,
getSchemaTpl,
tipedLabel
} from 'amis-editor-core';
import {BaseEventContext, BasePlugin, RegionConfig} from 'amis-editor-core';
import {getSchemaTpl} from 'amis-editor-core';
import {DSBuilderManager} from '../builder/DSBuilderManager';
import {DSFeatureEnum, ModelDSBuilderKey} from '../builder';
import {getEventControlConfig} from '../renderer/event-control/helper';
import type {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core';
import type {
EditorManager,
RendererPluginAction,
RendererPluginEvent
} from 'amis-editor-core';
export class ServicePlugin extends BasePlugin {
static id = 'ServicePlugin';
// 关联渲染器名字
rendererName = 'service';
name = '服务Service';
panelTitle = '服务Service';
icon = 'fa fa-server';
pluginIcon = 'service-plugin';
panelIcon = 'service-plugin';
$schema = '/schemas/ServiceSchema.json';
// 组件名称
name = '服务 Service';
isBaseComponent = true;
order = -850;
description =
'功能性容器,可以用来加载数据或者加载渲染器配置。加载到的数据在容器可以使用。';
docLink = '/amis/zh-CN/components/service';
tags = ['数据容器'];
icon = 'fa fa-server';
pluginIcon = 'service-plugin';
scaffold = {
type: 'service',
/** region 区域的 placeholder 会撑开内容区 */
body: []
};
previewSchema = {
type: 'service',
body: [
{
type: 'tpl',
tpl: '内容',
wrapperComponent: '',
inline: false
tpl: '内容区域',
inline: false,
className: 'bg-light wrapper'
}
]
};
previewSchema = {
type: 'tpl',
wrapperComponent: '',
tpl: '功能性组件,用于数据拉取。'
};
regions: Array<RegionConfig> = [
{
key: 'body',
label: '内容区'
label: '内容区',
placeholder: amisRender({
type: 'wrapper',
size: 'lg',
body: {type: 'tpl', tpl: '内容区域'}
})
}
];
@ -149,9 +177,76 @@ export class ServicePlugin extends BasePlugin {
}
];
panelTitle = '服务';
dsManager: DSBuilderManager;
constructor(manager: EditorManager) {
super(manager);
this.dsManager = new DSBuilderManager(manager);
}
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);
}
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
}
}
}
: {}),
useFieldManager: builderKey === ModelDSBuilderKey
}
})
])
};
}
);
return getSchemaTpl('tabs', [
{
title: '属性',
@ -162,112 +257,56 @@ export class ServicePlugin extends BasePlugin {
title: '基本',
body: [
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
getSchemaTpl('name'),
{
children: (
<Button
level="info"
size="sm"
className="m-b-sm"
block
onClick={() => {
// this.manager.showInsertPanel('body', context.id);
this.manager.showRendererPanel('');
}}
>
</Button>
)
}
]
},
{
title: '数据接口',
body: [
getSchemaTpl('apiControl', {
name: 'api',
label: '数据接口',
messageDesc:
'设置 service 默认提示信息,当 service 没有返回 msg 信息时有用,如果 service 返回携带了 msg 值,则还是以 service 返回为主'
}),
{
name: 'ws',
type: 'input-text',
label: 'WebSocket 实时更新接口'
},
/** initFetchOn可以通过api的sendOn属性控制 */
getSchemaTpl('switch', {
name: 'initFetch',
label: '数据接口初始加载',
visibleOn: 'this.api'
}),
{
name: 'interval',
label: '定时刷新间隔',
visibleOn: 'this.api',
type: 'input-number',
step: 500,
description: '设置后将自动定时刷新,单位 ms'
},
getSchemaTpl('switch', {
name: 'silentPolling',
label: '静默加载',
visibleOn: '!!data.interval',
description: '设置自动定时刷新是否显示加载动画'
}),
{
name: 'stopAutoRefreshWhen',
label: '停止定时刷新检测',
type: 'input-text',
visibleOn: '!!data.interval',
description:
'定时刷新一旦设置会一直刷新,除非给出表达式,条件满足后则不刷新了。'
}
]
},
{
title: 'Schema接口',
body: [
getSchemaTpl('apiControl', {
name: 'schemaApi',
label: '内容 Schema 接口'
}),
getSchemaTpl('switch', {
name: 'initFetchSchema',
label: 'Schema接口初始加载',
visibleOn: 'this.schemaApi'
})
]
},
{
title: '全局配置',
body: [
getSchemaTpl('loadingConfig', {}, {context}),
getSchemaTpl('data'),
{
type: 'js-editor',
allowFullscreen: true,
name: 'dataProvider',
label: '自定义函数获取数据',
description: '将会传递 data 和 setData 两个参数'
},
{
label: '默认消息信息',
type: 'combo',
name: 'messages',
multiLine: true,
description:
'设置 service 默认提示信息,当 service 没有返回 msg 信息时有用,如果 service 返回携带了 msg 值,则还是以 service 返回为主',
items: [
getSchemaTpl('fetchSuccess'),
getSchemaTpl('fetchFailed')
]
}
dsTypeSelect(),
...dsSetting
]
},
{
title: '状态',
body: [getSchemaTpl('ref'), getSchemaTpl('visible')]
body: [getSchemaTpl('hidden')]
},
{
title: '高级',
body: [
getSchemaTpl('combo-container', {
type: 'input-kv',
mode: 'normal',
name: 'data',
label: '初始化静态数据'
}),
getSchemaTpl('apiControl', {
name: 'schemaApi',
label: tipedLabel(
'Schema数据源',
'配置schemaApi后可以实现动态渲染页面内容'
)
}),
getSchemaTpl('initFetch', {
name: 'initFetchSchema',
label: '是否Schema初始加载',
visibleOn:
'typeof this.schemaApi === "string" ? this.schemaApi : this.schemaApi && this.schemaApi.url'
}),
{
name: 'ws',
type: 'input-text',
label: tipedLabel(
'WebSocket接口',
'Service 支持通过WebSocket(ws)获取数据,用于获取实时更新的数据。'
)
},
{
type: 'js-editor',
allowFullscreen: true,
name: 'dataProvider',
label: tipedLabel(
'自定义函数获取数据',
'对于复杂的数据获取情况,可以使用外部函数获取数据'
),
placeholder:
'/**\n * @param data 上下文数据\n * @param setData 更新数据的函数\n * @param env 环境变量\n */\ninterface DataProvider {\n (data: any, setData: (data: any) => void, env: any): void;\n}\n'
}
]
}
])
]
@ -289,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,
@ -331,6 +393,25 @@ export class ServicePlugin extends BasePlugin {
scope?.addSchema(jsonschema);
}
}
async getAvailableContextFields(
scopeNode: EditorNodeType,
node: EditorNodeType,
region?: EditorNodeType
) {
const builder = this.dsManager.getBuilderBySchema(scopeNode.schema);
if (builder && scopeNode.schema.api) {
return builder.getAvailableContextFields(
{
schema: scopeNode.schema,
sourceKey: 'api',
feat: DSFeatureEnum.List
},
node
);
}
}
}
registerEditorPlugin(ServicePlugin);

View File

@ -6,7 +6,6 @@ import {
RendererPluginEvent
} from 'amis-editor-core';
import {findTree, setVariable, someTree} from 'amis-core';
import {registerEditorPlugin, repeatArray, diff} from 'amis-editor-core';
import {
BasePlugin,
@ -19,6 +18,7 @@ import {
InsertEventContext,
ScaffoldForm
} from 'amis-editor-core';
import {DSBuilderManager} from '../builder/DSBuilderManager';
import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core';
import {mockValue} from 'amis-editor-core';
import {EditorNodeType} from 'amis-editor-core';
@ -34,6 +34,8 @@ import {
} from '../util';
import {reaction} from 'mobx';
import type {EditorManager} from 'amis-editor-core';
export class TablePlugin extends BasePlugin {
static id = 'TablePlugin';
// 关联渲染器名字
@ -41,7 +43,7 @@ export class TablePlugin extends BasePlugin {
$schema = '/schemas/TableSchema.json';
// 组件名称
name = '表格';
name = '原子表格';
tags = ['展示'];
isBaseComponent = true;
description =
@ -433,7 +435,16 @@ export class TablePlugin extends BasePlugin {
description: '开启表格拖拽排序功能'
}
];
panelJustify = true;
dsManager: DSBuilderManager;
constructor(manager: EditorManager) {
super(manager);
this.dsManager = new DSBuilderManager(manager);
}
panelBodyCreator = (context: BaseEventContext) => {
const isCRUDBody = context.schema.type === 'crud';
const i18nEnabled = getI18nEnabled();
@ -888,6 +899,38 @@ export class TablePlugin extends BasePlugin {
};
}
async getAvailableContextFields(
scopeNode: EditorNodeType,
node: EditorNodeType,
region?: EditorNodeType
) {
if (node?.info?.renderer?.name === 'table-cell') {
if (
scopeNode.parent?.type === 'service' &&
scopeNode.parent?.parent?.path?.endsWith('service')
) {
return scopeNode.parent.parent.info.plugin.getAvailableContextFields?.(
scopeNode.parent.parent,
node,
region
);
}
}
const builder = this.dsManager.getBuilderBySchema(scopeNode.schema);
if (builder && scopeNode.schema.api) {
return builder.getAvailableContextFields(
{
schema: scopeNode.schema,
sourceKey: 'api',
feat: 'List'
},
node
);
}
}
editHeaderDetail(id: string) {
const manager = this.manager;
const store = manager.store;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -189,7 +189,13 @@ export class TagPlugin extends BasePlugin {
{
title: '基本',
body: [
getSchemaTpl('label'),
getSchemaTpl('valueFormula', {
name: 'label',
label: '标签内容',
rendererSchema: {
type: 'input-text'
}
}),
{
type: 'button-group-select',
label: '模式',

View File

@ -230,7 +230,23 @@ export class TplPlugin extends BasePlugin {
pipeIn: defaultValue(true),
hiddenOn: 'data.wrapperComponent !== ""'
}),
{
type: 'input-number',
label: '最大显示行数',
name: 'maxLine',
min: 0
},
getSchemaTpl('tpl:content'),
{
type: 'textarea',
name: 'editorSetting.mock.tpl',
mode: 'vertical',
label: tipedLabel(
'填充假数据',
'只在编辑区显示的假数据文本,运行时将显示文本实际内容'
),
pipeOut: (value: any) => (value === '' ? undefined : value)
},
getSchemaTpl('tpl:rich-text')
]
},

View File

@ -12,12 +12,7 @@ export * from './Tabs'; // 选项卡
// 数据容器
export * from './CRUD'; // 增删改查
export {
TableCRUDPlugin,
ListCRUDPlugin,
CardsCRUDPlugin,
CRUDPlugin as CRUD2Plugin
} from './CRUD2';
export * from './CRUD2/CRUDTable'; // 增删改查v2.0
export * from './Form/Form'; // 表单
export * from './Service'; // 服务service
@ -91,6 +86,7 @@ export * from './Tpl'; // 文字
export * from './Icon'; // 图标
export * from './Link'; // 链接
export * from './List'; // 列表
export * from './List2'; // 列表
export * from './Mapping'; // 映射
export * from './Avatar'; // 头像
export * from './Card'; // 卡片

View File

@ -15,11 +15,12 @@ import debounce from 'lodash/debounce';
import remove from 'lodash/remove';
import React from 'react';
import {EditorManager, EditorNodeType, autobind} from 'amis-editor-core';
import type {DSField, DSFieldGroup} from 'amis-editor-core';
import {matchSorter} from 'match-sorter';
import type {SchemaCollection} from 'amis';
import {default as cx} from 'classnames';
import type {SchemaCollection} from 'amis';
import type {DSField, DSFieldGroup} from '../builder';
export interface DataBindingProps extends FormControlProps {
node: EditorNodeType;
manager: EditorManager;

View File

@ -4,15 +4,18 @@
import React from 'react';
import {findDOMNode} from 'react-dom';
import Sortable from 'sortablejs';
import cx from 'classnames';
import {FormItem, Button, Icon, FormControlProps, autobind} from 'amis';
import clone from 'lodash/clone';
import remove from 'lodash/remove';
import isPlainObject from 'lodash/isPlainObject';
import {FormItem, Button, Icon, FormControlProps, autobind} from 'amis';
import {Checkbox} from 'amis-ui';
import {evalExpression} from 'amis-core';
import {GoConfigControl} from './GoConfigControl';
import Sortable from 'sortablejs';
const klass = 'ae-FeatureControl';
export type FeatureOption = {
label: string;
value: any;
@ -28,9 +31,16 @@ interface FeatureControlProps extends FormControlProps {
addable?: boolean;
addText?: string;
sortable?: boolean;
checkable?: boolean;
checkableOn?: string;
features: Array<FeatureOption> | ((schema: any) => Array<FeatureOption>);
goFeatureComp?: (item: FeatureOption) => string; // 去子组件
onSort?: (value: FeatureOption[]) => void;
goFeatureComp?: (item: FeatureOption, index: number) => string; // 去子组件
onSort?: (data: any, value: {oldIndex: number; newIndex: number}) => void;
// 自定义添加内容,按钮变成普通按钮
customAction?: (props: {schema: any; onBulkChange: any}) => any;
onItemCheck?: (checked: boolean, index: number, schema: any) => void;
// 所有都添加完成后,隐藏添加按钮
hideAddWhenAll?: boolean;
}
interface FeatureControlState {
@ -97,7 +107,6 @@ export default class FeatureControl extends React.Component<
handleRemove(item: FeatureOption, index: number) {
const {removeFeature, data, onBulkChange} = this.props;
const {inUseFeat, unUseFeat} = this.state;
item.remove?.(data);
removeFeature?.(item, data);
onBulkChange?.(data);
@ -108,6 +117,12 @@ export default class FeatureControl extends React.Component<
this.setState({inUseFeat, unUseFeat});
}
handleSort(e: any) {
const {data, onBulkChange, onSort} = this.props;
onSort?.(data, e);
onBulkChange?.(data);
}
@autobind
handleAdd(item: any) {
const {addFeature, data, onBulkChange} = this.props;
@ -173,7 +188,10 @@ export default class FeatureControl extends React.Component<
const value = this.state.inUseFeat.concat();
value[e.oldIndex] = value.splice(e.newIndex, 1, value[e.oldIndex])[0];
this.setState({inUseFeat: value}, () => {
this.props.onSort?.(value);
this.handleSort({
oldIndex: e.oldIndex,
newIndex: e.newIndex
});
});
}
}
@ -187,8 +205,24 @@ export default class FeatureControl extends React.Component<
this.sortable && this.sortable.destroy();
}
renderItem(item: FeatureOption, index: number) {
const {sortable, goFeatureComp, node, manager} = this.props;
@autobind
handleCheck(res: boolean, index: number) {
const {data, onBulkChange, onItemCheck} = this.props;
const schema = clone(data);
onItemCheck?.(res, index, schema);
onBulkChange?.(schema);
}
renderItem(item: FeatureOption, index: number, checkable: boolean) {
const {
sortable,
goFeatureComp,
node,
manager,
onItemCheck,
isItemChecked,
data
} = this.props;
let content = null;
@ -199,7 +233,7 @@ export default class FeatureControl extends React.Component<
className={cx(`${klass}Item-go`)}
label={item.label}
manager={manager}
compId={() => goFeatureComp(item)}
compId={() => goFeatureComp(item, index)}
/>
);
} else {
@ -208,12 +242,21 @@ export default class FeatureControl extends React.Component<
return (
<li className={klass + 'Item'} key={index}>
{sortable && (
<a className={klass + 'Item-dragBar'}>
<Icon icon="drag-bar" className="icon" />
</a>
{checkable && onItemCheck && (
<Checkbox
checked={isItemChecked(item, index, data)}
onChange={(val: any) => this.handleCheck(val, index)}
/>
)}
{content}
<div className={klass + 'Item-content'}>
{sortable && (
<a className={klass + 'Item-dragBar'}>
<Icon icon="drag-bar" className="icon" />
</a>
)}
{content}
</div>
<Button
className={klass + 'Item-action'}
onClick={() => this.handleRemove(item, index)}
@ -225,11 +268,31 @@ export default class FeatureControl extends React.Component<
}
renderAction() {
const {addable, addText, render} = this.props;
const {
addable,
addText,
render,
customAction,
data,
onBulkChange,
hideAddWhenAll
} = this.props;
if (!addable) {
return null;
}
if (customAction && typeof customAction === 'function') {
const schema = customAction({onBulkChange, schema: clone(data)});
if (isPlainObject(schema) && typeof schema.type === 'string') {
return render('custom-action', schema);
}
}
if (hideAddWhenAll && !this.state.unUseFeat.length) {
return null;
}
return render('action', {
type: 'dropdown-button',
closeOnClick: true,
@ -252,13 +315,21 @@ export default class FeatureControl extends React.Component<
}
render() {
const {className} = this.props;
const {className, checkable, checkableOn, data} = this.props;
let isCheckable = false;
if (checkable !== undefined) {
isCheckable = checkable;
} else if (checkableOn) {
isCheckable = evalExpression(checkableOn, data) === true;
}
return (
<div className={cx('ae-FeatureControl', className)}>
<ul className={cx('ae-FeatureControl-features')} ref={this.dragRef}>
{this.state.inUseFeat.map((item, index) =>
this.renderItem(item, index)
this.renderItem(item, index, isCheckable)
)}
</ul>

View File

@ -0,0 +1,585 @@
/**
* @file FieldSetting.tsx
* @desc
*/
import React from 'react';
import {reaction} from 'mobx';
import pick from 'lodash/pick';
import {findDOMNode} from 'react-dom';
import {
FormItem,
FormControlProps,
autobind,
isValidApi,
normalizeApi
} from 'amis-core';
import {
Form,
InputTable,
Controller,
InputBox,
Select,
Button,
toast
} from 'amis-ui';
import type {IReactionDisposer} from 'mobx';
import type {InputTableColumnProps} from 'amis-ui';
import type {DSFeatureType, ScaffoldField} from '../builder/type';
interface FieldSettingProps extends FormControlProps {
/** 脚手架渲染类型 */
renderer?: string;
feat: DSFeatureType;
config: {
showInputType?: boolean;
showDisplayType?: boolean;
};
onAutoGenerateFields: (params: {
api: any;
props: FieldSettingProps;
setState: (state: any) => void;
}) => Promise<any[]>;
}
interface RowData extends ScaffoldField {}
export class FieldSetting extends React.Component<
FieldSettingProps,
{loading: boolean}
> {
static defaultProps = {
config: {
showInputType: true,
showDisplayType: true
}
};
static validator = (items: RowData[], isInternal?: boolean) => {
const cache: Record<string, boolean> = {};
const fields = items ?? [];
let error: string | boolean = false;
for (let [index, item] of fields.entries()) {
/** 提交时再校验 */
if (!item.name && isInternal !== true) {
error = `序号「${index + 1}」的字段名称不能为空`;
break;
}
if (!cache.hasOwnProperty(item.name)) {
cache[item.name] = true;
continue;
}
error = `序号「${index + 1}」的字段名称「${item.name}」不唯一`;
break;
}
return error;
};
reaction: IReactionDisposer;
dom: HTMLElement;
formRef = React.createRef<{submit: () => Promise<Record<string, any>>}>();
tableRef = React.createRef<any>();
scaffold: RowData = {
label: '',
name: '',
displayType: 'tpl',
inputType: 'input-text'
};
constructor(props: FieldSettingProps) {
super(props);
this.state = {loading: false};
this.reaction = reaction(
() => {
const ctx = props?.store?.data;
const initApi = ctx?.initApi;
const listApi = ctx?.listApi;
return `${initApi}${listApi}`;
},
() => this.forceUpdate()
);
}
componentDidMount() {
this.dom = findDOMNode(this) as HTMLElement;
}
componentWillUnmount() {
this.reaction?.();
}
@autobind
handleColumnBlur() {
this?.formRef?.current?.submit();
}
@autobind
handleSubmit(data: {items: RowData[]}) {
const {value} = this.props;
const items = (data?.items ?? []).map((field: RowData) => {
const item = value?.find((f: RowData) => f.name === field.name);
return {
...pick(
{
...item,
...field
},
['label', 'name', 'displayType', 'inputType']
),
checked: true
};
});
this.handleFieldsChange(items);
}
@autobind
async handleGenerateFields(e: React.MouseEvent<any>) {
const {
store,
renderer,
feat,
env,
manager,
data: ctx,
onAutoGenerateFields
} = this.props;
const scaffoldData = store?.data;
let api =
renderer === 'form'
? scaffoldData?.initApi
: renderer === 'crud'
? scaffoldData?.listApi
: '';
if (!api || (renderer === 'form' && feat !== 'Edit')) {
return;
}
this.setState({loading: true});
let fields: RowData[] = [];
if (onAutoGenerateFields && typeof onAutoGenerateFields === 'function') {
try {
fields = await onAutoGenerateFields({
api: api,
props: this.props,
setState: this.setState
});
} catch (error) {
toast.warning(
error.message ?? 'API返回格式不正确请查看接口响应格式要求'
);
}
} else {
const schemaFilter = manager?.store?.schemaFilter;
if (schemaFilter) {
api = schemaFilter({
api
}).api;
}
try {
const result = await env?.fetcher(api, ctx);
if (!result.ok) {
toast.warning(
result.defaultMsg ??
result.msg ??
'API返回格式不正确请查看接口响应格式要求'
);
this.setState({loading: false});
return;
}
let sampleRow: Record<string, any>;
if (feat === 'List') {
const items = result.data?.rows || result.data?.items || result.data;
sampleRow = items?.[0];
} else {
sampleRow = result.data;
}
if (sampleRow) {
Object.entries(sampleRow).forEach(([key, value]) => {
fields.push({
label: key,
name: key,
displayType: 'tpl',
inputType:
typeof value === 'number' ? 'input-number' : 'input-text',
checked: true
});
});
}
} catch (error) {
toast.warning(
error.message ?? 'API返回格式不正确请查看接口响应格式要求'
);
}
}
if (fields && fields.length > 0) {
this.handleFieldsChange(fields);
}
this.setState({loading: false});
}
@autobind
handleFieldsChange(fields: RowData[]) {
const {
onChange,
onBulkChange,
submitOnChange,
renderer,
data: ctx
} = this.props;
const isFirstStep = ctx?.__step === 0;
if (renderer === 'form') {
onChange?.(fields, submitOnChange, true);
} else {
if (isFirstStep) {
onBulkChange?.(
{
listFields: fields,
editFields: fields,
bulkEditFields: fields,
insertFields: fields,
viewFields: fields,
simpleQueryFields: fields
},
submitOnChange
);
} else {
onChange?.(fields, submitOnChange, true);
}
}
}
@autobind
renderFooter() {
const {renderer, store, data: ctx, feat} = this.props;
const scaffoldData = store?.data;
const {initApi, listApi} = scaffoldData || {};
const {loading} = this.state;
const fieldApi =
renderer === 'form' ? initApi : renderer === 'crud' ? listApi : '';
const isApiValid = isValidApi(normalizeApi(fieldApi)?.url);
const showAutoGenBtn =
(renderer === 'form' && feat === 'Edit') ||
(renderer === 'crud' && feat === 'List' && ctx?.__step === 0);
return showAutoGenBtn ? (
<>
<Button
size="sm"
level="link"
loading={loading}
disabled={!isApiValid}
disabledTip={{
content:
renderer === 'form' ? '请先填写初始化接口' : '请先填写接口',
tooltipTheme: 'dark'
}}
onClick={e => this.handleGenerateFields(e)}
>
<span></span>
</Button>
</>
) : null;
}
render() {
const {
classnames: cx,
value: formValue,
defaultValue: formDefaultValue,
env,
renderer,
config,
data: ctx,
feat
} = this.props;
const {showDisplayType, showInputType} = config || {};
const isForm = renderer === 'form';
const defaultValue = Array.isArray(formDefaultValue)
? {items: formDefaultValue}
: {items: []};
const value = Array.isArray(formValue) ? {items: formValue} : undefined;
const popOverContainer = env?.getModalContainer?.() ?? this.dom;
const isFirstStep = ctx?.__step === 0;
return (
<Form
className={cx('ae-FieldSetting')}
defaultValue={defaultValue}
value={value}
autoSubmit={false}
// onChange={this.handleTableChange}
onSubmit={this.handleSubmit}
ref={this.formRef}
>
{({control}: any) => (
<>
<InputTable
ref={this.tableRef}
name="items"
label={false}
labelAlign="left"
mode="horizontal"
horizontal={{left: 4}}
control={control}
scaffold={this.scaffold}
addable={true}
removable={true}
isRequired={false}
rules={{
validate: (values: any[]) =>
FieldSetting.validator(values, true)
}}
addButtonText="添加字段"
addButtonProps={{level: 'link'}}
scroll={{y: '315.5px'}}
footer={this.renderFooter}
columns={
[
{
title: '序号',
tdRender: (
{control}: any,
index: number,
rowIndex: number
) => {
return (
<Controller
name="index"
control={control}
render={({field, fieldState}) => (
<span>{rowIndex + 1}</span>
)}
/>
);
}
},
{
title: '字段名称',
tdRender: ({control}: any) => {
return (
<Controller
name="name"
control={control}
render={renderProps => {
const {field, fieldState} = renderProps;
return (
<InputBox
{...field}
onBlur={() => {
field.onBlur();
this.handleColumnBlur();
}}
hasError={!!fieldState.error}
className={cx('ae-FieldSetting-input')}
/>
);
}}
/>
);
}
},
{
title: '标题',
tdRender: ({control}: any) => {
return (
<Controller
name="label"
control={control}
render={renderProps => {
const {field, fieldState} = renderProps;
return (
<InputBox
{...field}
onBlur={() => {
field.onBlur();
this.handleColumnBlur();
}}
hasError={!!fieldState.error}
className={cx('ae-FieldSetting-input')}
/>
);
}}
/>
);
}
},
showInputType &&
!(renderer === 'crud' && feat === 'List' && !isFirstStep)
? {
title: '输入类型',
tdRender: ({control}: any, index: number) => {
return (
<Controller
name="inputType"
control={control}
isRequired
render={({field, fieldState}) => (
<Select
{...field}
className={'w-full'}
hasError={!!fieldState.error}
searchable
disabled={false}
clearable={false}
popOverContainer={popOverContainer}
options={[
{
label: '单行文本框',
value: 'input-text'
},
{
label: '多行文本',
value: 'textarea'
},
{
label: '数字输入',
value: 'input-number'
},
{
label: '单选框',
value: 'radios'
},
{
label: '勾选框',
value: 'checkbox'
},
{
label: '复选框',
value: 'checkboxes'
},
{
label: '下拉框',
value: 'select'
},
{
label: '开关',
value: 'switch'
},
{
label: '日期',
value: 'input-date'
},
{
label: '表格编辑',
value: 'input-table'
},
{
label: '组合输入',
value: 'combo'
},
{
label: '文件上传',
value: 'input-file'
},
{
label: '图片上传',
value: 'input-image'
},
{
label: '富文本编辑器',
value: 'input-rich-text'
}
]}
/>
)}
/>
);
}
}
: undefined,
showDisplayType
? {
title: '展示类型',
tdRender: ({control}: any) => {
return (
<Controller
name="displayType"
control={control}
isRequired
render={({field, fieldState}) => (
<Select
{...field}
className={'w-full'}
hasError={!!fieldState.error}
searchable
disabled={false}
clearable={false}
popOverContainer={popOverContainer}
options={[
{
value: 'tpl',
label: '文本',
typeKey: 'tpl'
},
{
value: 'image',
label: '图片',
typeKey: 'src'
},
{
value: 'date',
label: '日期',
typeKey: 'value'
},
{
value: 'progress',
label: '进度',
typeKey: 'value'
},
{
value: 'status',
label: '状态',
typeKey: 'value'
},
{
value: 'mapping',
label: '映射',
typeKey: 'value'
},
{
value: 'list',
label: '列表',
typeKey: 'value'
}
]}
/>
)}
/>
);
}
}
: undefined
].filter(
(f): f is Exclude<typeof f, null | undefined> => f != null
) as InputTableColumnProps[]
}
/>
</>
)}
</Form>
);
}
}
@FormItem({type: 'ae-field-setting'})
export default class FieldSettingRenderer extends FieldSetting {}

View File

@ -46,6 +46,12 @@ export interface SwitchMoreProps extends FormControlProps {
onClose: (e: React.UIEvent<any> | void) => void;
clearChildValuesOnOff?: boolean; // 关闭开关时,删除子表单字段,默认 true
defaultData?: any; // 默认数据
isChecked?: (options: {
data: any;
value: any;
name?: string;
bulk?: boolean;
}) => boolean;
}
interface SwitchMoreState {
@ -104,13 +110,25 @@ export default class SwitchMore extends React.Component<
}
initState() {
const {data, value, trueValue, falseValue, name, bulk, hiddenOnDefault} =
this.props;
const {
data,
value,
trueValue,
falseValue,
name,
bulk,
hiddenOnDefault,
isChecked
} = this.props;
let checked = false;
let show = false;
if (isChecked && typeof isChecked === 'function') {
checked = isChecked({data, value, name, bulk});
show = checked;
}
// 这个开关 无具体属性对应
if (!name) {
else if (!name) {
// 子表单项是组件根属性,遍历看是否有值
if (bulk) {
const formNames = this.getFormItemNames();

View File

@ -0,0 +1,211 @@
/**
* @file
*/
import React from 'react';
import cx from 'classnames';
import findIndex from 'lodash/findIndex';
import {FormControlProps, FormItem, TreeSelection} from 'amis';
import {toNumber} from 'amis-core';
import {getSchemaTpl} from 'amis-editor-core';
interface optionType {
label: string;
value: string;
}
export interface TableColumnWidthProps extends FormControlProps {
className?: string;
}
export interface TableColumnWidthState {
columns?: Array<any>;
activeOption: optionType;
}
export default class TableColumnWidthControl extends React.Component<
TableColumnWidthProps,
TableColumnWidthState
> {
options: Array<optionType> = [
{
label: '自适应',
value: 'adaptive'
},
{
label: '百分比',
value: 'percentage'
},
{
label: '固定宽度',
value: 'fixed'
}
];
constructor(props: any) {
super(props);
this.state = {
activeOption: this.options[0]
};
}
componentDidMount(): void {
const {value} = this.props;
if (value === undefined) return;
if (typeof value === 'number') {
this.state.activeOption !== this.options[2] &&
this.setState({
activeOption: this.options[2]
});
} else if (typeof value === 'string' && value.endsWith('%')) {
this.state.activeOption !== this.options[1] &&
this.setState({
activeOption: this.options[1]
});
} else {
this.state.activeOption !== this.options[0] &&
this.setState({
activeOption: this.options[0]
});
}
}
handleOptionChange(item: optionType) {
if (item === this.state.activeOption) return;
this.setState({
activeOption: item
});
this.props?.onChange?.(undefined);
}
renderHeader() {
const {
render,
formLabel,
labelRemark,
useMobileUI,
env,
popOverContainer,
data
} = this.props;
const classPrefix = env?.theme?.classPrefix;
const {activeOption} = this.state;
return (
<div className="ae-columnWidthControl-header">
<label className={cx(`${classPrefix}Form-label`)}>
{formLabel || ''}
{labelRemark
? render('label-remark', {
type: 'remark',
icon: labelRemark.icon || 'warning-mark',
tooltip: labelRemark,
className: cx(`Form-lableRemark`, labelRemark?.className),
useMobileUI,
container: popOverContainer
? popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
})
: null}
</label>
{render(
'columnWidthControl-options',
{
type: 'dropdown-button',
level: 'link',
size: 'sm',
label: activeOption.label,
align: 'right',
closeOnClick: true,
closeOnOutside: true,
buttons: this.options.map(item => ({
...item,
onClick: () => this.handleOptionChange(item)
}))
},
{
popOverContainer: null
}
)}
</div>
);
}
handleChange(type: 'fixed' | 'percentage', val: number) {
const onChange = this.props.onChange;
if (typeof val !== 'number' || isNaN(val)) return;
if (val <= 0) {
onChange?.(undefined);
return;
}
onChange?.(type === 'percentage' ? val + '%' : val);
}
renderBody() {
const {onBulkChange, render, onChange, value} = this.props;
const {activeOption} = this.state;
if (activeOption.value === 'adaptive') {
return null;
}
if (activeOption.value === 'fixed') {
return render(
'columnWidthControl-fixed',
getSchemaTpl('withUnit', {
label: '固定列宽',
name: 'interval',
control: {
type: 'input-number',
min: 0,
value
// onChange: (val: number) => this.handleChange('fixed', val)
},
unit: 'px',
className: 'mt-3'
}),
{
onChange: (val: number) => this.handleChange('fixed', val)
}
);
}
return render('columnWidthControl-fixed', {
type: 'input-range',
name: 'range',
min: 0,
max: 100,
step: 1,
label: '百分比列宽',
value: toNumber(value),
onChange: (val: number) =>
activeOption.value === 'percentage' &&
this.handleChange('percentage', val)
});
}
render() {
return (
<div className={cx('ae-columnWidthControl')}>
{this.renderHeader()}
{this.renderBody()}
</div>
);
}
}
@FormItem({
type: 'ae-columnWidthControl',
renderLabel: false
})
export class TableColumnWidthControlRender extends TableColumnWidthControl {}

View File

@ -0,0 +1,298 @@
/**
* @file AddColumnModal
* @desc
*/
import {useEffect, useRef} from 'react';
import omit from 'lodash/omit';
import React, {useState, useCallback} from 'react';
import {Button, Modal, themeable, ThemeProps, utils} from 'amis';
import {getSchemaTpl, JSONPipeIn, EditorManager} from 'amis-editor-core';
import {DSFeatureType, DSFeatureEnum, ModelDSBuilderKey} from '../../builder';
import type {RendererProps, BaseApiObject} from 'amis';
import type {CRUDColumnControlState} from './CRUDColumnControl';
import type {ColumnSchema} from 'amis/lib/renderers/Table2';
import type {DSBuilderInterface} from '../../builder';
type InitData = Exclude<CRUDColumnControlState['addModalData'], undefined>;
interface AddColumnModalProps extends ThemeProps {
visible: boolean;
initData: InitData;
ctx: Record<string, any>;
manager: EditorManager;
builder: DSBuilderInterface;
render: RendererProps['render'];
onConfirm: (scaffold: Record<string, any>) => void;
onClose: () => void;
}
/** 表单数据 */
interface FormData extends InitData {
name: string;
title: string;
feats: Extract<DSFeatureType, 'View' | 'Edit' | 'Delete'>[];
viewApi?: string | BaseApiObject;
editApi?: string | BaseApiObject;
deleteApi?: string | BaseApiObject;
__fieldItem: Record<string, any>[];
}
const AddColumnModal: React.FC<AddColumnModalProps> = props => {
const {
classnames: cx,
render,
visible,
initData,
ctx,
manager,
builder,
onConfirm,
onClose
} = props;
const componentId = ctx?.id;
const modalRef = useRef<any>(null);
const formRef = useRef<any>(null);
const [loading, setLoading] = useState(false);
const handleModalConfirm = useCallback(async () => {
const form = formRef?.current?.getWrappedInstance?.();
let schema;
let errorStack: any;
setLoading(true);
if (form) {
try {
schema = await form.submit?.(async (values: FormData) => {
let scaffold;
if (values.colType === 'field') {
const column = await builder.buildCRUDColumn?.(
values.__fieldItem
? {
...values.__fieldItem,
checked: true
}
: {
...values,
label: values.title,
name: values.name,
displayType: 'tpl'
},
{
renderer: 'crud',
inScaffold: false,
schema: ctx
},
componentId
);
scaffold =
column !== false
? column
: {
label: values.title,
name: values.name
};
} else if (values.colType === 'operation') {
const fields = (ctx?.columns ?? []).map((item: ColumnSchema) => ({
displayType: item.type ?? 'input-text',
inputType: item.type ?? 'input-text',
name: item.name,
label: item.title
}));
scaffold = await builder.buildCRUDOpColumn?.(
{
renderer: 'crud',
inScaffold: false,
feats: values.feats,
schema: ctx,
scaffoldConfig: {
viewFields: fields,
editFields: fields,
viewApi: values?.viewApi,
editApi: values?.editApi,
deleteApi: values?.deleteApi
},
buildSettings: {
useDefaultFields: true
}
},
componentId
);
}
return Promise.resolve(JSONPipeIn(omit(scaffold, ['key'])));
});
} catch (error) {
errorStack = error.stack;
}
}
setLoading(false);
if (!errorStack) {
onConfirm(schema);
onClose?.();
} else {
/** 表单校验没通过就不自动关闭Dialog */
console.error(errorStack);
}
}, [onConfirm]);
useEffect(() => {}, []);
return (
<React.Fragment>
<Modal
ref={modalRef}
size="sm"
show={visible}
onHide={onClose}
closeOnEsc={false}
contentClassName="ae-Scaffold-Modal"
>
<Modal.Header showCloseButton onClose={onClose}>
<Modal.Title></Modal.Title>
</Modal.Header>
<Modal.Body>
{render(
'column-control-modal',
{
type: 'form',
title: '',
mode: 'horizontal',
horizontal: {
justify: true,
leftFixed: 'sm'
},
submitOnChange: true,
wrapWithPanel: false,
clearValueOnHidden: true,
preventEnterSubmit: true,
actions: [],
body: [
{
type: 'input-tag',
name: 'colType',
label: '列类型',
static: true,
className: 'mb-2',
options: [
{label: '字段列', value: 'field'},
{label: '操作列', value: 'operation'}
]
},
...(initData?.colType === 'field'
? [
getSchemaTpl('formItemName', {
name: 'name',
label: '列字段',
required: true,
onBindingChange: async (
field: Record<string, any>,
onBulkChange: (value: any, submit?: boolean) => void
) => {
onBulkChange?.(
{
name: field.value,
title: field.label,
__fieldItem: field
},
true
);
return false;
}
}),
{
name: 'title',
label: '列标题',
type: 'input-text',
required: true
}
]
: []),
...(initData?.colType === 'operation'
? [
{
type: 'checkboxes',
label: '数据操作',
name: 'feats',
joinValues: false,
extractValue: true,
multiple: true,
inline: false,
options: [
{label: '查看详情', value: 'View'},
{label: '编辑记录', value: 'Edit'},
{label: '删除记录', value: 'Delete'}
],
value: [
DSFeatureEnum.View,
DSFeatureEnum.Edit,
DSFeatureEnum.Delete
]
},
...(builder.key !== ModelDSBuilderKey
? [
...builder.makeSourceSettingForm({
feat: 'View',
renderer: 'crud',
inScaffold: false,
sourceSettings: {
name: 'viewApi',
visibleOn:
"data.feats && data.feats.indexOf('View') > -1"
}
}),
...builder.makeSourceSettingForm({
feat: 'Edit',
renderer: 'crud',
inScaffold: false,
sourceSettings: {
name: 'editApi',
visibleOn:
"data.feats && data.feats.indexOf('Edit') > -1"
}
}),
...builder.makeSourceSettingForm({
feat: 'Delete',
renderer: 'crud',
inScaffold: false,
sourceSettings: {
name: 'deleteApi',
visibleOn:
"data.feats && data.feats.indexOf('Delete') > -1"
}
})
].filter(Boolean)
: [])
].filter(i => !!i)
: [])
]
},
{
ref: formRef,
popOverContainer: modalRef.current,
disabled: loading,
data: utils.createObject(ctx, {...initData})
}
)}
</Modal.Body>
<Modal.Footer>
<Button onClick={onClose}></Button>
<Button
loading={loading}
loadingClassName={cx('ae-CRUDConfigControl-modal-btn-loading')}
level="primary"
onClick={handleModalConfirm}
>
</Button>
</Modal.Footer>
</Modal>
</React.Fragment>
);
};
export default themeable(AddColumnModal);

View File

@ -0,0 +1,489 @@
/**
* @file CRUDColumnControl
* @desc
*/
import React from 'react';
import {findDOMNode} from 'react-dom';
import Sortable from 'sortablejs';
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';
import type {FormControlProps} from 'amis';
import type {SortableEvent} from 'sortablejs';
import type {ColumnSchema} from 'amis/lib/renderers/Table2';
import type {DSBuilderInterface} from '../../builder';
interface Option {
label: string;
value: string;
nodeId: string;
hidden: boolean;
/** 原始结构 */
pristine: DesignColumnSchema;
/** 字段信息 */
context?: any;
}
interface DesignColumnSchema extends ColumnSchema {
/** 设计态节点 ID */
$$id: string;
/** schema ID */
id: string;
/** 绑定的实体字段的 ID */
fieldId?: string;
relationBuildSetting?: any;
}
export interface CRUDColumnControlProps extends FormControlProps {
/** CRUD 节点的 ID */
nodeId: string;
builder: DSBuilderInterface;
}
export interface CRUDColumnControlState {
options: Option[];
loading: boolean;
showAddModal: boolean;
addModalData?: {
colTypeLabel: string;
colType: 'field' | 'operation';
};
}
export class CRUDColumnControl extends React.Component<
CRUDColumnControlProps,
CRUDColumnControlState
> {
sortable?: Sortable;
drag?: HTMLElement | null;
dom?: HTMLElement;
constructor(props: CRUDColumnControlProps) {
super(props);
this.state = {
options: [],
loading: false,
showAddModal: false
};
}
componentDidMount(): void {
this.dom = findDOMNode(this) as HTMLElement;
this.initOptions();
}
componentDidUpdate(prevProps: Readonly<CRUDColumnControlProps>): void {
if (prevProps.value !== this.props.value) {
this.initOptions();
}
}
transformOption(option: DesignColumnSchema): Option | false {
if (option.name || option.type === 'operation') {
return {
label:
typeof option.title === 'string'
? option.title
: option.type === 'tpl' && typeof (option as any).tpl === 'string'
? (option as any).tpl /** 处理 SchemaObject 的场景 */
: option.name,
value: option.name ?? (option as any).key,
/** 使用$$id用于定位 */
nodeId: option.$$id,
hidden: option.type === 'operation',
pristine: option
};
}
return false;
}
async initOptions() {
const {manager, nodeId, builder, data: ctx} = this.props;
const store = manager.store;
const node = store.getNodeById(nodeId);
this.setState({loading: true});
if (builder && builder.getCRUDListFields) {
try {
const options = await builder.getCRUDListFields<Option>({
renderer: 'crud',
schema: node.schema,
inScaffold: false,
controlSettings: {
fieldMapper: this.transformOption.bind(this)
}
});
this.setState({options});
} catch (error) {}
} else {
/** 从 Node 取是为了获取设计态节点 ID */
const columns = node?.schema?.columns as DesignColumnSchema[];
const result: Option[] = [];
columns.forEach(col => {
const option = this.transformOption(col);
if (option !== false) {
result.push(option);
}
});
this.setState({options: result});
}
this.setState({loading: false});
}
@autobind
dragRef(ref: any) {
if (!this.drag && ref) {
this.initDragging();
} else if (this.drag && !ref) {
this.destroyDragging();
}
this.drag = ref;
}
initDragging() {
const {classnames: cx} = this.props;
const dom = findDOMNode(this) as HTMLElement;
this.sortable = new Sortable(
dom.querySelector(
`.${cx('ae-CRUDConfigControl-list')}`
) as HTMLUListElement,
{
group: 'CRUDColumnControlGroup',
animation: 150,
handle: `.${cx('ae-CRUDConfigControl-list-item')}`,
ghostClass: `.${cx('ae-CRUDConfigControl-list-item--dragging')}`,
onEnd: (e: SortableEvent) => {
if (
e.newIndex === e.oldIndex ||
e.newIndex == null ||
e.oldIndex == null
) {
return;
}
const parent = e.to as HTMLElement;
if (e.oldIndex < parent.childNodes.length - 1) {
parent.insertBefore(e.item, parent.childNodes[e.oldIndex]);
} else {
parent.appendChild(e.item);
}
const options = this.state.options.concat();
options[e.oldIndex] = options.splice(
e.newIndex,
1,
options[e.oldIndex]
)[0];
this.setState({options}, () => this.handleSort());
}
}
);
}
destroyDragging() {
this.sortable && this.sortable.destroy();
}
@autobind
handleSort() {
const {onBulkChange} = this.props;
const options = this.state.options.concat();
onBulkChange?.({columns: options.map(item => item.pristine)});
}
@autobind
handleEdit(item: Option) {
const {manager, node} = this.props;
const columns = node?.schema?.columns ?? [];
const idx = columns.findIndex((c: any) => c.id === item.pristine.id);
if (!~idx) {
toast.warning(`未找到对应列「${item.label}`);
return;
}
// FIXME: 理论上用item.nodeId就可以不知道为何会重新构建一次导致store中node.id更新
manager.setActiveId(columns[idx]?.$$id);
}
/** 添加列 */
@autobind
handleAddColumn(type: 'field' | 'empty' | 'container' | 'operation') {
const {onBulkChange} = this.props;
const options = this.state.options.concat();
let scaffold: any;
switch (type) {
case 'field':
this.setState({
showAddModal: true,
addModalData: {colTypeLabel: '字段列', colType: type}
});
break;
case 'empty':
scaffold = {
title: '空列',
name: 'empty'
};
break;
case 'container':
scaffold = {
title: '容器',
name: 'container',
type: 'container',
style: {
position: 'static',
display: 'block'
},
wrapperBody: false,
body: []
};
break;
case 'operation':
this.setState({
showAddModal: true,
addModalData: {colTypeLabel: '操作列', colType: type}
});
break;
}
if (!scaffold) {
return;
}
const columnSchema = JSONPipeIn({...scaffold});
options.push({
label: columnSchema.title,
value: columnSchema.name,
nodeId: columnSchema.$$id,
hidden: type === 'operation',
pristine: columnSchema
});
this.setState({options}, () => {
onBulkChange?.({columns: options.map(item => item.pristine)});
});
}
@autobind
handleAddModalConfirm(scaffold: DesignColumnSchema) {
const {onBulkChange} = this.props;
const options = this.state.options.concat();
options.push({
label:
typeof scaffold.title === 'string' ? scaffold.title : scaffold.name,
value: scaffold.name,
nodeId: scaffold.$$id,
hidden: scaffold.type === 'operation',
pristine: scaffold
});
this.setState({options}, () => {
onBulkChange?.({columns: options.map(item => item.pristine)});
});
}
@autobind
handleAddModalClose() {
this.setState({
showAddModal: false,
addModalData: undefined
});
}
@autobind
async handleDelete(item: Option, index: number) {
const {onBulkChange, env} = this.props;
const options = this.state.options;
const confirmed = await env.confirm(`确定要删除列「${item.label}」吗?`);
if (~index && confirmed) {
options.splice(index, 1);
this.setState({options}, () => {
onBulkChange?.({columns: options.map(item => item.pristine)});
});
}
}
@autobind
renderOption(item: Option, index: number) {
const {
classnames: cx,
data: ctx,
render,
popOverContainer,
env
} = this.props;
return (
<li
key={index}
className={cx('ae-CRUDConfigControl-list-item', 'is-draggable')}
>
<TooltipWrapper
tooltip={{
content: item.label,
tooltipTheme: 'dark',
style: {fontSize: '12px'}
}}
container={popOverContainer || env?.getModalContainer?.()}
trigger={['hover']}
delay={150}
>
<div className={cx('ae-CRUDConfigControl-list-item-info')}>
<span>{item.label}</span>
</div>
</TooltipWrapper>
<div className={cx('ae-CRUDConfigControl-list-item-actions')}>
{item.hidden || !item?.context?.isCascadingField ? null : (
<Tag
label={item?.context?.modelLabel}
displayMode="normal"
className={cx(
'ae-CRUDConfigControl-list-item-tag',
'ae-CRUDConfigControl-list-item-tag--cascading'
)}
/>
)}
<Button
level="link"
size="sm"
tooltip={{
content: '去编辑',
tooltipTheme: 'dark',
style: {fontSize: '12px'}
}}
onClick={() => this.handleEdit(item)}
>
<Icon icon="column-setting" className="icon" />
</Button>
<Button
level="link"
size="sm"
onClick={() => this.handleDelete(item, index)}
>
<Icon icon="column-delete" className="icon" />
</Button>
</div>
</li>
);
}
renderHeader() {
const {classnames: cx, data: ctx, render, env} = this.props;
return (
<header className={cx('ae-CRUDConfigControl-header')}>
<span className={cx('Form-label')}></span>
{render('column-control-dropdown', {
type: 'dropdown-button',
closeOnClick: true,
hideCaret: true,
level: 'link',
align: 'right',
trigger: ['click'],
popOverContainer: env.getModalContainer ?? this.dom ?? document.body,
icon: 'column-add',
label: '添加列',
className: cx('ae-CRUDConfigControl-dropdown'),
buttons: [
{
type: 'button',
label: '字段列',
onClick: () => this.handleAddColumn('field')
},
{
type: 'button',
label: '空列',
onClick: () => this.handleAddColumn('empty')
},
{
type: 'button',
label: '容器列',
onClick: () => this.handleAddColumn('container')
},
{
type: 'button',
label: '操作列',
onClick: () => this.handleAddColumn('operation')
}
]
})}
</header>
);
}
render() {
const {classnames: cx, data: ctx, manager, builder} = this.props;
const {options, loading, showAddModal, addModalData} = this.state;
return (
<div className={cx('ae-CRUDConfigControl')}>
{loading ? (
<Spinner
show
tip="字段加载中"
tipPlacement="bottom"
size="sm"
className={cx('flex')}
/>
) : Array.isArray(options) && options.length > 0 ? (
<>
{this.renderHeader()}
<ul className={cx('ae-CRUDConfigControl-list')} ref={this.dragRef}>
{options.map((item, index) => {
return this.renderOption(item, index);
})}
</ul>
</>
) : (
<ul className={cx('ae-CRUDConfigControl-list')} ref={this.dragRef}>
<p className={cx(`ae-CRUDConfigControl-placeholder`)}></p>
</ul>
)}
{showAddModal ? (
<AddColumnModal
render={this.props.render}
visible={showAddModal}
initData={addModalData as any}
ctx={ctx}
manager={manager}
builder={builder}
onConfirm={this.handleAddModalConfirm}
onClose={this.handleAddModalClose}
/>
) : null}
</div>
);
}
}
@FormItem({
type: 'ae-crud-column-control',
renderLabel: false,
wrap: false
})
export class CRUDColumnControlRenderer extends CRUDColumnControl {}

View File

@ -0,0 +1,807 @@
/**
* @file CRUDFiltersControl
* @desc
*/
import React from 'react';
import {findDOMNode} from 'react-dom';
import cloneDeep from 'lodash/cloneDeep';
import uniq from 'lodash/uniq';
import {
FormItem,
Button,
Icon,
toast,
Switch,
Spinner,
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';
import type {
DSFeatureType,
DSBuilderInterface,
CRUDScaffoldConfig
} from '../../builder';
import type {EditorNodeType} from 'amis-editor-core';
import type {FormControlProps} from 'amis';
interface Option {
label: string;
value: string;
nodeId: string;
node?: EditorNodeType;
/** 原始结构 */
pristine: Record<string, any>;
/** 字段信息 */
context?: Record<string, any>;
}
interface CRUDFiltersControlProps extends FormControlProps {
/** CRUD配置面板的数据 */
data: Record<string, any>;
/** CRUD 节点的 ID */
nodeId: string;
// TODO暂时支持简单查询先不扩展了
feat: Extract<DSFeatureType, 'SimpleQuery' | 'AdvancedQuery' | 'FuzzyQuery'>;
/** 数据源构造器 */
builder: DSBuilderInterface;
}
interface CRUDFiltersControlState {
options: Option[];
loading: boolean;
checked: boolean;
/** 目标组件的 Node.id */
targetNodeId?: string;
}
export class CRUDFiltersControl extends React.Component<
CRUDFiltersControlProps,
CRUDFiltersControlState
> {
dom?: HTMLElement;
constructor(props: CRUDFiltersControlProps) {
super(props);
this.state = {
options: [],
loading: false,
checked: false
};
}
componentDidMount(): void {
this.dom = findDOMNode(this) as HTMLElement;
this.initOptions();
}
componentDidUpdate(
prevProps: Readonly<CRUDFiltersControlProps>,
prevState: Readonly<CRUDFiltersControlState>,
snapshot?: any
): void {
if (
prevProps.data.headerToolbar !== this.props.data.headerToolbar ||
prevProps.data.filter !== this.props.data.filter
) {
this.initOptions();
}
}
transformOption(option: any): Option | false {
if (option.name) {
return {
label:
typeof option.label === 'string'
? option.label
: option.label?.type === 'tpl' &&
typeof (option.label as any).tpl === 'string'
? (option.label as any).tpl /** 处理 SchemaObject 的场景 */
: option.name,
value: option.name ?? (option as any).key,
/** 使用id用于定位 */
nodeId: option.$$id,
pristine: option
};
}
return false;
}
@autobind
async initOptions() {
const {manager, nodeId, feat, builder} = this.props;
const store = manager.store;
const node = store.getNodeById(nodeId);
const CRUDSchema = node.schema;
const filterSchema = CRUDSchema?.filter
? Array.isArray(CRUDSchema.filter)
? CRUDSchema.filter.find(
(item: any) => item.behavior && Array.isArray(item.behavior)
)
: CRUDSchema.filter?.type === 'form'
? CRUDSchema.filter
: undefined
: undefined;
let targetNodeId: string = filterSchema ? filterSchema.$$id : '';
if (!builder) {
const options: Option[] = [];
(filterSchema?.body ?? []).forEach((formItem: any) => {
if (
formItem.type === 'condition-builder' ||
formItem.behavior === 'AdvancedQuery'
) {
return;
}
const option = this.transformOption(formItem);
if (option !== false) {
options.push(option);
}
});
this.setState({
options,
checked: options.length > 0,
targetNodeId: targetNodeId
});
return;
}
this.setState({loading: true});
const baseOpitons = {
feat,
renderer: 'crud',
schema: node.schema,
inScaffold: false,
controlSettings: {
fieldMapper: this.transformOption.bind(this)
}
};
try {
if (feat === DSFeatureEnum.SimpleQuery && builder.filterByFeat(feat)) {
const fields =
(await builder.getCRUDSimpleQueryFields?.<Option>({
...baseOpitons
})) ?? [];
this.setState({
options: fields,
checked: fields.length > 0,
targetNodeId
});
} else if (
feat === DSFeatureEnum.AdvancedQuery &&
builder.filterByFeat(feat)
) {
const fields =
(await builder?.getCRUDAdvancedQueryFields?.<Option>({
...baseOpitons
})) ?? [];
const CBSchema = (filterSchema?.body ?? []).find(
(item: any) =>
item.type === 'condition-builder' &&
(item.behavior === DSFeatureEnum.AdvancedQuery ||
item.name === '__filter')
);
targetNodeId = CBSchema ? CBSchema.$$id : '';
this.setState({
options: fields,
checked: fields.length > 0,
targetNodeId
});
} else if (
feat === DSFeatureEnum.FuzzyQuery &&
builder.filterByFeat(feat)
) {
const fields =
(await builder?.getCRUDFuzzyQueryFields?.<Option>({
...baseOpitons
})) ?? [];
let fuzzyQuerySchema: any;
traverseSchemaDeep(CRUDSchema, (key: string, value: any, host: any) => {
if (
key === 'behavior' &&
value === DSFeatureEnum.FuzzyQuery &&
host.type === 'search-box'
) {
fuzzyQuerySchema = host;
}
return [key, value];
});
targetNodeId = fuzzyQuerySchema ? fuzzyQuerySchema.$$id : '';
this.setState({
options: fields,
checked: fields.length > 0,
targetNodeId
});
}
} catch (error) {}
this.setState({loading: false});
}
async updateSimpleQuery(enable: boolean) {
const {manager, nodeId, builder} = this.props;
const store = manager.store;
const CRUDNode = store.getNodeById(nodeId);
const CRUDSchema = CRUDNode?.schema;
const CRUDSchemaID = CRUDSchema?.schema?.id;
const config = await builder.guessCRUDScaffoldConfig({schema: CRUDSchema});
const filterSchema = cloneDeep(
CRUDSchema?.filter
? Array.isArray(CRUDSchema.filter)
? CRUDSchema.filter.find(
(item: any) =>
item.behavior &&
Array.isArray(item.behavior) &&
item.type === 'form'
)
: CRUDSchema.filter?.type === 'form'
? CRUDSchema.filter
: undefined
: undefined
);
if (filterSchema) {
if (enable) {
const simpleQuerySchema =
(await builder.buildSimpleQueryCollectionSchema?.({
renderer: 'crud',
schema: CRUDSchema,
inScaffold: false,
buildSettings: {
useDefaultFields: true
}
})) ?? [];
const newFilterSchema = traverseSchemaDeep(
filterSchema,
(key: string, value: any, host: any) => {
/** 更新标识符 */
if (key === 'behavior' && Array.isArray(value)) {
return [
key,
uniq([...value, DSFeatureEnum.SimpleQuery].filter(Boolean))
];
}
/** 更新内容区 */
if (
key === 'body' &&
Array.isArray(value) &&
host?.type === 'form'
) {
return [key, [...value, ...simpleQuerySchema].filter(Boolean)];
}
return [key, value];
}
);
const targetNode = manager.store.getNodeById(filterSchema.$$id);
if (targetNode) {
targetNode.updateSchema(newFilterSchema);
}
} else {
const newFilterSchema = traverseSchemaDeep(
filterSchema,
(key: string, value: any, host: any) => {
/** 更新标识符 */
if (key === 'behavior' && Array.isArray(value)) {
return [
key,
value.filter(
(i: DSFeatureType) => i !== DSFeatureEnum.SimpleQuery
)
];
}
/** 更新内容区 */
if (
key === 'body' &&
Array.isArray(value) &&
host?.type === 'form'
) {
return [
key,
value.filter(
(item: any) => item?.behavior !== DSFeatureEnum.SimpleQuery
)
];
}
return [key, value];
}
);
const targetNode = manager.store.getNodeById(filterSchema.$$id);
if (targetNode) {
targetNode.updateSchema(newFilterSchema);
}
}
} else {
if (enable) {
/** 没有查询表头新建一个 */
const simpleQuerySchema =
(await builder.buildSimpleQueryCollectionSchema?.({
renderer: 'crud',
schema: CRUDSchema,
inScaffold: false,
buildSettings: {
useDefaultFields: true
}
})) ?? [];
const filter = await builder.buildCRUDFilterSchema({
renderer: 'crud',
inScaffold: false,
schema: CRUDSchema,
feats: [DSFeatureEnum.SimpleQuery],
scaffoldConfig: {
dsType: CRUDSchema.dsType,
simpleQueryFields: simpleQuerySchema
},
buildSettings: {
useDefaultFields: true
}
});
const newFilterSchema = cloneDeep(CRUDNode?.schema.filter);
const isArrayFilter = Array.isArray(newFilterSchema);
if (isArrayFilter) {
newFilterSchema.push(filter);
}
CRUDNode.updateSchema({
...CRUDSchema,
filter: isArrayFilter ? newFilterSchema : filter
});
}
}
}
@autobind
async updateAdvancedQuery(enable: boolean) {
const {manager, nodeId, builder} = this.props;
const store = manager.store;
const CRUDNode = store.getNodeById(nodeId);
const CRUDSchema = CRUDNode?.schema;
const filterSchema = cloneDeep(
Array.isArray(CRUDSchema.filter)
? CRUDNode?.schema.filter.find(
(item: any) => item.behavior && Array.isArray(item.behavior)
)
: CRUDNode?.schema.filter
);
if (filterSchema) {
if (enable) {
const advancedQuerySchema = await builder.buildAdvancedQuerySchema?.({
renderer: 'crud',
inScaffold: false,
schema: CRUDSchema,
buildSettings: {
useDefaultFields: true
}
});
const newFilterSchema = traverseSchemaDeep(
filterSchema,
(key: string, value: any, host: any) => {
/** 更新标识符 */
if (key === 'behavior' && Array.isArray(value)) {
return [
key,
uniq([...value, DSFeatureEnum.AdvancedQuery].filter(Boolean))
];
}
/** 更新内容区 */
if (
key === 'body' &&
Array.isArray(value) &&
host?.type === 'form'
) {
return [key, [advancedQuerySchema, ...value]];
}
return [key, value];
}
);
const targetNode = manager.store.getNodeById(filterSchema.$$id);
if (targetNode) {
targetNode.updateSchema(newFilterSchema);
}
} else {
const newFilterSchema = traverseSchemaDeep(
filterSchema,
(key: string, value: any, host: any) => {
/** 更新标识符 */
if (key === 'behavior' && Array.isArray(value)) {
return [
key,
value.filter(
(i: DSFeatureType) => i !== DSFeatureEnum.AdvancedQuery
)
];
}
/** 更新内容区 */
if (
key === 'body' &&
Array.isArray(value) &&
host?.type === 'form'
) {
return [
key,
value.filter(
(item: any) =>
item?.behavior !== DSFeatureEnum.AdvancedQuery &&
item.type !== 'condition-builder'
)
];
}
return [key, value];
}
);
const targetNode = manager.store.getNodeById(filterSchema.$$id);
if (targetNode) {
targetNode.updateSchema(newFilterSchema);
}
}
} else {
if (enable) {
/** 没有查询表头新建一个 */
const filter = await builder.buildCRUDFilterSchema({
renderer: 'crud',
inScaffold: false,
schema: CRUDSchema,
feats: [DSFeatureEnum.AdvancedQuery],
buildSettings: {
useDefaultFields: true
}
});
const newFilterSchema = cloneDeep(CRUDNode?.schema.filter);
const isArrayFilter = Array.isArray(newFilterSchema);
if (isArrayFilter) {
newFilterSchema.push(filter);
}
CRUDNode.updateSchema({
...CRUDSchema,
filter: isArrayFilter ? newFilterSchema : filter
});
}
}
}
@autobind
async updateFuzzyQuery(enable: boolean) {
const {manager, nodeId, builder} = this.props;
const store = manager.store;
const CRUDNode = store.getNodeById(nodeId);
const CRUDSchema = CRUDNode?.schema;
const CRUDSchemaID = CRUDSchema?.schema?.id;
const headerToolbar = cloneDeep(CRUDSchema.headerToolbar);
/** 关闭功能且存在定位容器 */
if (!enable) {
if (headerToolbar) {
deepRemove(
headerToolbar,
(schema: any) => {
return (
schema.behavior === DSFeatureEnum.FuzzyQuery &&
schema.type === 'search-box'
);
},
true
);
CRUDNode.updateSchema({
...CRUDSchema,
headerToolbar
});
}
return;
}
/** 没有工具栏的话直接重新创建 */
if (!headerToolbar) {
CRUDNode.updateSchema({
...CRUDSchema,
headerToolbar: await builder.buildCRUDHeaderToolbar?.(
{
renderer: 'crud',
inScaffold: false,
schema: CRUDSchema,
buildSettings: {
useDefaultFields: true
}
},
CRUDSchemaID
)
});
return;
}
/** 定位容器 */
let fuzzyQueryParent: any;
traverseSchemaDeep(CRUDSchema, (key: string, value: any, host: any) => {
if (
key === 'behavior' &&
Array.isArray(value) &&
host?.behavior.includes('FuzzyQuery') &&
host?.type === 'container' &&
Array.isArray(host?.body)
) {
fuzzyQueryParent = host;
}
return [key, value];
});
if (fuzzyQueryParent) {
const fuzzyQuerySchema = await builder.buildFuzzyQuerySchema?.({
renderer: 'crud',
inScaffold: false,
schema: CRUDSchema,
buildSettings: {
useDefaultFields: true
}
});
const newFuzzyParent = cloneDeep(fuzzyQueryParent);
newFuzzyParent.body = [...newFuzzyParent.body, fuzzyQuerySchema];
const targetNode = manager.store.getNodeById(fuzzyQueryParent.$$id);
if (targetNode) {
targetNode.updateSchema(newFuzzyParent);
}
} else {
const fuzzyQuerySchema = await builder.buildFuzzyQuerySchema?.({
renderer: 'crud',
inScaffold: false,
schema: CRUDSchema,
buildSettings: {
useDefaultFields: true
}
});
if (
headerToolbar?.[0]?.type === 'flex' &&
Array.isArray(headerToolbar[0]?.items)
) {
const newFlexContainer = cloneDeep(headerToolbar[0]);
/** toolbar 里有 flex 容器,直接追加 */
const container = await builder.buildFuzzyQuerySchema?.({
renderer: 'crud',
inScaffold: false,
schema: CRUDSchema,
buildSettings: {
useDefaultFields: true,
wrapContainer: 'container'
}
});
newFlexContainer.items.push(container);
const targetNode = manager.store.getNodeById(headerToolbar[0].$$id);
if (targetNode) {
targetNode.updateSchema(newFlexContainer);
}
} else {
/** toolbar 里没有 flex 容器,重新创建一个 */
const newHeaderToolbar = cloneDeep(headerToolbar);
const flexContainer = await builder.buildFuzzyQuerySchema?.({
renderer: 'crud',
inScaffold: false,
schema: CRUDSchema,
buildSettings: {
useDefaultFields: true,
wrapContainer: 'flex'
}
});
newHeaderToolbar.push(flexContainer);
CRUDNode.updateSchema({
...CRUDSchema,
headerToolbar: newHeaderToolbar
});
}
}
}
@autobind
async handleToggle(checked: boolean) {
const {feat, builder} = this.props;
this.setState({loading: true, checked});
try {
if (feat === DSFeatureEnum.SimpleQuery && builder.filterByFeat(feat)) {
await this.updateSimpleQuery(checked);
} else if (
feat === DSFeatureEnum.AdvancedQuery &&
builder.filterByFeat(feat)
) {
await this.updateAdvancedQuery(checked);
}
if (feat === DSFeatureEnum.FuzzyQuery && builder.filterByFeat(feat)) {
await this.updateFuzzyQuery(checked);
}
} catch (error) {}
this.setState({loading: false, checked});
}
@autobind
handleEdit(item?: Option) {
const {manager} = this.props;
const targetNodeId = item ? item?.nodeId : this.state.targetNodeId;
if (!targetNodeId) {
toast.warning(`未找到目标组件`);
return;
}
manager.setActiveId(targetNodeId);
}
@autobind
renderOption(item: Option, index: number) {
const {classnames: cx, feat, popOverContainer, env} = this.props;
return (
<li key={index} className={cx('ae-CRUDConfigControl-list-item')}>
<TooltipWrapper
tooltip={{
content: item.label,
tooltipTheme: 'dark',
style: {fontSize: '12px'}
}}
container={popOverContainer || env?.getModalContainer?.()}
trigger={['hover']}
delay={150}
>
<div className={cx('ae-CRUDConfigControl-list-item-info')}>
<span>{item.label}</span>
</div>
</TooltipWrapper>
<div className={cx('ae-CRUDConfigControl-list-item-actions')}>
{item?.context?.isCascadingField ? (
<Tag
label={item?.context?.modelLabel}
displayMode="normal"
className={cx(
'ae-CRUDConfigControl-list-item-tag',
'ae-CRUDConfigControl-list-item-tag--cascading'
)}
/>
) : null}
{feat === 'SimpleQuery' ? (
<Button
level="link"
size="sm"
tooltip={{
content: '去编辑',
tooltipTheme: 'dark',
style: {fontSize: '12px'}
}}
onClick={() => this.handleEdit(item)}
>
<Icon icon="column-setting" className={cx('icon')} />
</Button>
) : null}
</div>
</li>
);
}
renderHeader() {
const {
classPrefix: ns,
classnames: cx,
render,
env,
label,
feat
} = this.props;
const {options, checked} = this.state;
return (
<header className={cx('ae-CRUDConfigControl-header', 'mb-2')}>
<span className={cx('Form-label')}>{label}</span>
<div className={cx('ae-CRUDConfigControl-header-actions')}>
<Switch
className={cx('ae-CRUDConfigControl-header-actions-switch')}
key="switch"
size="sm"
classPrefix={ns}
value={checked}
onChange={this.handleToggle}
/>
<div className={cx('ae-CRUDConfigControl-header-actions-divider')} />
<Button
level="link"
size="sm"
tooltip={{
content: '去编辑目标组件',
tooltipTheme: 'dark',
style: {fontSize: '12px'}
}}
onClick={() => this.handleEdit()}
>
<Icon
icon="share-link"
className={cx('icon')}
style={{width: '16px', height: '16px'}}
/>
</Button>
</div>
</header>
);
}
render(): React.ReactNode {
const {classnames: cx} = this.props;
const {options, loading} = this.state;
return (
<div className={cx('ae-CRUDConfigControl')}>
{this.renderHeader()}
<ul className={cx('ae-CRUDConfigControl-list')}>
{loading ? (
<Spinner
show
tip="字段加载中"
tipPlacement="bottom"
size="sm"
className={cx('flex')}
/>
) : Array.isArray(options) && options.length > 0 ? (
options.map((item, index) => {
return this.renderOption(item, index);
})
) : (
<p className={cx(`ae-CRUDConfigControl-placeholder`)}></p>
)}
</ul>
</div>
);
}
}
@FormItem({
type: 'ae-crud-filters-control',
renderLabel: false,
wrap: false
})
export class CRUDFiltersControlRenderer extends CRUDFiltersControl {}

View File

@ -0,0 +1,408 @@
/**
* @file CRUDToolbarControl
* @desc
*/
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';
import {deepRemove} from '../../plugin/CRUD2/utils';
import type {FormControlProps} from 'amis';
import type {EditorNodeType} from 'amis-editor-core';
import type {ColumnSchema} from 'amis/lib/renderers/Table2';
import type {DSBuilderInterface} from '../../builder';
type ActionValue =
| Extract<DSFeatureType, 'Insert' | 'BulkEdit' | 'BulkDelete'>
| 'custom';
interface Option {
label: string;
value: ActionValue;
nodeId: string;
/** 原始结构 */
pristine: Record<string, any>;
node?: EditorNodeType;
}
export interface CRUDToolbarControlProps extends FormControlProps {
/** CRUD 节点的 ID */
nodeId: string;
builder: DSBuilderInterface;
}
export interface CRUDToolbarControlState {
options: Option[];
loading: boolean;
}
export class CRUDToolbarControl extends React.Component<
CRUDToolbarControlProps,
CRUDToolbarControlState
> {
drag?: HTMLElement | null;
dom?: HTMLElement;
/** 可供使用的功能集合 */
collection: ActionValue[] = [
DSFeatureEnum.Insert,
DSFeatureEnum.BulkEdit,
DSFeatureEnum.BulkDelete
];
constructor(props: CRUDToolbarControlProps) {
super(props);
this.state = {
options: [],
loading: false
};
}
componentDidMount(): void {
this.dom = findDOMNode(this) as HTMLElement;
const actions = this.getActions(this.props);
this.initOptions(actions);
}
componentDidUpdate(prevProps: Readonly<CRUDToolbarControlProps>): void {
if (prevProps.data.headerToolbar !== this.props.data.headerToolbar) {
const actions = this.getActions(this.props);
this.initOptions(actions);
}
}
getActions(props: CRUDToolbarControlProps) {
const {manager, nodeId} = props;
const store = manager.store;
const node: EditorNodeType = store.getNodeById(nodeId);
const actions = findTreeAll(node.children, item =>
[
DSFeatureEnum.Insert,
DSFeatureEnum.BulkEdit,
DSFeatureEnum.BulkDelete,
'custom'
].includes(item.schema.behavior)
) as unknown as EditorNodeType[];
return actions;
}
initOptions(actions: EditorNodeType[]) {
if (!actions || !actions.length) {
this.setState({options: []});
return;
}
const options = actions.map(node => {
const schema = node.schema;
const behavior = schema.behavior as ActionValue;
return {
label: this.getOptionLabel(schema, behavior),
value: behavior,
nodeId: schema.$$id,
node: node,
pristine: node.schema
};
});
this.setState({options});
}
getOptionLabel(schema: any, behavior: ActionValue) {
return behavior === 'custom' ? schema.label : DSFeature[behavior].label;
}
@autobind
handleEdit(item: Option) {
const {manager} = this.props;
if (!item.nodeId) {
toast.warning(`未找到工具栏中对应操作「${item.label}`);
return;
}
manager.setActiveId(item.nodeId);
}
/** 添加列 */
@autobind
async handleAddAction(type: ActionValue) {
this.setState({loading: true});
const {onBulkChange, data: ctx, nodeId, manager, builder} = this.props;
const options = this.state.options.concat();
const node = manager.store.getNodeById(nodeId);
const CRUDSchemaID = node?.schema?.id;
let scaffold: any;
switch (type) {
case 'Insert':
scaffold = await builder.buildInsertSchema(
{
feat: DSFeatureEnum.Insert,
renderer: 'crud',
inScaffold: false,
schema: ctx,
scaffoldConfig: {
insertFields: (ctx?.columns ?? [])
.filter((item: ColumnSchema) => item.type !== 'operation')
.map((item: ColumnSchema) => ({
inputType: item.type ?? 'input-text',
name: item.name,
label: item.title
})),
insertApi: ''
}
},
CRUDSchemaID
);
break;
case 'BulkEdit':
scaffold = await builder.buildBulkEditSchema(
{
feat: DSFeatureEnum.BulkEdit,
renderer: 'crud',
inScaffold: false,
schema: ctx,
scaffoldConfig: {
bulkEditFields: (ctx?.columns ?? [])
.filter((item: ColumnSchema) => item.type !== 'operation')
.map((item: ColumnSchema) => ({
inputType: item.type ?? 'input-text',
name: item.name,
label: item.title
})),
bulkEdit: ''
}
},
CRUDSchemaID
);
break;
case 'BulkDelete':
scaffold = await builder.buildCRUDBulkDeleteSchema(
{
feat: DSFeatureEnum.BulkDelete,
renderer: 'crud',
inScaffold: false,
schema: ctx,
scaffoldConfig: {
bulkDeleteApi: ''
}
},
CRUDSchemaID
);
break;
default:
scaffold = {
type: 'button',
label: '按钮',
behavior: 'custom',
className: 'm-r-xs',
onEvent: {
click: {
actions: []
}
}
};
}
if (!scaffold) {
this.setState({loading: false});
return;
}
const headerToolbarSchema = cloneDeep(ctx.headerToolbar);
const actionSchema = JSONPipeIn({...scaffold});
options.push({
label: this.getOptionLabel(actionSchema, type),
value: type,
nodeId: actionSchema.$$id,
pristine: actionSchema
});
this.setState({options, loading: false}, () => {
const target = headerToolbarSchema?.[0]?.items?.[0]?.body;
if (target && Array.isArray(target)) {
target.push(actionSchema);
} else {
headerToolbarSchema.unshift(actionSchema);
}
onBulkChange?.({headerToolbar: headerToolbarSchema});
});
}
@autobind
async handleDelete(option: Option, index: number) {
const {env, data: ctx, onBulkChange} = this.props;
const options = this.state.options.concat();
const confirmed = await env.confirm(
`确定要删除工具栏中「${option.label}」吗?`
);
const headerToolbarSchema = cloneDeep(ctx.headerToolbar);
if (confirmed) {
const marked = deepRemove(
headerToolbarSchema,
item => item.behavior === option.value
);
if (marked) {
options.splice(index, 1);
this.setState({options}, () => {
onBulkChange?.({headerToolbar: headerToolbarSchema});
});
}
}
}
@autobind
renderOption(item: Option, index: number) {
const {classnames: cx, popOverContainer, env} = this.props;
return (
<li key={index} className={cx('ae-CRUDConfigControl-list-item')}>
<TooltipWrapper
tooltip={{
content: item.label,
tooltipTheme: 'dark',
style: {fontSize: '12px'}
}}
container={popOverContainer || env?.getModalContainer?.()}
trigger={['hover']}
delay={150}
>
<div className={cx('ae-CRUDConfigControl-list-item-info')}>
<span>{item.label}</span>
</div>
</TooltipWrapper>
<div className={cx('ae-CRUDConfigControl-list-item-actions')}>
<Button
level="link"
size="sm"
tooltip={{
content: '去编辑',
tooltipTheme: 'dark',
style: {fontSize: '12px'}
}}
onClick={() => this.handleEdit(item)}
>
<Icon icon="column-setting" className="icon" />
</Button>
<Button
level="link"
size="sm"
onClick={() => this.handleDelete(item, index)}
>
<Icon icon="column-delete" className="icon" />
</Button>
</div>
</li>
);
}
renderHeader() {
const {classnames: cx, render, env} = this.props;
const options = this.state.options;
const actions = this.collection.concat();
// options.forEach(item => {
// if (actions.includes(item.value)) {
// const idx = actions.indexOf(item.value);
// if (~idx) {
// actions.splice(idx, 1);
// }
// }
// });
const optionValues = options.map(item => item.value);
return (
<header className={cx('ae-CRUDConfigControl-header')}>
<span className={cx('Form-label')}></span>
{render('crud-toolbar-control-dropdown', {
type: 'dropdown-button',
closeOnClick: true,
hideCaret: true,
level: 'link',
align: 'right',
trigger: ['click'],
popOverContainer: env.getModalContainer ?? this.dom ?? document.body,
icon: 'column-add',
label: '添加操作',
className: cx('ae-CRUDConfigControl-dropdown'),
disabledTip: {
content: '暂无可添加操作',
tooltipTheme: 'dark'
},
buttons: actions
.map((item: Exclude<ActionValue, 'custom'>) => ({
type: 'button',
label: DSFeature[item].label,
disabled: !!~optionValues.findIndex(op => op === item),
onClick: () => this.handleAddAction(item)
}))
.concat({
type: 'button',
label: '自定义按钮',
disabled: false,
onClick: () => this.handleAddAction('custom')
})
})}
</header>
);
}
render() {
const {classnames: cx, data: ctx} = this.props;
const {options, loading} = this.state;
return (
<div className={cx('ae-CRUDConfigControl')}>
{loading ? (
<Spinner
show
tip="操作生成中"
tipPlacement="bottom"
size="sm"
className={cx('flex')}
/>
) : (
<>
{this.renderHeader()}
<ul className={cx('ae-CRUDConfigControl-list')}>
{Array.isArray(options) && options.length > 0 ? (
options.map((item, index) => {
return this.renderOption(item, index);
})
) : (
<p className={cx(`ae-CRUDConfigControl-placeholder`)}>
</p>
)}
</ul>
</>
)}
</div>
);
}
}
@FormItem({
type: 'ae-crud-toolbar-control',
renderLabel: false,
wrap: false
})
export class CRUDToolbarControlRenderer extends CRUDToolbarControl {}

View File

@ -2702,7 +2702,7 @@ export const getEventControlConfig = (
: ACTION_TYPE_TREE(manager);
const allComponents = manager?.store?.getComponentTreeSource();
const checkComponent = (node: any, action: RendererPluginAction) => {
const actionType = action.actionType!;
const actionType = action?.actionType;
const actions = manager?.pluginActions[node.type];
const haveChild = !!node.children?.length;
let isSupport = false;
@ -2711,7 +2711,7 @@ export const getEventControlConfig = (
action.supportComponents === '*' ||
action.supportComponents === node.type;
// 内置逻辑
if (action.supportComponents === 'byComponent') {
if (action.supportComponents === 'byComponent' && actionType) {
isSupport = hasActionType(actionType, actions);
node.scoped = isSupport;
}
@ -2756,6 +2756,10 @@ export const getEventControlConfig = (
return manager.dataSchema;
},
getComponents: (action: RendererPluginAction) => {
if (!action) {
return [];
}
let components = manager?.store?.getComponentTreeSource();
let finalCmpts: any[] = [];
if (isSubEditor) {

View File

@ -418,7 +418,7 @@ export class EventControl extends React.Component<
// 找到激活的事件面板
Object.keys(onEvent)
.filter((key: string) => {
return onEvent[key].actions.length && eventPanelActive[key];
return onEvent[key]?.actions?.length && eventPanelActive[key];
})
.forEach((key: string, index: number) => {
if (!this.eventPanelSortMap[key]) {
@ -748,7 +748,7 @@ export class EventControl extends React.Component<
getContextSchemas,
...actionConfig,
groupType: actionConfig?.__actionType || action.actionType,
__actionDesc: actionNode!.description!, // 树节点描述
__actionDesc: actionNode?.description ?? '', // 树节点描述
__actionSchema: actionNode!.schema, // 树节点schema
__subActions: hasSubActionNode?.actions, // 树节点子动作
__cmptTreeSource: supportComponents ?? [],

View File

@ -4,7 +4,7 @@
import React, {MouseEvent} from 'react';
import cx from 'classnames';
import {Icon, FormItem, TooltipWrapper} from 'amis';
import {Icon, FormItem, TooltipWrapper, Spinner} from 'amis';
import {autobind, FormControlProps, render as renderAmis} from 'amis-core';
import {CodeMirrorEditor, FormulaEditor} from 'amis-ui';
import type {VariableItem, CodeMirror} from 'amis-ui';
@ -120,6 +120,8 @@ interface TextareaFormulaControlState {
isFullscreen: boolean; //是否全屏
tooltipStyle: {[key: string]: string}; // 提示框样式
loading: boolean;
}
export class TextareaFormulaControl extends React.Component<
@ -150,7 +152,8 @@ export class TextareaFormulaControl extends React.Component<
formulaPickerOpen: false,
formulaPickerValue: '',
isFullscreen: false,
tooltipStyle: {}
tooltipStyle: {},
loading: false
};
}
@ -282,12 +285,46 @@ export class TextareaFormulaControl extends React.Component<
});
}
@autobind
async handleFormulaEditorOpen() {
const {node, manager, data} = this.props;
const onFormulaEditorOpen = manager?.config?.onFormulaEditorOpen;
this.setState({loading: true});
try {
if (
manager &&
onFormulaEditorOpen &&
typeof onFormulaEditorOpen === 'function'
) {
const res = await onFormulaEditorOpen(node, manager, data);
if (res !== false) {
const variables = await getVariables(this);
this.setState({variables});
}
}
} catch (error) {
console.error(
'[amis-editor][TextareaFormulaControl] onFormulaEditorOpen failed: ',
error?.stack
);
}
this.setState({loading: false});
}
@autobind
async handleFormulaClick(e: React.MouseEvent, type?: string) {
if (this.props.onOverallClick) {
return;
}
try {
await this.handleFormulaEditorOpen();
} catch (error) {}
const variablesArr = await getVariables(this);
this.setState({
@ -337,7 +374,8 @@ export class TextareaFormulaControl extends React.Component<
formulaPickerValue,
isFullscreen,
variables,
tooltipStyle
tooltipStyle,
loading
} = this.state;
const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker;
@ -394,14 +432,22 @@ export class TextareaFormulaControl extends React.Component<
/>
</a>
</li>
<li className="ae-TextareaResultBox-footer-fxIcon">
<a
data-tooltip="表达式"
data-position="top"
onClick={this.handleFormulaClick}
>
<Icon icon="input-add-fx" className="icon" />
</a>
<li
className={cx('ae-TextareaResultBox-footer-fxIcon', {
'is-loading': loading
})}
>
{loading ? (
<Spinner show icon="reload" size="sm" />
) : (
<a
data-tooltip="表达式"
data-position="top"
onClick={this.handleFormulaClick}
>
<Icon icon="function" className="icon" />
</a>
)}
</li>
{/* 附加底部按钮菜单项 */}
{Array.isArray(additionalMenus) &&

View File

@ -373,23 +373,12 @@ setSchemaTpl(
name: fieldName,
type: 'radios',
inline: true,
onChange: () => {},
value: false,
// pipeIn: (value:any) => typeof value === 'boolean' ? value : '1'
options: [
{
label: '是',
value: true
},
{
label: '否',
value: false
},
{
label: '表达式',
value: ''
}
{label: '是', value: true},
{label: '否', value: false},
{label: '表达式', value: ''}
]
},
@ -460,29 +449,38 @@ setSchemaTpl('apiControl', (patch: any = {}) => {
};
});
setSchemaTpl('interval', (more: any = {}) => ({
type: 'ae-switch-more',
label: '定时刷新',
name: 'interval',
formType: 'extend',
bulk: true,
mode: 'normal',
form: {
body: [
getSchemaTpl('withUnit', {
label: '刷新间隔',
name: 'interval',
control: {
type: 'input-number',
setSchemaTpl(
'interval',
(config?: {
switchMoreConfig?: any;
formItems?: any[];
intervalConfig?: any;
}) => ({
type: 'ae-switch-more',
label: '定时刷新',
name: 'interval',
formType: 'extend',
bulk: true,
mode: 'normal',
form: {
body: [
getSchemaTpl('withUnit', {
label: '刷新间隔',
name: 'interval',
value: 1000
},
unit: '毫秒'
})
]
},
...more
}));
control: {
type: 'input-number',
name: 'interval',
value: 1000
},
unit: '毫秒',
...((config && config.intervalConfig) || {})
}),
...((config && config.formItems) || [])
]
},
...((config && config.switchMoreConfig) || {})
})
);
setSchemaTpl('silentPolling', () =>
getSchemaTpl('switch', {

View File

@ -6,7 +6,6 @@ import {
tipedLabel,
EditorManager
} from 'amis-editor-core';
import type {DSField} from 'amis-editor-core';
import type {SchemaObject} from 'amis';
import flatten from 'lodash/flatten';
import {InputComponentName} from '../component/InputComponentName';
@ -17,6 +16,8 @@ import map from 'lodash/map';
import omit from 'lodash/omit';
import keys from 'lodash/keys';
import type {DSField} from '../builder';
/**
* @deprecated switch
*/
@ -428,6 +429,7 @@ setSchemaTpl(
variables?: Array<VariableItem> | Function; // 自定义变量集合
requiredDataPropsVariables?: boolean; // 是否再从amis数据域中取变量结合 默认 false
variableMode?: 'tabs' | 'tree'; // 变量展现模式
className?: string; // 外层类名
[key: string]: any; // 其他属性例如包括表单项pipeIn\Out 等等
}) => {
const {
@ -463,6 +465,7 @@ setSchemaTpl(
// 上下展示,可避免 自定义渲染器 出现挤压
mode: mode === 'vertical' ? 'vertical' : 'horizontal',
visibleOn,
className: config?.className,
body: [
getSchemaTpl('formulaControl', {
label: label ?? '默认值',
@ -1691,3 +1694,13 @@ setSchemaTpl('anchorNavTitle', {
type: 'input-text',
required: true
});
setSchemaTpl('primaryField', {
type: 'input-text',
name: 'primaryField',
label: tipedLabel(
'主键',
'每行记录的唯一标识符,通常用于行选择、批量操作等场景。'
),
pipeIn: defaultValue('id')
});

View File

@ -1,4 +1,4 @@
import {findTree, resolveVariableAndFilter} from 'amis';
import {JSONValueMap, findTree, resolveVariableAndFilter} from 'amis';
import isString from 'lodash/isString';
/**
@ -64,3 +64,19 @@ export const resolveOptionType = (options: any) => {
return value !== undefined ? typeof value : 'string';
};
/**
*
*
* @param conf schema
* @param keys key列表
* @returns
*/
export function escapeFormula(conf: any, keys: string[] = ['tpl']) {
return JSONValueMap(conf, (value: any, key: string | number) => {
if (keys.includes(String(key)) && /(^|[^\\])\$\{.+\}/.test(value)) {
return value.replace(/\${/g, ' \\${');
}
return value;
});
}

View File

@ -1,6 +1,6 @@
{
"name": "amis-formula",
"version": "3.4.0",
"version": "3.4.1-alpha.0",
"description": "负责 amis 里面的表达式实现,内置公式,编辑器等",
"main": "lib/index.js",
"module": "esm/index.js",

View File

@ -3,7 +3,7 @@
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts",
"version": "3.4.0",
"version": "3.4.1-alpha.0",
"description": "",
"scripts": {
"build": "npm run clean-dist && NODE_ENV=production rollup -c ",
@ -36,8 +36,8 @@
},
"dependencies": {
"@rc-component/mini-decimal": "^1.0.1",
"amis-core": "^3.4.0",
"amis-formula": "^3.4.0",
"amis-core": "^3.4.1-alpha.0",
"amis-formula": "^3.4.1-alpha.0",
"classnames": "2.3.2",
"codemirror": "^5.63.0",
"downshift": "6.1.12",

View File

@ -3,3 +3,11 @@
display: inline;
}
}
.max-line {
overflow : hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
word-break: break-word;
}

View File

@ -0,0 +1,17 @@
.#{$ns}InputTable-UI {
.#{$ns}Table-contentWrap.is-fixed {
overflow: auto;
}
table {
text-align: left;
position: relative;
border-collapse: collapse;
& thead > tr > th {
position: sticky;
top: 0;
z-index: $zindex-sticky; /* 遮挡一下 tbody */
}
}
}

View File

@ -11,7 +11,7 @@
@import '../layout/aside';
@import '../layout/hbox';
@import '../layout/vbox';
@import '../components/button'; // 这个要放在最前面
@import '../components/button';
@import '../components/avatar';
@import '../components/breadcrumb';
@import '../components/badge';
@ -126,6 +126,8 @@
@import '../components/form/icon-select';
@import '../components/form/form';
@import '../components/form/user-select';
@import '../components/form/input-table';
@import '../components/anchor-nav';
@import '../components/markdown';
@import '../components/link';

View File

@ -8,7 +8,8 @@ import TooltipWrapper, {TooltipObject, Trigger} from './TooltipWrapper';
import {pickEventsProps} from 'amis-core';
import {ClassNamesFn, themeable} from 'amis-core';
import Spinner, {SpinnerExtraProps} from './Spinner';
interface ButtonProps
export interface ButtonProps
extends React.DOMAttributes<HTMLButtonElement>,
SpinnerExtraProps {
id?: string;

View File

@ -117,7 +117,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
{clearable && !disabled && value ? (
<a onClick={this.clearValue} className={cx('InputBox-clear')}>
<Icon icon="close" className="icon" />
<Icon icon="input-clear" className="icon" />
</a>
) : null}
</div>

View File

@ -20,11 +20,26 @@ import Button from './Button';
import FormField, {FormFieldProps} from './FormField';
import {Icon} from './icons';
import type {ButtonProps} from './Button';
export interface tdRenderFunc {
(
methods: UseFormReturn,
colIndex: number,
rowIndex: number
): JSX.Element | null;
}
export interface InputTableColumnProps {
title?: string;
className?: string;
thRender?: () => JSX.Element;
tdRender: (methods: UseFormReturn, index: number) => JSX.Element | null;
tdRender: tdRenderFunc;
}
interface InputTableScrollProps {
/** 垂直滚动区域的最大高度 */
y: number | string;
}
export interface InputTabbleProps<T = any>
@ -45,10 +60,24 @@ export interface InputTabbleProps<T = any>
addable?: boolean;
addButtonClassName?: string;
addButtonText?: string;
addButtonProps?: Partial<Omit<ButtonProps, 'onClick'>>;
maxLength?: number;
minLength?: number;
removable?: boolean;
/** 表格CSS类名 */
tableClassName?: string;
/** 表格头CSS类名 */
tableHeadClassName?: string;
/** 表格内容区CSS类名 */
tableBodyClassName?: string;
/** 空状态文字 */
placeholder?: React.ReactNode;
/** 滚动设置 */
scroll?: InputTableScrollProps;
/** 底部工具栏 */
footer?: () => React.ReactNode;
onItemAdd?: (values: Record<string, any>) => void;
}
export function InputTable({
@ -69,12 +98,23 @@ export function InputTable({
addable,
addButtonText,
addButtonClassName,
addButtonProps,
scaffold,
minLength,
maxLength,
isRequired,
rules
rules,
tableClassName,
tableHeadClassName,
tableBodyClassName,
placeholder,
scroll,
footer,
onItemAdd
}: InputTabbleProps) {
const enableScroll = scroll?.y != null;
const tBodyRef = React.useRef<HTMLTableElement>(null);
const tableRef = React.useRef<HTMLTableElement>(null);
const subForms = React.useRef<Record<any, UseFormReturn>>({});
const subFormRef = React.useCallback(
(subform: UseFormReturn | null, id: string) => {
@ -160,11 +200,32 @@ export function InputTable({
);
function renderBody() {
const handleItemAdd = () => {
const values = {...scaffold};
append(values);
/** 开启滚动后新增元素定位到底部 */
if (enableScroll && tableRef) {
requestAnimationFrame(() => {
tableRef?.current?.scrollIntoView?.({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
});
});
}
onItemAdd?.(values);
};
return (
<div className={cx(`Table`)}>
<div className={cx(`Table-contentWrap`)}>
<table className={cx(`Table-table`)}>
<thead>
<div className={cx(`Table`, `InputTable-UI`, className)}>
<div
className={cx(`Table-contentWrap`, {'is-fixed': enableScroll})}
style={{maxHeight: enableScroll ? scroll.y : 'unset'}}
>
<table className={cx(`Table-table`, tableClassName)} ref={tableRef}>
<thead className={cx(tableHeadClassName)}>
<tr>
{columns.map((item, index) => (
<th key={index} className={item.className}>
@ -174,7 +235,7 @@ export function InputTable({
<th key="operation">{__('Table.operation')}</th>
</tr>
</thead>
<tbody>
<tbody className={cx(tableBodyClassName)}>
{fields.length ? (
fields.map((field, index) => (
<tr key={field.id}>
@ -212,7 +273,7 @@ export function InputTable({
icon="desk-empty"
className={cx('Table-placeholder-empty-icon', 'icon')}
/>
{__('placeholder.noData')}
{placeholder ?? __('placeholder.noData')}
</td>
</tr>
)}
@ -223,12 +284,14 @@ export function InputTable({
<div className={cx(`InputTable-toolbar`)}>
<Button
className={cx(addButtonClassName)}
onClick={() => append({...scaffold})}
size="sm"
{...addButtonProps}
onClick={() => handleItemAdd()}
>
<Icon icon="plus" className="icon" />
<span>{__(addButtonText || 'add')}</span>
</Button>
{footer?.()}
</div>
) : null}
</div>
@ -257,7 +320,7 @@ export interface InputTableRowProps {
value: any;
control: Control<any>;
columns: Array<{
tdRender: (methods: UseFormReturn, index: number) => JSX.Element | null;
tdRender: tdRenderFunc;
className?: string;
}>;
update: (index: number, data: Record<string, any>) => void;
@ -293,9 +356,9 @@ export function InputTableRow({
return (
<>
{columns.map((item, index) => (
<td key={index} className={item.className}>
{item.tdRender(methods, index)}
{columns.map((item, colIndex) => (
<td key={colIndex} className={item.className}>
{item.tdRender(methods, colIndex, index)}
</td>
))}
</>

View File

@ -149,7 +149,10 @@ export class Tag extends React.Component<TagProps> {
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
<span className={cx('Tag-text')}>
<span
className={cx('Tag-text')}
title={typeof label === 'string' ? label : undefined}
>
{prevIcon}
{label || children}
</span>
@ -187,6 +190,7 @@ class CheckableTagComp extends React.Component<CheckableTagProps> {
})}
onClick={disabled ? noop : this.handleClick}
style={style}
title={typeof label === 'string' ? label : undefined}
>
{label || children}
</span>

View File

@ -468,7 +468,7 @@ export class Table extends React.PureComponent<TableProps, TableState> {
}
current && this.updateTableDom(current);
if (this.props.draggable) {
if (this.props.draggable && this.tbodyDom?.current) {
this.initDragging();
}
@ -1140,8 +1140,8 @@ export class Table extends React.PureComponent<TableProps, TableState> {
if (record) {
let target = event.target;
if (target.tagName !== 'TR') {
target = target.closest('tr');
if (target?.tagName !== 'TR') {
target = target?.closest('tr');
}
this.setState({hoverRow: {target, rowIndex, record}});

View File

@ -67,11 +67,7 @@ test('Renderer:input table', async () => {
});
test('Renderer: input-table with default value column', async () => {
const onSubmitCallbackFn = jest
.fn()
.mockImplementation((values: any, actions: any) => {
return true;
});
const onSubmitCallbackFn = jest.fn();
const {container, getByText} = render(
amisRender(
{

View File

@ -1279,6 +1279,7 @@ exports[`5. Renderer:crud cards 1`] = `
>
<div
class="cxd-Grid-col--sm6"
style="flex: 0 0 50%; max-width: 50%;"
>
<div
class="cxd-Card cxd-Card--link"
@ -1347,6 +1348,7 @@ exports[`5. Renderer:crud cards 1`] = `
</div>
<div
class="cxd-Grid-col--sm6"
style="flex: 0 0 50%; max-width: 50%;"
>
<div
class="cxd-Card cxd-Card--link"
@ -1415,6 +1417,7 @@ exports[`5. Renderer:crud cards 1`] = `
</div>
<div
class="cxd-Grid-col--sm6"
style="flex: 0 0 50%; max-width: 50%;"
>
<div
class="cxd-Card cxd-Card--link"
@ -1483,6 +1486,7 @@ exports[`5. Renderer:crud cards 1`] = `
</div>
<div
class="cxd-Grid-col--sm6"
style="flex: 0 0 50%; max-width: 50%;"
>
<div
class="cxd-Card cxd-Card--link"
@ -1551,6 +1555,7 @@ exports[`5. Renderer:crud cards 1`] = `
</div>
<div
class="cxd-Grid-col--sm6"
style="flex: 0 0 50%; max-width: 50%;"
>
<div
class="cxd-Card cxd-Card--link"
@ -1619,6 +1624,7 @@ exports[`5. Renderer:crud cards 1`] = `
</div>
<div
class="cxd-Grid-col--sm6"
style="flex: 0 0 50%; max-width: 50%;"
>
<div
class="cxd-Card cxd-Card--link"
@ -1687,6 +1693,7 @@ exports[`5. Renderer:crud cards 1`] = `
</div>
<div
class="cxd-Grid-col--sm6"
style="flex: 0 0 50%; max-width: 50%;"
>
<div
class="cxd-Card cxd-Card--link"
@ -1755,6 +1762,7 @@ exports[`5. Renderer:crud cards 1`] = `
</div>
<div
class="cxd-Grid-col--sm6"
style="flex: 0 0 50%; max-width: 50%;"
>
<div
class="cxd-Card cxd-Card--link"
@ -1823,6 +1831,7 @@ exports[`5. Renderer:crud cards 1`] = `
</div>
<div
class="cxd-Grid-col--sm6"
style="flex: 0 0 50%; max-width: 50%;"
>
<div
class="cxd-Card cxd-Card--link"
@ -1891,6 +1900,7 @@ exports[`5. Renderer:crud cards 1`] = `
</div>
<div
class="cxd-Grid-col--sm6"
style="flex: 0 0 50%; max-width: 50%;"
>
<div
class="cxd-Card cxd-Card--link"

View File

@ -1,6 +1,6 @@
{
"name": "amis",
"version": "3.4.0",
"version": "3.4.1-alpha.0",
"description": "一种MIS页面生成工具",
"main": "lib/index.js",
"module": "esm/index.js",
@ -37,8 +37,8 @@
}
],
"dependencies": {
"amis-core": "^3.4.0",
"amis-ui": "^3.4.0",
"amis-core": "^3.4.1-alpha.0",
"amis-ui": "^3.4.1-alpha.0",
"attr-accept": "2.2.2",
"blueimp-canvastoblob": "2.1.0",
"classnames": "2.3.2",

Some files were not shown because too many files have changed in this diff Show More