diff --git a/packages/core/client/src/application/schema-initializer/types.ts b/packages/core/client/src/application/schema-initializer/types.ts index 188ee33f08..8a51a083b5 100644 --- a/packages/core/client/src/application/schema-initializer/types.ts +++ b/packages/core/client/src/application/schema-initializer/types.ts @@ -110,7 +110,7 @@ export interface SchemaInitializerOptions { insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; designable?: boolean; wrap?: (s: ISchema, options?: any) => ISchema; - useWrap?: () => ((s: ISchema, options?: any) => ISchema); + useWrap?: () => (s: ISchema, options?: any) => ISchema; onSuccess?: (data: any) => void; insert?: InsertType; useInsert?: () => InsertType; diff --git a/packages/core/client/src/application/schema-initializer/withInitializer.tsx b/packages/core/client/src/application/schema-initializer/withInitializer.tsx index 997a571d5e..a20515aa21 100644 --- a/packages/core/client/src/application/schema-initializer/withInitializer.tsx +++ b/packages/core/client/src/application/schema-initializer/withInitializer.tsx @@ -108,7 +108,7 @@ export function withInitializer(C: ComponentType) { {...popoverProps} arrow={false} overlayClassName={overlayClassName} - open={visible} + open={visible} onOpenChange={setVisible} content={wrapSSR(
{ return { key: setting.name, icon: setting.icon, - label: setting.link ?
window.open(setting.link)}>{compile(setting.title)}
: + label: setting.link ? ( +
window.open(setting.link)}>{compile(setting.title)}
+ ) : ( {compile(setting.title)} + ), }; }); }, [app, t]); diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index cc57119fc5..a77a3c02c1 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -16,7 +16,7 @@ import fs from 'fs'; import type { TFuncKey, TOptions } from 'i18next'; import { resolve } from 'path'; import { Application } from './application'; -import { InstallOptions, getExposeChangelogUrl, getExposeReadmeUrl } from './plugin-manager'; +import { getExposeChangelogUrl, getExposeReadmeUrl, InstallOptions } from './plugin-manager'; import { checkAndGetCompatible, getPluginBasePath } from './plugin-manager/utils'; export interface PluginInterface { @@ -138,6 +138,7 @@ export abstract class Plugin implements PluginInterface { if (!this.name) { throw new Error(`plugin name invalid`); } + await this.app.syncMessageManager.publish(this.name, message, options); } @@ -172,13 +173,6 @@ export abstract class Plugin implements PluginInterface { }); } - private async getPluginBasePath() { - if (!this.options.packageName) { - return; - } - return getPluginBasePath(this.options.packageName); - } - /** * @internal */ @@ -245,6 +239,13 @@ export abstract class Plugin implements PluginInterface { return results; } + + private async getPluginBasePath() { + if (!this.options.packageName) { + return; + } + return getPluginBasePath(this.options.packageName); + } } export default Plugin; diff --git a/packages/core/server/src/sync-message-manager.ts b/packages/core/server/src/sync-message-manager.ts index 298e7822a2..4769e1dc66 100644 --- a/packages/core/server/src/sync-message-manager.ts +++ b/packages/core/server/src/sync-message-manager.ts @@ -39,17 +39,18 @@ export class SyncMessageManager { const timer = setTimeout(() => { reject(new Error('publish timeout')); }, 5000); + transaction.afterCommit(async () => { try { const r = await this.app.pubSubManager.publish(`${this.app.name}.sync.${channel}`, message, { skipSelf: true, ...others, }); - clearTimeout(timer); resolve(r); } catch (error) { - clearTimeout(timer); reject(error); + } finally { + clearTimeout(timer); } }); }); diff --git a/packages/core/test/src/server/mock-data-source.ts b/packages/core/test/src/server/mock-data-source.ts new file mode 100644 index 0000000000..9e8ca8d773 --- /dev/null +++ b/packages/core/test/src/server/mock-data-source.ts @@ -0,0 +1,25 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { CollectionManager, DataSource } from '@nocobase/data-source-manager'; +import { waitSecond } from '@nocobase/test'; + +export class MockDataSource extends DataSource { + static testConnection(options?: any): Promise { + return Promise.resolve(true); + } + + async load(): Promise { + await waitSecond(1000); + } + + createCollectionManager(options?: any): any { + return new CollectionManager(options); + } +} diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 1c6b3bbc97..465ab4d705 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -11,10 +11,10 @@ import { mockDatabase } from '@nocobase/database'; import { Application, ApplicationOptions, AppSupervisor, Gateway, PluginManager } from '@nocobase/server'; import { uid } from '@nocobase/utils'; import jwt from 'jsonwebtoken'; -import _ from 'lodash'; import qs from 'qs'; import supertest, { SuperAgentTest } from 'supertest'; import { MemoryPubSubAdapter } from './memory-pub-sub-adapter'; +import { MockDataSource } from './mock-data-source'; interface ActionParams { filterByTk?: any; @@ -77,6 +77,10 @@ interface ExtendedAgent extends SuperAgentTest { } export class MockServer extends Application { + registerMockDataSource() { + this.dataSourceManager.factory.register('mock', MockDataSource); + } + async loadAndInstall(options: any = {}) { await this.load({ method: 'install' }); @@ -231,13 +235,15 @@ export function mockServer(options: ApplicationOptions = {}) { PluginManager.findPackagePatched = true; } - const app = new MockServer({ + const mockServerOptions = { acl: false, syncMessageManager: { debounce: 500, }, ...options, - }); + }; + + const app = new MockServer(mockServerOptions); const basename = app.options.pubSubManager?.channelPrefix; @@ -280,16 +286,27 @@ export async function createMockCluster({ ...options }: MockClusterOptions = {}) { const nodes: MockServer[] = []; + let dbOptions; + for (let i = 0; i < number; i++) { + if (dbOptions) { + options['database'] = { + ...dbOptions, + }; + } + const app: MockServer = await createMockServer({ ...options, skipSupervisor: true, name: clusterName + '_' + appName, - skipInstall: Boolean(i), pubSubManager: { channelPrefix: clusterName, }, }); + + if (!dbOptions) { + dbOptions = app.db.options; + } console.log('-------------', await app.isInstalled()); nodes.push(app); } diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/model/RoleResourceModel.ts b/packages/plugins/@nocobase/plugin-acl/src/server/model/RoleResourceModel.ts index 2c48fcc161..5559e899d4 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/server/model/RoleResourceModel.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/server/model/RoleResourceModel.ts @@ -17,7 +17,7 @@ export class RoleResourceModel extends Model { role.revokeResource(resourceName); } - async writeToACL(options: { acl: ACL; transaction: any }) { + async writeToACL(options: { acl: ACL; transaction?: any }) { const { acl } = options; const resourceName = this.get('name') as string; const roleName = this.get('roleName') as string; diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts new file mode 100644 index 0000000000..ce64564fc2 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts @@ -0,0 +1,235 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { createMockCluster, sleep } from '@nocobase/test'; + +describe('cluster', () => { + let cluster; + beforeEach(async () => { + cluster = await createMockCluster({ + plugins: ['error-handler', 'data-source-main', 'ui-schema-storage'], + acl: false, + }); + }); + + afterEach(async () => { + await cluster.destroy(); + }); + + describe('sync collection', () => { + test('create collection', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + }); + + test('update collection', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + description: 'test collection', + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + + await app1.db.getRepository('collections').update({ + filterByTk: 'tests', + values: { + description: 'new test collection', + }, + context: {}, + }); + + await sleep(2000); + + expect(testsCollection.options.description).toBe('new test collection'); + }); + + test('destroy collection', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + + await app1.db.getRepository('collections').destroy({ + filterByTk: 'tests', + context: {}, + }); + + await sleep(2000); + + expect(app2.db.getCollection('tests')).toBeFalsy(); + }); + }); + + describe('sync fields', () => { + test('create field', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + + await app1.db.getRepository('fields').create({ + values: { + name: 'age', + type: 'integer', + collectionName: 'tests', + }, + context: {}, + }); + + await sleep(2000); + + const ageField = testsCollection.getField('age'); + expect(ageField).toBeTruthy(); + }); + + test('update field', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + + await app1.db.getRepository('fields').create({ + values: { + name: 'age', + type: 'integer', + collectionName: 'tests', + }, + context: {}, + }); + + await sleep(2000); + + await app1.db.getRepository('collections.fields', 'tests').update({ + filterByTk: 'age', + values: { + description: 'age field', + }, + context: {}, + }); + + await sleep(2000); + + const ageField = testsCollection.getField('age'); + expect(ageField).toBeTruthy(); + + expect(ageField.options.description).toBe('age field'); + }); + + test('destroy field', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + { + name: 'age', + type: 'integer', + }, + ], + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + + await app1.db.getRepository('collections.fields', 'tests').destroy({ + filterByTk: 'age', + context: {}, + }); + + await sleep(2000); + + expect(testsCollection.getField('age')).toBeFalsy(); + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts index ab61bee1e5..acf95af08e 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts @@ -43,7 +43,8 @@ export class PluginDataSourceMainServer extends Plugin { async handleSyncMessage(message) { const { type, collectionName } = message; - if (type === 'newCollection') { + + if (type === 'syncCollection') { const collectionModel: CollectionModel = await this.app.db.getCollection('collections').repository.findOne({ filter: { name: collectionName, @@ -52,6 +53,26 @@ export class PluginDataSourceMainServer extends Plugin { await collectionModel.load(); } + + if (type === 'removeField') { + const { collectionName, fieldName } = message; + const collection = this.app.db.getCollection(collectionName); + if (!collection) { + return; + } + + return collection.removeFieldFromDb(fieldName); + } + + if (type === 'removeCollection') { + const { collectionName } = message; + const collection = this.app.db.getCollection(collectionName); + if (!collection) { + return; + } + + collection.remove(); + } } async beforeLoad() { @@ -92,10 +113,15 @@ export class PluginDataSourceMainServer extends Plugin { transaction, }); - this.sendSyncMessage({ - type: 'newCollection', - collectionName: model.get('name'), - }); + this.sendSyncMessage( + { + type: 'syncCollection', + collectionName: model.get('name'), + }, + { + transaction, + }, + ); } }, ); @@ -113,6 +139,16 @@ export class PluginDataSourceMainServer extends Plugin { } await model.remove(removeOptions); + + this.sendSyncMessage( + { + type: 'removeCollection', + collectionName: model.get('name'), + }, + { + transaction: options.transaction, + }, + ); }); // 要在 beforeInitOptions 之前处理 @@ -262,6 +298,16 @@ export class PluginDataSourceMainServer extends Plugin { }; await collection.sync(syncOptions); + + this.sendSyncMessage( + { + type: 'syncCollection', + collectionName: model.get('collectionName'), + }, + { + transaction, + }, + ); } }); @@ -273,6 +319,17 @@ export class PluginDataSourceMainServer extends Plugin { this.app.db.on('fields.beforeDestroy', async (model: FieldModel, options) => { await mutex.runExclusive(async () => { await model.remove(options); + + this.sendSyncMessage( + { + type: 'removeField', + collectionName: model.get('collectionName'), + fieldName: model.get('name'), + }, + { + transaction: options.transaction, + }, + ); }); }); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/cluster.test.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/cluster.test.ts new file mode 100644 index 0000000000..f5154cc062 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/cluster.test.ts @@ -0,0 +1,49 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { createMockCluster, waitSecond } from '@nocobase/test'; + +describe('cluster', () => { + let cluster; + beforeEach(async () => { + cluster = await createMockCluster({ + plugins: ['nocobase'], + acl: false, + }); + + for (const node of cluster.nodes) { + node.registerMockDataSource(); + } + }); + + afterEach(async () => { + await cluster.destroy(); + }); + + test('create data source', async () => { + const app1 = cluster.nodes[0]; + + await app1.db.getRepository('dataSources').create({ + values: { + key: 'mockInstance1', + type: 'mock', + displayName: 'Mock', + options: {}, + }, + }); + + await waitSecond(2000); + + const dataSource = app1.dataSourceManager.get('mockInstance1'); + expect(dataSource).toBeDefined(); + + const dataSourceInApp2 = cluster.nodes[1].dataSourceManager.get('mockInstance1'); + expect(dataSourceInApp2).toBeDefined(); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts index 8df2e0677f..8200b7886f 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts @@ -36,6 +36,96 @@ export class PluginDataSourceManagerServer extends Plugin { [dataSourceKey: string]: DataSourceState; } = {}; + async handleSyncMessage(message) { + const { type } = message; + if (type === 'syncRole') { + const { roleName, dataSourceKey } = message; + const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey); + + const dataSourceRole: DataSourcesRolesModel = await this.app.db.getRepository('dataSourcesRoles').findOne({ + filter: { + dataSourceKey, + roleName, + }, + }); + + await dataSourceRole.writeToAcl({ + acl: dataSource.acl, + }); + } + + if (type === 'syncRoleResource') { + const { roleName, dataSourceKey, resourceName } = message; + const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey); + + const dataSourceRoleResource: DataSourcesRolesResourcesModel = await this.app.db + .getRepository('dataSourcesRolesResources') + .findOne({ + filter: { + dataSourceKey, + roleName, + name: resourceName, + }, + }); + + await dataSourceRoleResource.writeToACL({ + acl: dataSource.acl, + }); + } + if (type === 'loadDataSource') { + const { dataSourceKey } = message; + const dataSourceModel = await this.app.db.getRepository('dataSources').findOne({ + filter: { + key: dataSourceKey, + }, + }); + + if (!dataSourceModel) { + return; + } + + await dataSourceModel.loadIntoApplication({ + app: this.app, + }); + } + + if (type === 'loadDataSourceField') { + const { key } = message; + const fieldModel = await this.app.db.getRepository('dataSourcesFields').findOne({ + filter: { + key, + }, + }); + + fieldModel.load({ + app: this.app, + }); + } + if (type === 'removeDataSourceCollection') { + const { dataSourceKey, collectionName } = message; + const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey); + dataSource.collectionManager.removeCollection(collectionName); + } + + if (type === 'removeDataSourceField') { + const { key } = message; + const fieldModel = await this.app.db.getRepository('dataSourcesFields').findOne({ + filter: { + key, + }, + }); + + fieldModel.unload({ + app: this.app, + }); + } + + if (type === 'removeDataSource') { + const { dataSourceKey } = message; + this.app.dataSourceManager.dataSources.delete(dataSourceKey); + } + } + async beforeLoad() { this.app.db.registerModels({ DataSourcesCollectionModel, @@ -100,6 +190,16 @@ export class PluginDataSourceManagerServer extends Plugin { model.loadIntoApplication({ app: this.app, }); + + this.sendSyncMessage( + { + type: 'loadDataSource', + dataSourceKey: model.get('key'), + }, + { + transaction: options.transaction, + }, + ); } }); @@ -264,6 +364,7 @@ export class PluginDataSourceManagerServer extends Plugin { } }); + const self = this; this.app.actions({ async ['dataSources:listEnabled'](ctx, next) { const dataSources = await ctx.db.getRepository('dataSources').find({ @@ -302,6 +403,7 @@ export class PluginDataSourceManagerServer extends Plugin { async ['dataSources:refresh'](ctx, next) { const { filterByTk, clientStatus } = ctx.action.params; + const dataSourceModel: DataSourceModel = await ctx.db.getRepository('dataSources').findOne({ filter: { key: filterByTk, @@ -317,6 +419,11 @@ export class PluginDataSourceManagerServer extends Plugin { dataSourceModel.loadIntoApplication({ app: ctx.app, }); + + ctx.app.syncMessageManager.publish(self.name, { + type: 'loadDataSource', + dataSourceKey: dataSourceModel.get('key'), + }); } ctx.body = { @@ -352,21 +459,52 @@ export class PluginDataSourceManagerServer extends Plugin { } }); - this.app.db.on('dataSourcesCollections.afterDestroy', async (model: DataSourcesCollectionModel) => { + this.app.db.on('dataSourcesCollections.afterDestroy', async (model: DataSourcesCollectionModel, options) => { const dataSource = this.app.dataSourceManager.dataSources.get(model.get('dataSourceKey')); dataSource.collectionManager.removeCollection(model.get('name')); + + this.sendSyncMessage( + { + type: 'removeDataSourceCollection', + dataSourceKey: model.get('dataSourceKey'), + collectionName: model.get('name'), + }, + { + transaction: options.transaction, + }, + ); }); - this.app.db.on('dataSourcesFields.afterSaveWithAssociations', async (model: DataSourcesFieldModel) => { + this.app.db.on('dataSourcesFields.afterSaveWithAssociations', async (model: DataSourcesFieldModel, options) => { model.load({ app: this.app, }); + + this.sendSyncMessage( + { + type: 'loadDataSourceField', + key: model.get('key'), + }, + { + transaction: options.transaction, + }, + ); }); - this.app.db.on('dataSourcesFields.afterDestroy', async (model: DataSourcesFieldModel) => { + this.app.db.on('dataSourcesFields.afterDestroy', async (model: DataSourcesFieldModel, options) => { model.unload({ app: this.app, }); + + this.sendSyncMessage( + { + type: 'removeDataSourceField', + key: model.get('key'), + }, + { + transaction: options.transaction, + }, + ); }); this.app.db.on( @@ -379,8 +517,18 @@ export class PluginDataSourceManagerServer extends Plugin { }, ); - this.app.db.on('dataSources.afterDestroy', async (model: DataSourceModel) => { + this.app.db.on('dataSources.afterDestroy', async (model: DataSourceModel, options) => { this.app.dataSourceManager.dataSources.delete(model.get('key')); + + this.sendSyncMessage( + { + type: 'removeDataSource', + dataSourceKey: model.get('key'), + }, + { + transaction: options.transaction, + }, + ); }); this.app.on('afterStart', async (app: Application) => { @@ -412,6 +560,19 @@ export class PluginDataSourceManagerServer extends Plugin { acl: dataSource.acl, transaction: transaction, }); + + // sync roles resources between nodes + this.sendSyncMessage( + { + type: 'syncRoleResource', + roleName: model.get('roleName'), + dataSourceKey: model.get('dataSourceKey'), + resourceName: model.get('name'), + }, + { + transaction, + }, + ); }, ); @@ -429,6 +590,18 @@ export class PluginDataSourceManagerServer extends Plugin { acl: dataSource.acl, transaction: transaction, }); + + this.sendSyncMessage( + { + type: 'syncRoleResource', + roleName: resource.get('roleName'), + dataSourceKey: resource.get('dataSourceKey'), + resourceName: resource.get('name'), + }, + { + transaction, + }, + ); }, ); @@ -440,6 +613,18 @@ export class PluginDataSourceManagerServer extends Plugin { if (role) { role.revokeResource(model.get('name')); } + + this.sendSyncMessage( + { + type: 'syncRoleResource', + roleName, + dataSourceKey: model.get('dataSourceKey'), + resourceName: model.get('name'), + }, + { + transaction: options.transaction, + }, + ); }); this.app.db.on('dataSourcesRoles.afterSave', async (model: DataSourcesRolesModel, options) => { @@ -462,6 +647,18 @@ export class PluginDataSourceManagerServer extends Plugin { hooks: false, transaction, }); + + // sync role between nodes + this.sendSyncMessage( + { + type: 'syncRole', + roleName: model.get('roleName'), + dataSourceKey: model.get('dataSourceKey'), + }, + { + transaction, + }, + ); }); this.app.on('acl:writeResources', async ({ roleName, transaction }) => { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx index 44d0d4d88b..f37d1cbbcf 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx @@ -46,36 +46,34 @@ const app = mockApp({ 'mobileRoutes:list': { data: [ { - "id": 10, - "createdAt": "2024-07-08T13:22:33.763Z", - "updatedAt": "2024-07-08T13:22:33.763Z", - "parentId": null, - "title": "Test1", - "icon": "AppstoreOutlined", - "schemaUid": "test", - "type": "page", - "options": null, - "sort": 1, - "createdById": 1, - "updatedById": 1, + id: 10, + createdAt: '2024-07-08T13:22:33.763Z', + updatedAt: '2024-07-08T13:22:33.763Z', + parentId: null, + title: 'Test1', + icon: 'AppstoreOutlined', + schemaUid: 'test', + type: 'page', + options: null, + sort: 1, + createdById: 1, + updatedById: 1, }, { - "id": 13, - "createdAt": "2024-07-08T13:23:01.929Z", - "updatedAt": "2024-07-08T13:23:12.433Z", - "parentId": null, - "title": "Test2", - "icon": "aliwangwangoutlined", - "schemaUid": null, - "type": "link", - "options": { - "schemaUid": null, - "url": "https://github.com", - "params": [ - {} - ] - } - } + id: 13, + createdAt: '2024-07-08T13:23:01.929Z', + updatedAt: '2024-07-08T13:23:12.433Z', + parentId: null, + title: 'Test2', + icon: 'aliwangwangoutlined', + schemaUid: null, + type: 'link', + options: { + schemaUid: null, + url: 'https://github.com', + params: [{}], + }, + }, ], }, }, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-schema.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-schema.tsx index e7d4ce01bf..b2012ac28a 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-schema.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-schema.tsx @@ -15,8 +15,8 @@ const Demo = () => { title: 'Link', icon: 'AppstoreOutlined', options: { - url: 'https://github.com' - } + url: 'https://github.com', + }, }), )} /> diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx index b3d74786c4..31a50f695f 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx @@ -16,8 +16,8 @@ const schema = getMobileTabBarItemSchema({ title: 'Link', icon: 'AppstoreOutlined', options: { - url: 'https://github.com' - } + url: 'https://github.com', + }, }); const Demo = () => { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-schema.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-schema.tsx index fad8c60625..febb8124bb 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-schema.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-schema.tsx @@ -14,13 +14,7 @@ const schema = getMobileTabBarItemSchema({ }); const Demo = () => { - return ( - - ); + return ; }; class MyPlugin extends Plugin { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.Item/schema.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.Item/schema.ts index 6a9691873e..22c9d4222e 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.Item/schema.ts +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.Item/schema.ts @@ -18,5 +18,5 @@ export function getMobileTabBarItemSchema(routeItem: MobileRouteItem) { schemaUid: routeItem.schemaUid, ...(routeItem.options || {}), }, - } + }; } diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts index 1252cf0109..a5fd542b2d 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts @@ -143,6 +143,34 @@ export class PluginMultiAppManagerServer extends Plugin { return lodash.cloneDeep(lodash.omit(oldConfig, ['migrator'])); } + async handleSyncMessage(message) { + const { type } = message; + + if (type === 'startApp') { + const { appName } = message; + const model = await this.app.db.getRepository('applications').findOne({ + filter: { + name: appName, + }, + }); + + if (!model) { + return; + } + + const subApp = model.registerToSupervisor(this.app, { + appOptionsFactory: this.appOptionsFactory, + }); + + subApp.runCommand('start', '--quickstart'); + } + + if (type === 'removeApp') { + const { appName } = message; + await AppSupervisor.getInstance().removeApp(appName); + } + } + setSubAppUpgradeHandler(handler: SubAppUpgradeHandler) { this.subAppUpgradeHandler = handler; } @@ -186,6 +214,16 @@ export class PluginMultiAppManagerServer extends Plugin { context: options.context, }); + this.sendSyncMessage( + { + type: 'startApp', + appName: name, + }, + { + transaction, + }, + ); + const startPromise = subApp.runCommand('start', '--quickstart'); if (options?.context?.waitSubAppInstall) { @@ -194,8 +232,18 @@ export class PluginMultiAppManagerServer extends Plugin { }, ); - this.db.on('applications.afterDestroy', async (model: ApplicationModel) => { + this.db.on('applications.afterDestroy', async (model: ApplicationModel, options) => { await AppSupervisor.getInstance().removeApp(model.get('name') as string); + + this.sendSyncMessage( + { + type: 'removeApp', + appName: model.get('name'), + }, + { + transaction: options.transaction, + }, + ); }); const self = this;