feat: improve collection manager (#1013)

* feat: 图形化管理数据表

* feat: 图形化管理数据表

* feat: 图形化管理数据表

* feat: 图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat(collection-manager): add foreignKey Field and support relate field record foreignKey info

through collection record into collections and foreignKey field record info fields

* fix(collection-manager): if has through collection then don't create through collections record

* fix(client/route-switch): skip sub routes

* feat: 添加graphpostion

* feat: 图形化collection新增表时刷新数据

* fix(collection-manager): refactor afterCreateForRelateField

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化样式优化

* feat: styling

* feat: 图形化样式优化

* feat: 图形化样式优化

* feat: 图形化数据表多语言完善

* feat: 图形化数据表多语言完善

* feat: improve code

* feat: 图形化数据表连线样式修改

* feat: 图形化数据表样式修改

* feat: 图形化数据表样式修改

* feat: 图形化数据表样式修改

* feat: 图形化数据表样式修改

* fix(collection-manager): fix afterCreateForRelateField

* feat: 样式优化

* feat: 样式优化

* feat: afterCreateForForeignKeyField

* fix: timestamps: false

* feat: 连线锚点优化

* fix(collection-manager): when del foreign key field, relate fields will be del too

* fix: update package.json

* fix: update package.json

* feat: 文件名大小写

* feat: 连线锚点优化

* feat: 连线锚点通过计算得到样式优化

* feat: 连线锚点通过计算得到样式优化

* fix: fk

* fix: remove index

* feat: 连线hover时高亮

* fix: test error

* feat: 初始化计算位置

* feat: 初始化时计算坐标位置

* feat: 初始化时计算坐标位置

* feat: improve code (#933)

* fix: built in

* feat: 没有关系字段时也要连线

* feat: 自关联也要连线

* fix: styling

* feat: 滚动条问题

* feat: 拖拽优化

* feat: 画布paddig优化

* feat: 编辑时支持反向关联字段配置

* feat: 画布拖拽滚动优化

* feat: 画布拖拽滚动优化

* fix: reload

* feat: 修复数据表新建重叠

* fix: refreshCM & refreshGM

* feat: 修复表达式输入框显示异常

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* fix: 消除代码警告

* fix: 消除代码警告

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化

* feat: 渲染性能优化

* feat: 外键生成在位置在前面

* feat: 限制表最多显示10个字段其余滚动

* feat: 移动表位置的连线重新计算最优位置

* fix: error

* feat: 布局自动换行

* fix: test error

* fix: xpipe.eq

* fix: upgrade error

* fix: upgrade error

* feat: 选中表时只显示和目标表关联的表和连线

* feat: 连线优化

* fix: maxListenersExceededWarning

* feat: 连线优化

* feat: powerby样式优化

* feat: 表筛选优化

* feat: 新建字段优化

* feat: 点击线高亮主外键和关联字段

* feat: 点击线高亮主外键和关联字段

* feat: 鼠标hover连线高亮主外键和关联字段

* fix(collection-manager): foreign key sorting should follow ID

* fix(client/config-relation-field): set Relation field's ReverseField default value is false

* feat: 卡片默认显示主外键和关联字段其余通过折叠展示且分组区分显示

* fix(client/collection-manager): don't display auto create through collections and foreign key

only display in graph menu

* feat: 样式优化

* feat: 添加字段时默认展开折叠

* feat: 样式优化

* feat: foreign field migration (#1001)

* feat: 补充多语言

* feat: settings center tabs

* feat: 主键判断primaryKey

* fix(collection-manager): foreign key sorting should follow primaryKey

* fix(client/block-select-collection): filter auto create through collections

* fix(client/block-config-fields): filter isForeignKey fields

* fix(client/configuration-table): relation fileds select collection filter auto create through

* feat: 多对多连线高亮时全亮

* feat: 选中多对多中的一张表另一张表也显示

* feat: 连线mouseleave事件

* feat: 多语言更新

* feat: 计算新建表位置优化

* feat: 添加自动布局

* feat(client/configure-fields): categorize fields

* fix(client/configure-fields): display foreign key fields

* fix(client): package reference

* fix: remove graph

* fix: remove

Co-authored-by: 唐小爱 <tangxiaoai@192.168.0.103>
Co-authored-by: lyf-coder <lyf-coder@foxmail.com>
Co-authored-by: katherinehhh <katherine_15995@163.com>
Co-authored-by: ChengLei Shao <chareice@live.com>
Co-authored-by: mytharcher <mytharcher@gmail.com>
This commit is contained in:
chenos 2022-11-02 22:13:25 +08:00 committed by GitHub
parent 8f5a93bf63
commit 0e70e3848a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 406 additions and 47 deletions

32
.vscode/launch.json vendored
View File

@ -4,41 +4,13 @@
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Server",
"cmd": "${workspaceRoot}",
"runtimeArgs": [
"-r", "dotenv/config",
"-r", "tsconfig-paths/register",
"-r", "ts-node/register"
],
"args": ["${workspaceRoot}/packages/app/server/src/index.ts", "start"],
"port": 9229,
"skipFiles": [
"<node_internals>/**"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "Debug Jest Tests",
"runtimeExecutable": "yarn",
"runtimeArgs": [
"run",
"--inspect-brk",
"test",
"--runInBand",
// could be any single file path to debug
"${workspaceFolder}/packages/plugins/workflow/src/__tests__/instructions/parallel.test.ts"
],
"port": 9229,
"skipFiles": [
"<node_internals>/**"
],
"runtimeArgs": ["run", "--inspect-brk", "test", "--runInBand", "${fileBasenameNoExtension}"],
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}

View File

@ -143,7 +143,7 @@ export const AddCollectionField = (props) => {
};
export const AddFieldAction = (props) => {
const { scope, getContainer, item: record, children } = props;
const { scope, getContainer, item: record, children,trigger } = props;
const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
@ -154,6 +154,7 @@ export const AddFieldAction = (props) => {
<ActionContext.Provider value={{ visible, setVisible }}>
<Dropdown
getPopupContainer={getContainer}
trigger={trigger}
overlay={
<Menu
style={{

View File

@ -0,0 +1,66 @@
import { createForm, Field } from '@formily/core';
import { FieldContext, FormContext, observer, useField, useFieldSchema } from '@formily/react';
import { Options, Result } from 'ahooks/lib/useRequest/src/types';
import React, { useMemo } from 'react';
import { AsyncDataProvider, useAsyncData, useRequest } from '../..';
import { useAttach } from '@formily/react/lib/hooks/useAttach';
import { TableProps } from 'antd';
import { CollectionFieldsTableArray } from './CollectionFieldsTableArray';
type TableVoidProps = TableProps<any> & {
request?: any;
useSelectedRowKeys?: any;
useDataSource?: (
options?: Options<any, any> & { uid?: string },
props?: any,
) => Result<any, any> & { state?: any; setState?: any };
};
const useDefSelectedRowKeys = () => {
const result = useAsyncData();
return [result?.state?.selectedRowKeys, (selectedRowKeys) => result?.setState?.({ selectedRowKeys })];
};
const useDef = (options, props) => {
const { request, dataSource } = props;
if (request) {
return useRequest(request(props), options);
} else {
return Promise.resolve({
data: dataSource,
});
}
};
export const CollectionFieldsTable: React.FC<TableVoidProps> = observer((props) => {
const { rowKey = 'id', useDataSource = useDef, useSelectedRowKeys = useDefSelectedRowKeys } = props;
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const form = useMemo(() => createForm(), []);
const f = useAttach(form.createArrayField({ ...field.props, basePath: '' }));
const result = useDataSource(
{
uid: fieldSchema['x-uid'],
onSuccess(data) {
form.setValues({
[fieldSchema.name]: data?.data,
});
},
},
props,
);
return (
<AsyncDataProvider value={result}>
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>
<CollectionFieldsTableArray
{...props}
rowKey={rowKey}
loading={result?.['loading']}
useSelectedRowKeys={useSelectedRowKeys}
pagination={false}
/>
</FieldContext.Provider>
</FormContext.Provider>
</AsyncDataProvider>
);
});

View File

@ -0,0 +1,220 @@
import { css } from '@emotion/css';
import { ArrayField, Field } from '@formily/core';
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { Table, TableColumnProps } from 'antd';
import { default as classNames } from 'classnames';
import React, { useState } from 'react';
import { RecordIndexProvider, RecordProvider, useCollectionManager, useRequest, useSchemaInitializer } from '../..';
import { useTranslation } from 'react-i18next';
const isColumnComponent = (schema: Schema) => {
return schema['x-component']?.endsWith('.Column') > -1;
};
const useTableColumns = () => {
const field = useField<ArrayField>();
const schema = useFieldSchema();
const { exists, render } = useSchemaInitializer(schema['x-initializer']);
const columns = schema
.reduceProperties((buf, s) => {
if (isColumnComponent(s)) {
return buf.concat([s]);
}
}, [])
.map((s: Schema) => {
return {
title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
dataIndex: s.name,
key: s.name,
render: (v, record) => {
const index = field.value?.indexOf(record);
// console.log((Date.now() - start) / 1000);
return (
<RecordIndexProvider index={index}>
<RecordProvider record={record}>
<RecursionField schema={s} name={index} onlyRenderProperties />
</RecordProvider>
</RecordIndexProvider>
);
},
} as TableColumnProps<any>;
});
if (!exists) {
return columns;
}
return columns.concat({
title: render(),
dataIndex: 'TABLE_COLUMN_INITIALIZER',
key: 'TABLE_COLUMN_INITIALIZER',
});
};
export const components = {
body: {
row: (props) => {
return <tr {...props} />;
},
cell: (props) => (
<td
{...props}
className={classNames(
props.className,
css`
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`,
)}
/>
),
},
};
const useDef = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
return [selectedRowKeys, setSelectedRowKeys];
};
const useDefDataSource = (options, props) => {
const field = useField<Field>();
return useRequest(() => {
return Promise.resolve({
data: field.value,
});
}, options);
};
const groupColumns = [
{
dataIndex: 'name',
key: 'name',
},
];
type CategorizeKey = 'primaryAndForeignKey' | 'relation' | 'systemInfo' | 'basic';
const sortKeyArr: Array<CategorizeKey> = ['primaryAndForeignKey', 'relation', 'basic', 'systemInfo'];
const CategorizeKeyNameMap = new Map<CategorizeKey, string>([
['primaryAndForeignKey', 'Primary key & Foreign key fields'],
['relation', 'Relation fields'],
['systemInfo', 'System fields'],
['basic', 'General fields'],
]);
interface CategorizeDataItem {
key: CategorizeKey;
name: string;
data: Array<any>;
}
export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
const field = useField<ArrayField>();
const columns = useTableColumns();
const { t } = useTranslation();
const { getInterface } = useCollectionManager();
const {
showIndex = true,
useSelectedRowKeys = useDef,
useDataSource = useDefDataSource,
onChange,
...others
} = props;
const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
const [categorizeData, setCategorizeData] = useState<Array<CategorizeDataItem>>([]);
useDataSource({
onSuccess(data) {
field.value = data?.data || [];
// categorize field
const categorizeMap = new Map<CategorizeKey, any>();
const addCategorizeVal = (categorizeKey: CategorizeKey, val) => {
let fieldArr = categorizeMap.get(categorizeKey);
if (!fieldArr) {
fieldArr = [];
}
fieldArr.push(val);
categorizeMap.set(categorizeKey, fieldArr);
};
field.value.forEach((item) => {
const itemInterface = getInterface(item?.interface);
if (item?.primaryKey || item.isForeignKey) {
addCategorizeVal('primaryAndForeignKey', item);
return;
}
const group = itemInterface?.group as CategorizeKey;
switch (group) {
case 'systemInfo':
case 'relation':
addCategorizeVal(group, item);
break;
default:
addCategorizeVal('basic', item);
}
});
const tmpData: Array<CategorizeDataItem> = [];
sortKeyArr.forEach((key) => {
if (categorizeMap.get(key)?.length > 0) {
tmpData.push({
key,
name: t(CategorizeKeyNameMap.get(key)),
data: categorizeMap.get(key),
});
}
});
setCategorizeData(tmpData);
},
});
const restProps = {
rowSelection: props.rowSelection
? {
type: 'checkbox',
selectedRowKeys,
onChange(selectedRowKeys: any[]) {
setSelectedRowKeys(selectedRowKeys);
},
...props.rowSelection,
}
: undefined,
};
const defaultRowKey = (record: any) => {
return field.value?.indexOf?.(record);
};
const expandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => {
debugger;
return (
<Table
{...others}
{...restProps}
components={components}
showHeader={true}
columns={columns}
dataSource={record.data}
pagination={false}
/>
);
};
return (
<div
className={css`
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
`}
>
<Table
showHeader={false}
loading={props?.loading}
columns={groupColumns}
dataSource={categorizeData}
pagination={false}
expandable={{
expandedRowRender,
defaultExpandedRowKeys: sortKeyArr,
}}
/>
</div>
);
});

View File

@ -11,6 +11,7 @@ import { AddSubFieldAction } from './AddSubFieldAction';
import { FieldSummary } from './components/FieldSummary';
import { EditSubFieldAction } from './EditSubFieldAction';
import { collectionSchema } from './schemas/collections';
import { CollectionFieldsTable } from ".";
const useAsyncDataSource = (service: any) => (field: any) => {
field.loading = true;
@ -174,7 +175,9 @@ export const ConfigurationTable = () => {
const { collections = [] } = useCollectionManager();
const compile = useCompile();
const loadCollections = async (field: any) => {
return collections?.map((item: any) => ({
return collections
?.filter((item) => !(item.autoCreate && item.isThrough))
.map((item: any) => ({
label: compile(item.title),
value: item.name,
}));
@ -189,6 +192,7 @@ export const ConfigurationTable = () => {
AddSubFieldAction,
EditSubFieldAction,
FieldSummary,
CollectionFieldsTable
}}
scope={{
useDestroySubField,

View File

@ -6,6 +6,7 @@ export * from './ConfigurationTable';
export * from './EditFieldAction';
export * from './interfaces';
export * from './components';
export * from './CollectionFieldsTable';
registerValidateFormats({
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,

View File

@ -71,7 +71,7 @@ export const collectionFieldSchema: ISchema = {
resource: 'collections.fields',
action: 'list',
params: {
pageSize: 50,
paginate: false,
filter: {
'interface.$not': null,
},
@ -123,7 +123,7 @@ export const collectionFieldSchema: ISchema = {
table: {
type: 'void',
'x-uid': 'input',
'x-component': 'Table.Void',
'x-component': 'CollectionFieldsTable',
'x-component-props': {
rowKey: 'name',
rowSelection: {

View File

@ -63,6 +63,11 @@ export const collectionSchema: ISchema = {
pageSize: 50,
filter: {
inherit: false,
options: {
// filter auto create through collections
autoCreate: { $not: true },
isThrough: { $not: true },
},
},
sort: ['sort'],
appends: [],

View File

@ -78,7 +78,7 @@ export const reverseFieldProperties: Record<string, ISchema> = {
properties: {
autoCreateReverseField: {
type: 'boolean',
default: true,
default: false,
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Create inverse field in the target collection")}}',

View File

@ -121,6 +121,10 @@ export default {
"Configure columns": "Configure columns",
"Edit field": "Edit field",
"Configure fields of {{title}}": "Configure fields of {{title}}",
"Primary key & Foreign key fields": "Primary key & Foreign key fields",
"Relation fields": "Relation fields",
"System fields": "System fields",
"General fields": "General fields",
"Basic": "Basic",
"Single line text": "Single line text",
"Long text": "Long text",

View File

@ -125,6 +125,10 @@ export default {
"Configure columns": "配置字段",
"Edit field": "编辑字段",
"Configure fields of {{title}}": "「{{title}}」的字段配置",
"Primary key & Foreign key fields": "主外键字段",
"Relation fields": "关系字段",
"System fields": "系统字段",
"General fields": "普通字段",
"Basic": "基本类型",
"Single line text": "单行文本",
"Long text": "多行文本",

View File

@ -14,7 +14,7 @@ import { useCompile } from '../schema-component';
import { BlockTemplatesPane } from '../schema-templates';
import { SystemSettingsPane } from '../system-settings';
const SettingsCenterContext = createContext<any>({});
export const SettingsCenterContext = createContext<any>({});
const PluginCard = (props) => {
const history = useHistory<any>();

View File

@ -190,7 +190,7 @@ const InternalAdminLayout = (props: any) => {
text-align: center;
width: 100%;
z-index: 0;
padding: 10px 50px;
padding: 0px 50px;
}
`}
>

View File

@ -74,7 +74,7 @@ export const useTableColumnInitializerFields = () => {
const { name, fields = [] } = useCollection();
const { getInterface } = useCollectionManager();
return fields
.filter((field) => field?.interface && field?.interface !== 'subTable')
.filter((field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey)
.map((field) => {
const interfaceConfig = getInterface(field.interface);
const schema = {
@ -157,7 +157,7 @@ export const useFormItemInitializerFields = (options?: any) => {
const { readPretty = form.readPretty, block = 'Form' } = options || {};
return fields
?.filter((field) => field?.interface)
?.filter((field) => field?.interface && !field?.isForeignKey)
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
@ -433,7 +433,13 @@ export const useCollectionDataSourceItems = (componentName) => {
type: 'itemGroup',
title: t('Select collection'),
children: collections
?.filter((item) => !item.inherit)
?.filter((item) => {
if(item.inherit){
return false
}else{
return !(item?.isThrough && item?.autoCreate);
}
})
?.map((item, index) => {
const templates = getTemplatesByCollection(item.name).filter((template) => {
return (

View File

@ -2,7 +2,14 @@ import Database from '@nocobase/database';
export function afterCreateForForeignKeyField(db: Database) {
function generateFkOptions(collectionName: string, foreignKey: string) {
const M = db.getModel(collectionName);
const collection = db.getCollection(collectionName);
if (!collection) {
throw new Error('collection not found');
}
const M = collection.model;
const attr = M.rawAttributes[foreignKey];
if (!attr) {
throw new Error(`${collectionName}.${foreignKey} does not exists`);
@ -50,6 +57,7 @@ export function afterCreateForForeignKeyField(db: Database) {
},
transaction,
});
if (instance) {
if (instance.type !== values.type) {
throw new Error(`fk type invalid`);
@ -58,15 +66,30 @@ export function afterCreateForForeignKeyField(db: Database) {
instance.set('isForeignKey', true);
await instance.save({ transaction });
} else {
await r.create({
const creatInstance = await r.create({
values: {
isForeignKey: true,
sort:1,
...values,
},
transaction,
});
// SortField#setSortValue instance._previousDataValues[scopeKey] judgment cause create set sort:1 invalid, need update
creatInstance.set('sort', 1);
await creatInstance.save({ transaction });
}
// update ID sort:0
await r.update({
filter: {
collectionName,
options:{
primaryKey: true
}
},
values: {
sort: 0,
},
transaction,
});
}
return async (model, { transaction, context }) => {
@ -74,7 +97,9 @@ export function afterCreateForForeignKeyField(db: Database) {
if (!context) {
return;
}
const { type, interface: interfaceType, collectionName, target, through, foreignKey, otherKey } = model.get();
// foreign key in target collection
if (['oho', 'o2m'].includes(interfaceType)) {
const values = generateFkOptions(target, foreignKey);
@ -86,6 +111,7 @@ export function afterCreateForForeignKeyField(db: Database) {
transaction,
});
}
// foreign key in source collection
else if (['obo', 'm2o'].includes(interfaceType)) {
const values = generateFkOptions(collectionName, foreignKey);
@ -94,6 +120,7 @@ export function afterCreateForForeignKeyField(db: Database) {
transaction,
});
}
// foreign key in through collection
else if (['linkTo', 'm2m'].includes(interfaceType)) {
if (type !== 'belongsToMany') {

View File

@ -0,0 +1,48 @@
import { Migration } from '@nocobase/server';
import { afterCreateForForeignKeyField } from '../hooks/afterCreateForForeignKeyField';
export default class DropForeignKeysMigration extends Migration {
async up() {
const result = await this.app.version.satisfies('<0.8.0');
if (!result) {
return;
}
const transaction = await this.app.db.sequelize.transaction();
const callback = afterCreateForForeignKeyField(this.app.db);
try {
const fields = await this.app.db.getCollection('fields').repository.find({
filter: {
interface: {
$in: ['oho', 'o2m', 'obo', 'm2o', 'linkTo', 'm2m'],
},
collectionName: {
$not: null,
},
},
});
for (const field of fields) {
try {
await callback(field, {
transaction,
context: {},
});
} catch (error) {
if (error.message.includes('collection not found')) {
continue;
}
throw error;
}
}
await transaction.commit();
} catch (error) {
console.log(error);
await transaction.rollback();
}
}
}

View File

@ -83,6 +83,7 @@ export class CollectionManagerPlugin extends Plugin {
});
}
});
// after migrate
this.app.db.on('fields.afterCreate', afterCreateForForeignKeyField(this.app.db));