From f67afba9640fb129f70b1e9b269eb93105dde69d Mon Sep 17 00:00:00 2001 From: chenos Date: Fri, 28 Oct 2022 15:09:14 +0800 Subject: [PATCH] feat: improve code (#978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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: 唐小爱 Co-authored-by: lyf-coder Co-authored-by: katherinehhh --- packages/core/actions/src/actions/move.ts | 2 +- .../CollectionManagerProvider.tsx | 19 +-- .../CollectionManagerShortcut.tsx | 4 +- .../Configuration/AddFieldAction.tsx | 127 ++++++++++------ .../Configuration/EditFieldAction.tsx | 97 +++++++----- .../Configuration/index.tsx | 3 + .../Configuration/schemas/collectionFields.ts | 4 +- .../client/src/collection-manager/index.tsx | 2 + .../core/client/src/pm/PluginManagerLink.tsx | 5 + .../route-switch/antd/admin-layout/index.tsx | 5 +- packages/core/database/src/database.ts | 2 + .../src/fields/belongs-to-many-field.ts | 6 + packages/core/database/src/fields/field.ts | 4 +- packages/core/server/package.json | 3 +- packages/core/server/src/application.ts | 28 ++-- .../src/plugin-manager/PluginManager.ts | 34 ++++- packages/core/test/src/mockServer.ts | 4 +- .../plugins/acl/src/__tests__/acl.test.ts | 10 +- packages/plugins/acl/src/__tests__/prepare.ts | 5 +- packages/plugins/acl/src/server.ts | 9 +- .../__tests__/field-options/indexes.test.ts | 6 +- .../collection-manager/src/__tests__/index.ts | 5 - .../hooks/afterCreateForForeignKeyField.ts | 140 ++++++++++++++++++ .../hooks/afterDestroyForForeignKeyField.ts | 71 +++++++++ .../src/hooks/beforeDestroyForeignKey.ts | 51 +++++++ .../collection-manager/src/hooks/index.ts | 2 + .../plugins/collection-manager/src/server.ts | 6 + packages/presets/nocobase/src/index.ts | 13 ++ yarn.lock | 22 ++- 29 files changed, 545 insertions(+), 144 deletions(-) create mode 100644 packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts create mode 100644 packages/plugins/collection-manager/src/hooks/afterDestroyForForeignKeyField.ts create mode 100644 packages/plugins/collection-manager/src/hooks/beforeDestroyForeignKey.ts diff --git a/packages/core/actions/src/actions/move.ts b/packages/core/actions/src/actions/move.ts index 28b131028b..7631b57ad5 100644 --- a/packages/core/actions/src/actions/move.ts +++ b/packages/core/actions/src/actions/move.ts @@ -27,7 +27,7 @@ export async function move(ctx: Context, next) { await sortAbleCollection.sticky(sourceId); } } - + ctx.body = 'ok'; await next(); } diff --git a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx index f412f55c01..19d75d68e8 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx @@ -14,11 +14,7 @@ export const CollectionManagerProvider: React.FC = (pr service, interfaces: { ...defaultInterfaces, ...interfaces }, collections, - refreshCM: async () => { - if (refreshCM) { - await refreshCM(); - } - }, + refreshCM, }} > {props.children} @@ -47,13 +43,18 @@ export const RemoteCollectionManagerProvider = (props: any) => { } return ( { - setContentLoading(true); + refreshCM={async (opts) => { + if (opts?.reload) { + setContentLoading(true); + } const { data } = await api.request(options); service.mutate(data); - setContentLoading(false); + if (opts?.reload) { + setContentLoading(false); + } + return data?.data || []; }} {...props} /> diff --git a/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx b/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx index 4a5360d6b0..3a2651f756 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx @@ -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 ( - + ); }; diff --git a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx index 4c89f52823..85bd6b1c3b 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx @@ -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,53 +137,71 @@ const useCreateCollectionField = () => { }; }; -export const AddFieldAction = () => { +export const AddCollectionField = (props) => { + const record = useRecord(); + return ; +}; + +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 ( - - { - const schema = getSchema(getInterface(info.key), record, compile); - setSchema(schema); - setVisible(true); - }} - > - {options.map((option) => { - return ( - option.children.length > 0 && ( - - {option.children - .filter((child) => !['o2o', 'subTable'].includes(child.name)) - .map((child) => { - return {compile(child.title)}; - })} - - ) - ); - })} - - } - > - - - - + + + { + const schema = getSchema(getInterface(info.key), record, compile); + setSchema(schema); + setVisible(true); + }} + > + {options.map((option) => { + return ( + option.children.length > 0 && ( + + {option.children + .filter((child) => !['o2o', 'subTable'].includes(child.name)) + .map((child) => { + return {compile(child.title)}; + })} + + ) + ); + })} + + } + > + {children || ( + + )} + + + + ); }; diff --git a/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx index 70f464e4eb..656288ff76 100644 --- a/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx @@ -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 ; +}; + +export const EditFieldAction = (props) => { + const { scope, getContainer, item: record,children } = props; const { getInterface } = useCollectionManager(); const [visible, setVisible] = useState(false); const [schema, setSchema] = useState({}); @@ -118,42 +123,52 @@ export const EditFieldAction = (props) => { const { t } = useTranslation(); const compile = useCompile(); const [data, setData] = useState({}); + return ( - - { - const { data } = await api.resource('collections.fields', record.collectionName).get({ - filterByTk: record.name, - appends: ['uiSchema', 'reverseField'], - }); - setData(data?.data); - const interfaceConf = getInterface(record.interface); - const defaultValues: any = cloneDeep(data?.data) || {}; - if (!defaultValues?.reverseField) { - defaultValues.autoCreateReverseField = false; - defaultValues.reverseField = interfaceConf.default?.reverseField; - set(defaultValues.reverseField, 'name', `f_${uid()}`); - set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title); - } - const schema = getSchema( - { - ...interfaceConf, - default: defaultValues, - }, - record, - compile, - ); - setSchema(schema); - setVisible(true); - }} - > - {t('Edit')} - - - + + + { + const { data } = await api.resource('collections.fields', record.collectionName).get({ + filterByTk: record.name, + appends: ['uiSchema', 'reverseField'], + }); + setData(data?.data); + const interfaceConf = getInterface(record.interface); + const defaultValues: any = cloneDeep(data?.data) || {}; + if (!defaultValues?.reverseField) { + defaultValues.autoCreateReverseField = false; + defaultValues.reverseField = interfaceConf.default?.reverseField; + set(defaultValues.reverseField, 'name', `f_${uid()}`); + set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title); + } + const schema = getSchema( + { + ...interfaceConf, + default: defaultValues, + }, + record, + compile, + getContainer, + ); + setSchema(schema); + setVisible(true); + }} + > + {children||t('Edit')} + + + + ); }; diff --git a/packages/core/client/src/collection-manager/Configuration/index.tsx b/packages/core/client/src/collection-manager/Configuration/index.tsx index 40b8a121ce..00e42cb033 100644 --- a/packages/core/client/src/collection-manager/Configuration/index.tsx +++ b/packages/core/client/src/collection-manager/Configuration/index.tsx @@ -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_-]*$/, diff --git a/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts b/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts index 11aaff8eff..c68abc9e71 100644 --- a/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts +++ b/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts @@ -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', }, diff --git a/packages/core/client/src/collection-manager/index.tsx b/packages/core/client/src/collection-manager/index.tsx index 437328ddc3..33f33e83aa 100644 --- a/packages/core/client/src/collection-manager/index.tsx +++ b/packages/core/client/src/collection-manager/index.tsx @@ -9,4 +9,6 @@ export * from './context'; export * from './hooks'; export * from './ResourceActionProvider'; export * from './types'; +export * from './Configuration'; + diff --git a/packages/core/client/src/pm/PluginManagerLink.tsx b/packages/core/client/src/pm/PluginManagerLink.tsx index 505f30d7c0..d2ed7fe441 100644 --- a/packages/core/client/src/pm/PluginManagerLink.tsx +++ b/packages/core/client/src/pm/PluginManagerLink.tsx @@ -66,6 +66,10 @@ export const SettingsCenterDropdown = () => { title: t('Workflow'), path: 'workflow/workflows', }, + { + title: t('Graph Collections'), + path: 'graph/collections', + }, ]; return ( @@ -79,6 +83,7 @@ export const SettingsCenterDropdown = () => { onClick={() => { history.push('/admin/settings/' + item.path); }} + key={item.path} > {item.title} diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index 62e0e7272a..18f53a8f5f 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -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; } `} > diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index 3698140947..bae45f1f90 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -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 = { diff --git a/packages/core/database/src/fields/belongs-to-many-field.ts b/packages/core/database/src/fields/belongs-to-many-field.ts index 775701d7fd..d13dcb4446 100644 --- a/packages/core/database/src/fields/belongs-to-many-field.ts +++ b/packages/core/database/src/fields/belongs-to-many-field.ts @@ -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 的关联字段 diff --git a/packages/core/database/src/fields/field.ts b/packages/core/database/src/fields/field.ts index 0496784db4..115dcd6dff 100644 --- a/packages/core/database/src/fields/field.ts +++ b/packages/core/database/src/fields/field.ts @@ -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); } diff --git a/packages/core/server/package.json b/packages/core/server/package.json index ee23f7fca0..99bd779c31 100644 --- a/packages/core/server/package.json +++ b/packages/core/server/package.json @@ -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" diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 734c501211..2c7fa6a98a 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -207,10 +207,15 @@ export class Application exten this._events = []; // @ts-ignore this._eventsCount = []; + this.removeAllListeners(); this.middleware = new Toposort(); // this.context = Object.create(context); this.plugins = new Map(); 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 exten this.context.resourcer = this._resourcer; this.context.cache = this._cache; - this._pm = new PluginManager({ - app: this, - }); + 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 exten registerActions(this); } - this.loadPluginConfig(options.plugins || []); - registerCli(this); this._version = new ApplicationVersion(this); @@ -264,16 +272,6 @@ export class Application 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( middleware: Koa.Middleware, diff --git a/packages/core/server/src/plugin-manager/PluginManager.ts b/packages/core/server/src/plugin-manager/PluginManager.ts index bd362c8aa2..fe8ccfc54a 100644 --- a/packages/core/server/src/plugin-manager/PluginManager.ts +++ b/packages/core/server/src/plugin-manager/PluginManager.ts @@ -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(); 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 }, }); diff --git a/packages/core/test/src/mockServer.ts b/packages/core/test/src/mockServer.ts index d9fe5f83ff..034fdffce8 100644 --- a/packages/core/test/src/mockServer.ts +++ b/packages/core/test/src/mockServer.ts @@ -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, }, diff --git a/packages/plugins/acl/src/__tests__/acl.test.ts b/packages/plugins/acl/src/__tests__/acl.test.ts index 51f56205bc..0f28b693a5 100644 --- a/packages/plugins/acl/src/__tests__/acl.test.ts +++ b/packages/plugins/acl/src/__tests__/acl.test.ts @@ -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', diff --git a/packages/plugins/acl/src/__tests__/prepare.ts b/packages/plugins/acl/src/__tests__/prepare.ts index e8ed109c84..8a2dcc007d 100644 --- a/packages/plugins/acl/src/__tests__/prepare.ts +++ b/packages/plugins/acl/src/__tests__/prepare.ts @@ -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(); diff --git a/packages/plugins/acl/src/server.ts b/packages/plugins/acl/src/server.ts index 8a487a06fd..a6a1aea0f9 100644 --- a/packages/plugins/acl/src/server.ts +++ b/packages/plugins/acl/src/server.ts @@ -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; diff --git a/packages/plugins/collection-manager/src/__tests__/field-options/indexes.test.ts b/packages/plugins/collection-manager/src/__tests__/field-options/indexes.test.ts index 94b88bae0e..91e9f20f59 100644 --- a/packages/plugins/collection-manager/src/__tests__/field-options/indexes.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/field-options/indexes.test.ts @@ -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({ diff --git a/packages/plugins/collection-manager/src/__tests__/index.ts b/packages/plugins/collection-manager/src/__tests__/index.ts index ad86091d6f..6aede15ecc 100644 --- a/packages/plugins/collection-manager/src/__tests__/index.ts +++ b/packages/plugins/collection-manager/src/__tests__/index.ts @@ -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' }); diff --git a/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts b/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts new file mode 100644 index 0000000000..0f983983ad --- /dev/null +++ b/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts @@ -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, + }); + } + }; +} diff --git a/packages/plugins/collection-manager/src/hooks/afterDestroyForForeignKeyField.ts b/packages/plugins/collection-manager/src/hooks/afterDestroyForForeignKeyField.ts new file mode 100644 index 0000000000..8d8e640d6e --- /dev/null +++ b/packages/plugins/collection-manager/src/hooks/afterDestroyForForeignKeyField.ts @@ -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), + ); + } + + }; +} diff --git a/packages/plugins/collection-manager/src/hooks/beforeDestroyForeignKey.ts b/packages/plugins/collection-manager/src/hooks/beforeDestroyForeignKey.ts new file mode 100644 index 0000000000..7054a27bd8 --- /dev/null +++ b/packages/plugins/collection-manager/src/hooks/beforeDestroyForeignKey.ts @@ -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, + }); + }; +} diff --git a/packages/plugins/collection-manager/src/hooks/index.ts b/packages/plugins/collection-manager/src/hooks/index.ts index 931083d84e..5d7093e335 100644 --- a/packages/plugins/collection-manager/src/hooks/index.ts +++ b/packages/plugins/collection-manager/src/hooks/index.ts @@ -1,5 +1,7 @@ +export * from './afterCreateForForeignKeyField'; export * from './afterCreateForReverseField'; export * from './beforeCreateForChildrenCollection'; export * from './beforeCreateForReverseField'; +export * from './beforeDestroyForeignKey'; export * from './beforeInitOptions'; diff --git a/packages/plugins/collection-manager/src/server.ts b/packages/plugins/collection-manager/src/server.ts index db11c133ee..742df3abac 100644 --- a/packages/plugins/collection-manager/src/server.ts +++ b/packages/plugins/collection-manager/src/server.ts @@ -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); }); diff --git a/packages/presets/nocobase/src/index.ts b/packages/presets/nocobase/src/index.ts index 7593926c79..4451ec94e5 100644 --- a/packages/presets/nocobase/src/index.ts +++ b/packages/presets/nocobase/src/index.ts @@ -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) { diff --git a/yarn.lock b/yarn.lock index 43fb1b1ee0..6bc0a9be73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"