feat: improve code (#978)

* 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: 选中表时只显示和目标表关联的表和连线

* fix: maxListenersExceededWarning

* feat: remove graph-collection-manager

* fix: remove graph-collection-manager

* fix: update yarn.lock

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>
This commit is contained in:
chenos 2022-10-28 15:09:14 +08:00 committed by GitHub
parent 274d80d501
commit f67afba964
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 545 additions and 144 deletions

View File

@ -27,7 +27,7 @@ export async function move(ctx: Context, next) {
await sortAbleCollection.sticky(sourceId);
}
}
ctx.body = 'ok';
await next();
}

View File

@ -14,11 +14,7 @@ export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (pr
service,
interfaces: { ...defaultInterfaces, ...interfaces },
collections,
refreshCM: async () => {
if (refreshCM) {
await refreshCM();
}
},
refreshCM,
}}
>
<CollectionManagerSchemaComponentProvider>{props.children}</CollectionManagerSchemaComponentProvider>
@ -49,11 +45,16 @@ export const RemoteCollectionManagerProvider = (props: any) => {
<CollectionManagerProvider
service={{ ...service, contentLoading, setContentLoading }}
collections={service?.data?.data}
refreshCM={async () => {
refreshCM={async (opts) => {
if (opts?.reload) {
setContentLoading(true);
}
const { data } = await api.request(options);
service.mutate(data);
if (opts?.reload) {
setContentLoading(false);
}
return data?.data || [];
}}
{...props}
/>

View File

@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { PluginManager } from '../plugin-manager';
import { ActionContext, SchemaComponent } from '../schema-component';
import { AddFieldAction, ConfigurationTable, EditFieldAction } from './Configuration';
import { AddCollectionField, AddFieldAction, ConfigurationTable, EditFieldAction,EditCollectionField } from './Configuration';
const schema: ISchema = {
type: 'object',
@ -37,7 +37,7 @@ const schema2: ISchema = {
export const CollectionManagerPane = () => {
return (
<Card bordered={false}>
<SchemaComponent schema={schema2} components={{ ConfigurationTable, AddFieldAction, EditFieldAction }} />
<SchemaComponent schema={schema2} components={{ ConfigurationTable, AddFieldAction, AddCollectionField, EditFieldAction,EditCollectionField }} />
</Card>
);
};

View File

@ -7,9 +7,9 @@ import { cloneDeep } from 'lodash';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client';
import { useRecord } from '../../record-provider';
import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useCreateAction } from '../action-hooks';
import { useCancelAction, useCreateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
@ -44,6 +44,9 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
[uid()]: {
type: 'void',
'x-component': 'Action.Drawer',
'x-component-props': {
getContainer: '{{ getContainer }}',
},
'x-decorator': 'Form',
'x-decorator-props': {
useValues(options) {
@ -75,7 +78,7 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
useAction: '{{ useCancelAction }}',
},
},
action2: {
@ -94,11 +97,25 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
};
};
export const useCollectionFieldFormValues = () => {
const form = useForm();
return {
getValues() {
const values = cloneDeep(form.values);
if (values.autoCreateReverseField) {
} else {
delete values.reverseField;
}
delete values.autoCreateReverseField;
return values;
}
}
}
const useCreateCollectionField = () => {
const form = useForm();
const { run } = useCreateAction();
const { refreshCM } = useCollectionManager();
const { title } = useRecord();
const ctx = useActionContext();
const { refresh } = useResourceActionContext();
const { resource } = useResourceContext();
@ -120,16 +137,23 @@ const useCreateCollectionField = () => {
};
};
export const AddFieldAction = () => {
export const AddCollectionField = (props) => {
const record = useRecord();
return <AddFieldAction item={record} {...props} />;
};
export const AddFieldAction = (props) => {
const { scope, getContainer, item: record, children } = props;
const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
const compile = useCompile();
const { t } = useTranslation();
const record = useRecord();
return (
<RecordProvider record={record}>
<ActionContext.Provider value={{ visible, setVisible }}>
<Dropdown
getPopupContainer={getContainer}
overlay={
<Menu
style={{
@ -158,15 +182,26 @@ export const AddFieldAction = () => {
</Menu>
}
>
{children || (
<Button icon={<PlusOutlined />} type={'primary'}>
{t('Add field')}
</Button>
)}
</Dropdown>
<SchemaComponent
schema={schema}
components={{ ...components, ArrayTable }}
scope={{ createOnly: true, useCreateCollectionField, record, showReverseFieldConfig: true }}
scope={{
getContainer,
useCancelAction,
createOnly: true,
useCreateCollectionField,
record,
showReverseFieldConfig: true,
...scope,
}}
/>
</ActionContext.Provider>
</RecordProvider>
);
};

View File

@ -6,15 +6,15 @@ import set from 'lodash/set';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client';
import { useRecord } from '../../record-provider';
import { useRecord, RecordProvider } from '../../record-provider';
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useUpdateAction } from '../action-hooks';
import { useCancelAction, useUpdateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components';
const getSchema = (schema: IField, record: any, compile): ISchema => {
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
if (!schema) {
return;
}
@ -64,7 +64,7 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
useAction: '{{ useCancelAction }}',
},
},
action2: {
@ -109,8 +109,13 @@ const useUpdateCollectionField = () => {
};
};
export const EditFieldAction = (props) => {
export const EditCollectionField = (props) => {
const record = useRecord();
return <EditFieldAction item={record} {...props} />;
};
export const EditFieldAction = (props) => {
const { scope, getContainer, item: record,children } = props;
const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
@ -118,7 +123,9 @@ export const EditFieldAction = (props) => {
const { t } = useTranslation();
const compile = useCompile();
const [data, setData] = useState<any>({});
return (
<RecordProvider record={record}>
<ActionContext.Provider value={{ visible, setVisible }}>
<a
onClick={async () => {
@ -142,18 +149,26 @@ export const EditFieldAction = (props) => {
},
record,
compile,
getContainer,
);
setSchema(schema);
setVisible(true);
}}
>
{t('Edit')}
{children||t('Edit')}
</a>
<SchemaComponent
schema={schema}
components={{ ...components, ArrayTable }}
scope={{ useUpdateCollectionField, showReverseFieldConfig: !data?.reverseField }}
scope={{
getContainer,
useUpdateCollectionField,
useCancelAction,
showReverseFieldConfig: !data?.reverseField,
...scope,
}}
/>
</ActionContext.Provider>
</RecordProvider>
);
};

View File

@ -1,8 +1,11 @@
import { registerValidateFormats } from '@formily/core';
import { exp } from 'mathjs';
export * from './AddFieldAction';
export * from './ConfigurationTable';
export * from './EditFieldAction';
export * from './interfaces';
export * from './components';
registerValidateFormats({
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,

View File

@ -113,7 +113,7 @@ export const collectionFieldSchema: ISchema = {
create: {
type: 'void',
title: '{{ t("Add new") }}',
'x-component': 'AddFieldAction',
'x-component': 'AddCollectionField',
'x-component-props': {
type: 'primary',
},
@ -181,7 +181,7 @@ export const collectionFieldSchema: ISchema = {
update: {
type: 'void',
title: '{{ t("Edit") }}',
'x-component': 'EditFieldAction',
'x-component': 'EditCollectionField',
'x-component-props': {
type: 'primary',
},

View File

@ -9,4 +9,6 @@ export * from './context';
export * from './hooks';
export * from './ResourceActionProvider';
export * from './types';
export * from './Configuration';

View File

@ -66,6 +66,10 @@ export const SettingsCenterDropdown = () => {
title: t('Workflow'),
path: 'workflow/workflows',
},
{
title: t('Graph Collections'),
path: 'graph/collections',
},
];
return (
<ActionContext.Provider value={{ visible, setVisible }}>
@ -79,6 +83,7 @@ export const SettingsCenterDropdown = () => {
onClick={() => {
history.push('/admin/settings/' + item.path);
}}
key={item.path}
>
{item.title}
</Menu.Item>

View File

@ -178,10 +178,10 @@ const InternalAdminLayout = (props: any) => {
className={css`
min-height: calc(100vh - 46px);
position: relative;
padding-bottom: 70px;
// padding-bottom: 70px;
> div {
position: relative;
z-index: 1;
// z-index: 1;
}
.ant-layout-footer {
position: absolute;
@ -189,6 +189,7 @@ const InternalAdminLayout = (props: any) => {
text-align: center;
width: 100%;
z-index: 0;
padding: 10px 50px;
}
`}
>

View File

@ -155,6 +155,8 @@ export class Database extends EventEmitter implements AsyncEmitter {
constructor(options: DatabaseOptions) {
super();
// this.setMaxListeners(100);
this.version = new DatabaseVersion(this);
const opts = {

View File

@ -17,6 +17,10 @@ export class BelongsToManyField extends RelationField {
);
}
get otherKey() {
return this.options.otherKey;
}
bind() {
const { database, collection } = this.context;
const Target = this.TargetModel;
@ -34,6 +38,7 @@ export class BelongsToManyField extends RelationField {
} else {
Through = database.collection({
name: through,
// timestamps: false,
});
Object.defineProperty(Through.model, 'isThrough', { value: true });
@ -80,6 +85,7 @@ export class BelongsToManyField extends RelationField {
unbind() {
const { database, collection } = this.context;
const Through = database.getCollection(this.through);
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
database.removePendingField(this);
// 删掉 model 的关联字段

View File

@ -5,12 +5,11 @@ import {
ModelIndexesOptions,
QueryInterfaceOptions,
SyncOptions,
Transactionable,
Transactionable
} from 'sequelize';
import { Collection } from '../collection';
import { Database } from '../database';
import { ModelEventTypes } from '../types';
import { checkIdentifier } from '../utils';
export interface FieldContext {
database: Database;
@ -84,6 +83,7 @@ export abstract class Field {
}
remove() {
this.collection.removeIndex([this.name]);
return this.collection.removeField(this.name);
}

View File

@ -26,7 +26,8 @@
"koa-bodyparser": "^4.3.0",
"koa-static": "^5.0.0",
"lodash": "^4.17.21",
"semver": "^7.3.7"
"semver": "^7.3.7",
"xpipe": "^1.0.5"
},
"devDependencies": {
"@types/semver": "^7.3.9"

View File

@ -207,10 +207,15 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this._events = [];
// @ts-ignore
this._eventsCount = [];
this.removeAllListeners();
this.middleware = new Toposort<any>();
// this.context = Object.create(context);
this.plugins = new Map<string, Plugin>();
this._acl = createACL();
if (this._db) {
// MaxListenersExceededWarning
this._db.removeAllListeners();
}
this._db = this.createDatabase(options);
this._resourcer = createResourcer(options);
this._cli = new Command('nocobase').usage('[command] [options]');
@ -220,9 +225,14 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this.context.resourcer = this._resourcer;
this.context.cache = this._cache;
if (this._pm) {
this._pm = this._pm.clone();
} else {
this._pm = new PluginManager({
app: this,
plugins: options.plugins,
});
}
this._appManager = new AppManager(this);
@ -236,8 +246,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
registerActions(this);
}
this.loadPluginConfig(options.plugins || []);
registerCli(this);
this._version = new ApplicationVersion(this);
@ -264,16 +272,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return this.pm.addStatic(pluginClass, options);
}
loadPluginConfig(pluginsConfigurations: PluginConfiguration[]) {
for (let pluginConfiguration of pluginsConfigurations) {
if (typeof pluginConfiguration == 'string') {
this.plugin(pluginConfiguration);
} else {
this.plugin(...pluginConfiguration);
}
}
}
// @ts-ignore
use<NewStateT = {}, NewContextT = {}>(
middleware: Koa.Middleware<StateT & NewStateT, ContextT & NewContextT>,

View File

@ -4,6 +4,7 @@ import execa from 'execa';
import fs from 'fs';
import net from 'net';
import { resolve } from 'path';
import xpipe from 'xpipe';
import Application from '../application';
import { Plugin } from '../plugin';
import collectionOptions from './options/collection';
@ -12,6 +13,7 @@ import { PluginManagerRepository } from './PluginManagerRepository';
export interface PluginManagerOptions {
app: Application;
plugins?: any[];
}
export interface InstallOptions {
@ -27,11 +29,12 @@ export class PluginManager {
plugins = new Map<string, Plugin>();
server: net.Server;
pmSock: string;
_tmpPluginArgs = [];
constructor(options: PluginManagerOptions) {
this.app = options.app;
const f = resolve(process.cwd(), 'storage', 'pm.sock');
this.pmSock = this.app.options.pmSock || f;
this.pmSock = xpipe.eq(this.app.options.pmSock || f);
this.app.db.registerRepositories({
PluginManagerRepository,
});
@ -71,6 +74,17 @@ export class PluginManager {
await this.repository.load();
}
});
this.addStaticMultiple(options.plugins);
}
addStaticMultiple(plugins: any) {
for (let plugin of plugins || []) {
if (typeof plugin == 'string') {
this.addStatic(plugin);
} else {
this.addStatic(...plugin);
}
}
}
getPlugins() {
@ -123,7 +137,20 @@ export class PluginManager {
await run('yarn', ['install']);
}
clone() {
const pm = new PluginManager({
app: this.app,
});
for (const arg of this._tmpPluginArgs) {
pm.addStatic(...arg);
}
return pm;
}
addStatic(plugin?: any, options?: any) {
if (!options?.async) {
this._tmpPluginArgs.push([plugin, options]);
}
let name: string;
if (typeof plugin === 'string') {
name = plugin;
@ -154,7 +181,10 @@ export class PluginManager {
// console.log(`adding ${plugin} plugin`);
const packageName = await PluginManager.findPackage(plugin);
const packageJson = require(`${packageName}/package.json`);
const instance = this.addStatic(plugin, options);
const instance = this.addStatic(plugin, {
...options,
async: true,
});
let model = await this.repository.findOne({
filter: { name: plugin },
});

View File

@ -56,11 +56,11 @@ interface Resource {
export class MockServer extends Application {
async loadAndInstall(options: any = {}) {
await this.load();
await this.load({ method: 'install' });
await this.install({
...options,
sync: {
force: true,
force: false,
alter: {
drop: false,
},

View File

@ -507,7 +507,7 @@ describe('acl', () => {
expect(response.statusCode).toEqual(200);
});
it('should sync data to acl before app start', async () => {
it('should sync data to acl after app reload', async () => {
const role = await db.getRepository('roles').create({
values: {
name: 'new',
@ -527,14 +527,14 @@ describe('acl', () => {
hooks: false,
});
expect(acl.getRole('new')).toBeUndefined();
expect(app.acl.getRole('new')).toBeUndefined();
await app.start();
await app.reload();
expect(acl.getRole('new')).toBeDefined();
expect(app.acl.getRole('new')).toBeDefined();
expect(
acl.can({
app.acl.can({
role: 'new',
resource: 'posts',
action: 'view',

View File

@ -8,12 +8,11 @@ export async function prepareApp() {
plugins: ['error-handler', 'users', 'ui-schema-storage', 'collection-manager'],
});
await app.cleanDb();
app.plugin(PluginACL, {
name: 'acl',
});
await app.loadAndInstall();
await app.loadAndInstall({ clean: true });
await app.db.sync();

View File

@ -269,7 +269,7 @@ export class PluginACL extends Plugin {
// sync database role data to acl
this.app.on('afterLoad', async (app, options) => {
if (options.method === 'install') {
if (options?.method === 'install') {
return;
}
const exists = await this.app.db.collectionExistsInDb('roles');
@ -278,6 +278,13 @@ export class PluginACL extends Plugin {
}
});
this.app.on('afterInstall', async (app, options) => {
const exists = await this.app.db.collectionExistsInDb('roles');
if (exists) {
await this.writeRolesToACL();
}
});
this.app.on('beforeInstallPlugin', async (plugin) => {
if (plugin.constructor.name !== 'UsersPlugin') {
return;

View File

@ -48,7 +48,7 @@ describe('field indexes', () => {
console.log(response3.body);
expect(response3.status).toBe(500);
expect(response3.status).toBe(400);
const response4 = await agent.resource(tableName).create({
values: { title: 't1' },
@ -84,7 +84,7 @@ describe('field indexes', () => {
title: 't1',
},
});
expect(response2.status).toBe(500);
expect(response2.status).toBe(400);
// update field to remove unique constraint
await agent.resource('fields').update({
@ -111,7 +111,7 @@ describe('field indexes', () => {
},
});
expect(response4.status).toBe(500);
expect(response4.status).toBe(400);
// remove a duplicated record
await agent.resource(tableName).destroy({

View File

@ -1,7 +1,6 @@
import PluginErrorHandler from '@nocobase/plugin-error-handler';
import PluginUiSchema from '@nocobase/plugin-ui-schema-storage';
import { mockServer } from '@nocobase/test';
import lodash from 'lodash';
import Plugin from '../';
export async function createApp(options = {}) {
@ -9,10 +8,6 @@ export async function createApp(options = {}) {
acl: false,
});
if (lodash.get(options, 'cleanDB', true)) {
await app.cleanDb();
}
app.plugin(PluginErrorHandler, { name: 'error-handler' });
app.plugin(Plugin, { name: 'collection-manager' });
app.plugin(PluginUiSchema, { name: 'ui-schema-storage' });

View File

@ -0,0 +1,140 @@
import Database from '@nocobase/database';
export function afterCreateForForeignKeyField(db: Database) {
function generateFkOptions(collectionName: string, foreignKey: string) {
const M = db.getModel(collectionName);
const attr = M.rawAttributes[foreignKey];
if (!attr) {
throw new Error(`${collectionName}.${foreignKey} does not exists`);
}
return attribute2field(attr);
}
// Foreign key types are only integer and string
function attribute2field(attribute: any) {
const type = attribute.type.constructor.name === 'INTEGER' ? 'integer' : 'string';
const name = attribute.fieldName;
const data = {
interface: 'integer',
name,
type,
uiSchema: {
type: 'number',
title: name,
'x-component': 'InputNumber',
'x-read-pretty': true,
},
};
if (type === 'string') {
data['interface'] = 'input';
data['uiSchema'] = {
type: 'string',
title: name,
'x-component': 'Input',
'x-read-pretty': true,
};
}
return data;
}
async function createFieldIfNotExists({ values, transaction }) {
const { collectionName, name } = values;
if (!collectionName || !name) {
throw new Error(`field options invalid`);
}
const r = db.getRepository('fields');
const instance = await r.findOne({
filter: {
collectionName,
name,
},
transaction,
});
if (instance) {
if (instance.type !== values.type) {
throw new Error(`fk type invalid`);
}
instance.set('sort',1);
instance.set('isForeignKey', true);
await instance.save({ transaction });
} else {
await r.create({
values: {
isForeignKey: true,
sort:1,
...values,
},
transaction,
});
}
}
return async (model, { transaction, context }) => {
// skip if no app context
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);
await createFieldIfNotExists({
values: {
collectionName: target,
...values,
},
transaction,
});
}
// foreign key in source collection
else if (['obo', 'm2o'].includes(interfaceType)) {
const values = generateFkOptions(collectionName, foreignKey);
await createFieldIfNotExists({
values: { collectionName, ...values },
transaction,
});
}
// foreign key in through collection
else if (['linkTo', 'm2m'].includes(interfaceType)) {
if (type !== 'belongsToMany') {
return;
}
const r = db.getRepository('collections');
const instance = await r.findOne({
filter: {
name: through,
},
transaction,
});
if (!instance) {
await r.create({
values: {
name: through,
title: through,
timestamps: false,
autoGenId: false,
autoCreate: true,
isThrough: true,
},
transaction,
});
}
const opts1 = generateFkOptions(through, foreignKey);
const opts2 = generateFkOptions(through, otherKey);
await createFieldIfNotExists({
values: {
collectionName: through,
...opts1,
},
transaction,
});
await createFieldIfNotExists({
values: {
collectionName: through,
...opts2,
},
transaction,
});
}
};
}

View File

@ -0,0 +1,71 @@
import Database, { FindOneOptions, FindOptions, Model } from '@nocobase/database';
import { Transaction } from 'sequelize';
async function destroyFields(db: Database, transaction: Transaction, fieldRecords: Model[]) {
const fieldsRepo = db.getRepository('fields');
for (const fieldRecord of fieldRecords) {
await fieldsRepo.destroy({
filter: {
name: fieldRecord.get('name'),
collectionName: fieldRecord.get('collectionName'),
},
transaction,
});
}
}
export function afterDestroyForForeignKeyField(db: Database) {
return async (model, opts) => {
const { transaction } = opts;
const options = model.get('options');
if (!options?.isForeignKey) {
return;
}
const collectionRepo = db.getRepository('collections');
const foreignKey = model.get('name');
const foreignKeyCollectionName = model.get('collectionName');
const collectionRecord = await collectionRepo.findOne({
filter: {
name: foreignKeyCollectionName,
},
transaction,
} as FindOneOptions);
const collectionOptions = collectionRecord.get('options');
const fieldsRepo = db.getRepository('fields');
if (collectionOptions?.isThrough) {
// through collection
const fieldRecords = await fieldsRepo.find({
filter: {
options: { through: foreignKeyCollectionName, foreignKey: foreignKey },
},
transaction,
} as FindOptions);
await destroyFields(db, transaction, fieldRecords);
} else {
await destroyFields(
db,
transaction,
await fieldsRepo.find({
filter: {
collectionName: foreignKeyCollectionName,
options: { foreignKey: foreignKey },
},
transaction,
} as FindOptions),
);
await destroyFields(
db,
transaction,
await fieldsRepo.find({
filter: {
options: { foreignKey: foreignKey, target: foreignKeyCollectionName },
},
transaction,
} as FindOptions),
);
}
};
}

View File

@ -0,0 +1,51 @@
import Database from '@nocobase/database';
export function beforeDestroyForeignKey(db: Database) {
return async (model, opts) => {
const { transaction } = opts;
const { isForeignKey, collectionName: fkCollectionName, name: fkName } = model.get();
if (!isForeignKey) {
return;
}
const fieldKeys = [];
for (const [sourceName, collection] of db.collections) {
for (const [, field] of collection.fields) {
const fieldKey = field.options?.key;
if (!fieldKey) {
continue;
}
// fk in source collection
if (field.type === 'belongsTo') {
if (sourceName === fkCollectionName && field.foreignKey === fkName) {
fieldKeys.push(fieldKey);
}
}
// fk in target collection
else if (field.type === 'hasOne' || field.type === 'hasMany') {
if (fkCollectionName === field.target && field.foreignKey === fkName) {
fieldKeys.push(fieldKey);
}
}
// fk in through collection
else if (field.type === 'belongsToMany' && field.through === fkCollectionName) {
console.log(field.foreignKey, field.otherKey);
if (field.foreignKey === fkName || field.otherKey === fkName) {
fieldKeys.push(fieldKey);
}
}
}
}
const r = db.getRepository('fields');
await r.destroy({
filter: {
'key.$in': fieldKeys,
},
transaction,
});
};
}

View File

@ -1,5 +1,7 @@
export * from './afterCreateForForeignKeyField';
export * from './afterCreateForReverseField';
export * from './beforeCreateForChildrenCollection';
export * from './beforeCreateForReverseField';
export * from './beforeDestroyForeignKey';
export * from './beforeInitOptions';

View File

@ -8,9 +8,11 @@ import { Plugin } from '@nocobase/server';
import { CollectionRepository } from '.';
import {
afterCreateForForeignKeyField,
afterCreateForReverseField,
beforeCreateForChildrenCollection,
beforeCreateForReverseField,
beforeDestroyForeignKey,
beforeInitOptions
} from './hooks';
import { CollectionModel, FieldModel } from './models';
@ -81,6 +83,8 @@ export class CollectionManagerPlugin extends Plugin {
});
}
});
// after migrate
this.app.db.on('fields.afterCreate', afterCreateForForeignKeyField(this.app.db));
this.app.db.on('fields.afterUpdate', async (model: FieldModel, { context, transaction }) => {
const prevOptions = model.previous('options');
@ -109,6 +113,8 @@ export class CollectionManagerPlugin extends Plugin {
}
});
// before field remove
this.app.db.on('fields.beforeDestroy', beforeDestroyForeignKey(this.app.db));
this.app.db.on('fields.beforeDestroy', async (model, options) => {
await model.remove(options);
});

View File

@ -27,6 +27,19 @@ export class PresetNocoBase extends Plugin {
}
afterAdd() {
this.app.on('beforeLoad', async (app, options) => {
if (options?.method !== 'upgrade') {
return;
}
const result = await this.app.version.satisfies('<0.8.0-alpha.1');
if (result) {
const r = await this.db.collectionExistsInDb('applicationPlugins');
if (r) {
await this.db.getRepository('applicationPlugins').destroy({ truncate: true });
await this.app.reload();
}
}
});
this.app.on('beforeUpgrade', async () => {
const result = await this.app.version.satisfies('<0.8.0-alpha.1');
if (result) {

View File

@ -18537,7 +18537,7 @@ rc-cascader@~3.2.1:
"@babel/runtime" "^7.12.5"
array-tree-filter "^2.1.0"
classnames "^2.3.1"
rc-select "~14.0.2"
rc-select "~14.0.0-alpha.23"
rc-tree "~5.4.3"
rc-util "^5.6.1"
@ -18772,6 +18772,19 @@ rc-resize-observer@^1.2.0:
rc-util "^5.15.0"
resize-observer-polyfill "^1.5.1"
rc-select@~14.0.0-alpha.23, rc-select@~14.0.0-alpha.8:
version "14.0.6"
resolved "https://registry.npmjs.org/rc-select/-/rc-select-14.0.6.tgz#93be0b185a9d66dc84795e079121f0f65310d8bf"
integrity sha512-HMb2BwfTvBxMmIWTR/afP4bcRJLbVKFSBW/VFfL5Z+kdV2XlrYdlliK2uHY7pRRvW16PPGwmOwGfV+eoulPINw==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "2.x"
rc-motion "^2.0.1"
rc-overflow "^1.0.0"
rc-trigger "^5.0.4"
rc-util "^5.16.1"
rc-virtual-list "^3.2.0"
rc-select@~14.0.2:
version "14.0.5"
resolved "https://registry.npmjs.org/rc-select/-/rc-select-14.0.5.tgz#145c42e7fd66a7fc6c5c56f6b0cf35d8b50f9e23"
@ -18874,7 +18887,7 @@ rc-tree-select@~5.1.1:
dependencies:
"@babel/runtime" "^7.10.1"
classnames "2.x"
rc-select "~14.0.2"
rc-select "~14.0.0-alpha.8"
rc-tree "~5.4.3"
rc-util "^5.16.1"
@ -23296,6 +23309,11 @@ xmldom-sre@^0.1.31:
resolved "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz#10860d5bab2c603144597d04bf2c4980e98067f4"
integrity sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==
xpipe@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/xpipe/-/xpipe-1.0.5.tgz#8dd8bf45fc3f7f55f0e054b878f43a62614dafdf"
integrity sha512-tuqoLk8xPl0o+7ny9iPlEZuzjfy1zC5ZJtAGjDDZWmVTVBK5PJP0arMGVu3Y53zSyeYK+YonMVSUv0DJgGN/ig==
xregexp@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"