diff --git a/packages/plugin-ui-schema-storage/src/__tests__/server-hook-impl.test.ts b/packages/plugin-ui-schema-storage/src/__tests__/server-hook-impl.test.ts index 688f6e1e0b..0e719119a0 100644 --- a/packages/plugin-ui-schema-storage/src/__tests__/server-hook-impl.test.ts +++ b/packages/plugin-ui-schema-storage/src/__tests__/server-hook-impl.test.ts @@ -111,8 +111,8 @@ describe('server hooks', () => { field: 'title', method: 'removeSchema', params: { - breakComponent: 'Grid', - removeEmptyParents: true, + breakRemoveOn: { 'x-component': 'Grid' }, + removeParentsIfNoChildren: true, }, }, ], @@ -137,8 +137,8 @@ describe('server hooks', () => { field: 'intro', method: 'removeSchema', params: { - breakComponent: 'Grid', - removeEmptyParents: true, + breakRemoveOn: { 'x-component': 'Grid' }, + removeParentsIfNoChildren: true, }, }, ], @@ -188,8 +188,8 @@ describe('server hooks', () => { collection: 'posts', method: 'removeSchema', params: { - breakComponent: 'row', - removeEmptyParents: true, + breakRemoveOn: { 'x-component': 'row' }, + removeParentsIfNoChildren: true, }, }, ], @@ -304,4 +304,91 @@ describe('server hooks', () => { const role2Menus = await db.getRepository('roles.menuUiSchemas', 'role2').find(); expect(role2Menus.length).toEqual(0); }); + + it('should remove parents on self move', async () => { + const schema = { + 'x-uid': 'A', + name: 'A', + properties: { + B: { + 'x-uid': 'B', + properties: { + C: { + 'x-uid': 'C', + properties: { + D: { + 'x-uid': 'D', + 'x-server-hooks': [ + { + type: 'onSelfMove', + method: 'removeParentsIfNoChildren', + }, + ], + }, + }, + }, + }, + }, + E: { + 'x-uid': 'E', + }, + }, + }; + + await uiSchemaRepository.insert(schema); + + await uiSchemaRepository.insertAfterEnd('E', { + 'x-uid': 'F', + name: 'F', + properties: { + G: { + 'x-uid': 'G', + properties: { + D: { + 'x-uid': 'D', + }, + }, + }, + }, + }); + + const A = await uiSchemaRepository.getJsonSchema('A'); + expect(A).toEqual({ + properties: { + E: { + 'x-uid': 'E', + 'x-async': false, + 'x-index': 2, + }, + F: { + properties: { + G: { + properties: { + D: { + 'x-server-hooks': [ + { + type: 'onSelfMove', + method: 'removeParentsIfNoChildren', + }, + ], + 'x-uid': 'D', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'G', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'F', + 'x-async': false, + 'x-index': 3, + }, + }, + name: 'A', + 'x-uid': 'A', + 'x-async': false, + }); + }); }); diff --git a/packages/plugin-ui-schema-storage/src/__tests__/server-hook.test.ts b/packages/plugin-ui-schema-storage/src/__tests__/server-hook.test.ts index 6080345144..46f979ce84 100644 --- a/packages/plugin-ui-schema-storage/src/__tests__/server-hook.test.ts +++ b/packages/plugin-ui-schema-storage/src/__tests__/server-hook.test.ts @@ -274,4 +274,68 @@ describe('server hooks', () => { }), ).toBeDefined(); }); + + it('should call onSelfMove', async () => { + const schema = { + 'x-uid': 'A', + name: 'A', + properties: { + B: { + 'x-uid': 'B', + properties: { + C: { + 'x-uid': 'C', + properties: { + D: { + 'x-uid': 'D', + 'x-server-hooks': [ + { + type: 'onSelfMove', + method: 'testOnSelfMove', + }, + ], + }, + }, + }, + }, + }, + E: { + 'x-uid': 'E', + }, + }, + }; + + const serverHooks = uiSchemaPlugin.serverHooks; + + const jestFn = jest.fn(); + + serverHooks.register('onSelfMove', 'testOnSelfMove', async ({ options }) => { + jestFn(); + }); + + await uiSchemaRepository.insert(schema); + + await uiSchemaRepository.insertAfterEnd( + 'E', + { + 'x-uid': 'F', + name: 'F', + properties: { + G: { + 'x-uid': 'G', + properties: { + D: { + 'x-uid': 'D', + }, + }, + }, + }, + }, + { + removeParentsIfNoChildren: true, + }, + ); + + expect(jestFn).toHaveBeenCalled(); + }); }); diff --git a/packages/plugin-ui-schema-storage/src/__tests__/ui-schema-repository.test.ts b/packages/plugin-ui-schema-storage/src/__tests__/ui-schema-repository.test.ts index 06849c24ee..d82b6e5fc9 100644 --- a/packages/plugin-ui-schema-storage/src/__tests__/ui-schema-repository.test.ts +++ b/packages/plugin-ui-schema-storage/src/__tests__/ui-schema-repository.test.ts @@ -35,7 +35,7 @@ describe('ui_schema repository', () => { }, }); repository = db.getCollection('uiSchemas').repository as UiSchemaRepository; - treePathCollection = db.getCollection('ui_schema_tree_path'); + treePathCollection = db.getCollection('uiSchemaTreePath'); }); it('should be registered', async () => { @@ -50,7 +50,7 @@ describe('ui_schema repository', () => { }; const transaction = await db.sequelize.transaction(); - await repository.insertSingleNode(singleNode, transaction); + await repository.insertSingleNode(singleNode, { transaction }); await transaction.commit(); // it should save in ui schema tables @@ -76,7 +76,7 @@ describe('ui_schema repository', () => { const transaction = await db.sequelize.transaction(); - await repository.insertSingleNode(singleNode, transaction); + await repository.insertSingleNode(singleNode, { transaction }); const child1: SchemaNode = { name: 'child1', @@ -88,7 +88,7 @@ describe('ui_schema repository', () => { }, }; - await repository.insertSingleNode(child1, transaction); + await repository.insertSingleNode(child1, { transaction }); const child11: SchemaNode = { name: 'child11', @@ -99,7 +99,7 @@ describe('ui_schema repository', () => { type: 'test', }, }; - await repository.insertSingleNode(child11, transaction); + await repository.insertSingleNode(child11, { transaction }); await transaction.commit(); expect( @@ -726,4 +726,215 @@ describe('ui_schema repository', () => { expect(newTree.properties.a1.title).toEqual('new a1 title'); }); }); + + it('should insertInner with removeParent', async () => { + const schema = { + 'x-uid': 'A', + name: 'A', + properties: { + B: { + 'x-uid': 'B', + properties: { + C: { + 'x-uid': 'C', + properties: { + D: { + 'x-uid': 'D', + }, + }, + }, + }, + }, + E: { + 'x-uid': 'E', + }, + }, + }; + + await repository.insert(schema); + + await repository.insertAfterBegin( + 'E', + { + 'x-uid': 'F', + name: 'F', + properties: { + G: { + 'x-uid': 'G', + properties: { + D: { + 'x-uid': 'D', + }, + }, + }, + }, + }, + { + removeParentsIfNoChildren: true, + }, + ); + + const A = await repository.getJsonSchema('A'); + + expect(A).toEqual({ + properties: { + E: { + properties: { + F: { + properties: { + G: { + properties: { + D: { + 'x-uid': 'D', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'G', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'F', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'E', + 'x-async': false, + 'x-index': 2, + }, + }, + name: 'A', + 'x-uid': 'A', + 'x-async': false, + }); + }); + + it('should insertBeside with removeParent', async () => { + const schema = { + 'x-uid': 'A', + name: 'A', + properties: { + B: { + 'x-uid': 'B', + properties: { + C: { + 'x-uid': 'C', + properties: { + D: { + 'x-uid': 'D', + }, + }, + }, + }, + }, + E: { + 'x-uid': 'E', + }, + }, + }; + + await repository.insert(schema); + + await repository.insertAfterEnd( + 'E', + { + 'x-uid': 'F', + name: 'F', + properties: { + G: { + 'x-uid': 'G', + properties: { + D: { + 'x-uid': 'D', + }, + }, + }, + }, + }, + { + removeParentsIfNoChildren: true, + }, + ); + + const A = await repository.getJsonSchema('A'); + + expect(A).toEqual({ + properties: { + E: { + 'x-uid': 'E', + 'x-async': false, + 'x-index': 2, + }, + F: { + properties: { + G: { + 'x-uid': 'G', + 'x-async': false, + 'x-index': 1, + properties: { + D: { + 'x-uid': 'D', + 'x-async': false, + 'x-index': 1, + }, + }, + }, + }, + 'x-uid': 'F', + 'x-async': false, + 'x-index': 3, + }, + }, + name: 'A', + 'x-uid': 'A', + 'x-async': false, + }); + }); + + it('should remove with breakOn', async () => { + const schema = { + 'x-uid': 'A', + name: 'A', + properties: { + B: { + 'x-uid': 'B', + properties: { + C: { + 'x-uid': 'C', + properties: { + D: { + 'x-uid': 'D', + }, + }, + }, + }, + }, + E: { + 'x-uid': 'E', + }, + }, + }; + + await repository.insert(schema); + + await repository.remove('D', { + removeParentsIfNoChildren: true, + }); + + const A = await repository.getJsonSchema('A'); + expect(A).toEqual({ + properties: { + E: { + 'x-uid': 'E', + 'x-async': false, + 'x-index': 2, + }, + }, + name: 'A', + 'x-uid': 'A', + 'x-async': false, + }); + }); }); diff --git a/packages/plugin-ui-schema-storage/src/actions/ui-schema-action.ts b/packages/plugin-ui-schema-storage/src/actions/ui-schema-action.ts index 872565c931..2c5f64a6b9 100644 --- a/packages/plugin-ui-schema-storage/src/actions/ui-schema-action.ts +++ b/packages/plugin-ui-schema-storage/src/actions/ui-schema-action.ts @@ -53,10 +53,13 @@ export const uiSchemaActions = { }, async insertAdjacent(ctx: Context, next) { - const { resourceIndex, position, values } = ctx.action.params; + const { resourceIndex, position, values, removeParentsIfNoChildren, breakRemoveOn } = ctx.action.params; const repository = getRepositoryFromCtx(ctx); - ctx.body = await repository.insertAdjacent(position, resourceIndex, values); + ctx.body = await repository.insertAdjacent(position, resourceIndex, values, { + removeParentsIfNoChildren, + breakRemoveOn, + }); await next(); }, @@ -68,9 +71,12 @@ export const uiSchemaActions = { function insertPositionActionBuilder(position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd') { return async function (ctx: Context, next) { - const { resourceIndex, values } = ctx.action.params; + const { resourceIndex, values, removeParentsIfNoChildren, breakRemoveOn } = ctx.action.params; const repository = getRepositoryFromCtx(ctx); - ctx.body = await repository.insertAdjacent(position, resourceIndex, values); + ctx.body = await repository.insertAdjacent(position, resourceIndex, values, { + removeParentsIfNoChildren, + breakRemoveOn, + }); await next(); }; } diff --git a/packages/plugin-ui-schema-storage/src/collections/ui_schema_tree_path.ts b/packages/plugin-ui-schema-storage/src/collections/ui_schema_tree_path.ts index a5f6c4d75d..981a17e48c 100644 --- a/packages/plugin-ui-schema-storage/src/collections/ui_schema_tree_path.ts +++ b/packages/plugin-ui-schema-storage/src/collections/ui_schema_tree_path.ts @@ -1,5 +1,5 @@ export default { - name: 'ui_schema_tree_path', + name: 'uiSchemaTreePath', autoGenId: false, timestamps: false, fields: [ diff --git a/packages/plugin-ui-schema-storage/src/model.ts b/packages/plugin-ui-schema-storage/src/model.ts index 8895026843..63f9697307 100644 --- a/packages/plugin-ui-schema-storage/src/model.ts +++ b/packages/plugin-ui-schema-storage/src/model.ts @@ -1,5 +1,11 @@ import { MagicAttributeModel } from '@nocobase/database'; +import { HookType } from './server-hooks'; -class UiSchemaModel extends MagicAttributeModel {} +class UiSchemaModel extends MagicAttributeModel { + getServerHooksByType(type: HookType) { + const hooks = this.get('x-server-hooks') || []; + return hooks.filter((hook) => hook.type === type); + } +} export { UiSchemaModel }; diff --git a/packages/plugin-ui-schema-storage/src/repository.ts b/packages/plugin-ui-schema-storage/src/repository.ts index 03692f911a..529b23c6d1 100644 --- a/packages/plugin-ui-schema-storage/src/repository.ts +++ b/packages/plugin-ui-schema-storage/src/repository.ts @@ -9,10 +9,20 @@ interface GetJsonSchemaOptions { transaction?: Transaction; } +type BreakRemoveOnType = { + [key: string]: any; +}; + +export interface removeParentOptions { + removeParentsIfNoChildren?: boolean; + breakRemoveOn?: BreakRemoveOnType; +} + +interface InsertAdjacentOptions extends removeParentOptions {} + const nodeKeys = ['properties', 'definitions', 'patternProperties', 'additionalProperties', 'items']; export class UiSchemaRepository extends Repository { - get uiSchemasTableName() { if (this.database.sequelize.getDialect() === 'postgres') { return `"${this.model.tableName}"`; @@ -21,7 +31,7 @@ export class UiSchemaRepository extends Repository { } get uiSchemaTreePathTableName() { - const model = this.database.getCollection('ui_schema_tree_path').model; + const model = this.database.getCollection('uiSchemaTreePath').model; if (this.database.sequelize.getDialect() === 'postgres') { return `"${model.tableName}"`; } @@ -94,12 +104,8 @@ export class UiSchemaRepository extends Repository { NodeInfo.type as type, NodeInfo.async as async, ParentPath.ancestor as parent, ParentPath.sort as sort FROM ${this.uiSchemaTreePathTableName} as TreePath LEFT JOIN ${this.uiSchemasTableName} as SchemaTable ON SchemaTable.uid = TreePath.descendant - LEFT JOIN ${ - this.uiSchemaTreePathTableName - } as NodeInfo ON NodeInfo.descendant = SchemaTable.uid and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0 - LEFT JOIN ${ - this.uiSchemaTreePathTableName - } as ParentPath ON (ParentPath.descendant = SchemaTable.uid AND ParentPath.depth = 1) + LEFT JOIN ${this.uiSchemaTreePathTableName} as NodeInfo ON NodeInfo.descendant = SchemaTable.uid and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0 + LEFT JOIN ${this.uiSchemaTreePathTableName} as ParentPath ON (ParentPath.descendant = SchemaTable.uid AND ParentPath.depth = 1) WHERE TreePath.ancestor = :ancestor AND (NodeInfo.async = false or TreePath.depth = 1)`; const nodes = await db.sequelize.query(rawSql, { @@ -194,7 +200,7 @@ export class UiSchemaRepository extends Repository { } treeCollection() { - return this.database.getCollection('ui_schema_tree_path'); + return this.database.getCollection('uiSchemaTreePath'); } async patch(newSchema: any, options?) { @@ -256,10 +262,30 @@ export class UiSchemaRepository extends Repository { ); } - protected async isSingleChild(uid, transaction) { + protected async childrenCount(uid, transaction) { const db = this.database; - const parent = await db.getRepository('ui_schema_tree_path').findOne({ + const countResult = await db.sequelize.query( + `SELECT COUNT(*) as count FROM ${this.uiSchemaTreePathTableName} where ancestor = :ancestor and depth = 1`, + { + replacements: { + ancestor: uid, + }, + type: 'SELECT', + transaction, + }, + ); + + return parseInt(countResult[0]['count']); + } + + protected async isLeafNode(uid, transaction) { + const childrenCount = await this.childrenCount(uid, transaction); + return childrenCount === 0; + } + + async findParentUid(uid, transaction?) { + const parent = await this.database.getRepository('uiSchemaTreePath').findOne({ filter: { descendant: uid, depth: 1, @@ -267,29 +293,38 @@ export class UiSchemaRepository extends Repository { transaction, }); + return parent ? (parent.get('ancestor') as string) : null; + } + + protected async findNodeSchemaWithParent(uid, transaction) { + const schema = await this.database.getRepository('uiSchemas').findOne({ + filter: { + uid, + }, + transaction, + }); + + return { + parentUid: await this.findParentUid(uid, transaction), + schema, + }; + } + + protected async isSingleChild(uid, transaction) { + const db = this.database; + + const parent = await this.findParentUid(uid, transaction); + if (!parent) { return null; } - const countResult = await db.sequelize.query( - `SELECT COUNT(*) as count FROM ${ - db.getCollection('ui_schema_tree_path').model.tableName - } where ancestor = :ancestor and depth = 1`, - { - replacements: { - ancestor: parent.get('ancestor'), - }, - type: 'SELECT', - transaction, - }, - ); - - const parentChildrenCount = countResult[0]['count']; + const parentChildrenCount = await this.childrenCount(parent, transaction); if (parentChildrenCount == 1) { const schema = await db.getRepository('uiSchemas').findOne({ filter: { - uid: parent.get('ancestor') as string, + uid: parent, }, transaction, }); @@ -300,15 +335,13 @@ export class UiSchemaRepository extends Repository { return null; } - async removeEmptyParents(options: TransactionAble & { uid: string; breakComponent?: string }) { - const { transaction, uid, breakComponent } = options; + async removeEmptyParents(options: TransactionAble & { uid: string; breakRemoveOn?: BreakRemoveOnType }) { + const { transaction, uid, breakRemoveOn } = options; const removeParent = async (nodeUid: string) => { const parent = await this.isSingleChild(nodeUid, transaction); - const nodeComponentType = parent ? parent.get('x-component') : null; - - if ((parent && !breakComponent) || (parent && breakComponent != nodeComponentType)) { + if (parent && !this.breakOnMatched(parent, breakRemoveOn)) { await removeParent(parent.get('uid') as string); } else { await this.remove(nodeUid, { transaction }); @@ -318,7 +351,49 @@ export class UiSchemaRepository extends Repository { await removeParent(uid); } - async remove(uid: string, options?: TransactionAble) { + private breakOnMatched(schemaInstance, breakRemoveOn: BreakRemoveOnType): boolean { + if (!breakRemoveOn) { + return false; + } + + for (const key of Object.keys(breakRemoveOn)) { + const instanceValue = schemaInstance.get(key); + const breakRemoveOnValue = breakRemoveOn[key]; + if (instanceValue !== breakRemoveOnValue) { + return false; + } + } + + return true; + } + + async recursivelyRemoveIfNoChildren(options: TransactionAble & { uid: string; breakRemoveOn?: BreakRemoveOnType }) { + const { uid, transaction, breakRemoveOn } = options; + + const removeLeafNode = async (nodeUid: string) => { + const isLeafNode = await this.isLeafNode(nodeUid, transaction); + + if (isLeafNode) { + const { parentUid, schema } = await this.findNodeSchemaWithParent(nodeUid, transaction); + + if (this.breakOnMatched(schema, breakRemoveOn)) { + // break at here + return; + } else { + // remove current node + await this.remove(nodeUid, { + transaction, + }); + // continue remove + await removeLeafNode(parentUid); + } + } + }; + + await removeLeafNode(uid); + } + + async remove(uid: string, options?: TransactionAble & removeParentOptions) { let handleTransaction: boolean = true; let transaction; @@ -332,6 +407,14 @@ export class UiSchemaRepository extends Repository { const treePathTable = this.uiSchemaTreePathTableName; try { + if (options?.removeParentsIfNoChildren) { + await this.removeEmptyParents({ transaction, uid, breakRemoveOn: options.breakRemoveOn }); + if (handleTransaction) { + await transaction.commit(); + } + return; + } + await this.database.sequelize.query( `DELETE FROM ${this.uiSchemasTableName} WHERE uid IN ( SELECT descendant FROM ${treePathTable} WHERE ancestor = :uid @@ -371,16 +454,13 @@ export class UiSchemaRepository extends Repository { } } - async insertBeside(targetUid: string, schema: any, side: 'before' | 'after') { - const targetParent = await this.treeCollection().repository.findOne({ - filter: { - descendant: targetUid, - depth: 1, - }, - }); + async insertBeside(targetUid: string, schema: any, side: 'before' | 'after', options?: InsertAdjacentOptions) { + const targetParent = await this.findParentUid(targetUid); const db = this.database; + const treeTable = this.uiSchemaTreePathTableName; + const typeQuery = await db.sequelize.query(`SELECT type from ${treeTable} WHERE ancestor = :uid AND depth = 0;`, { type: 'SELECT', replacements: { @@ -393,7 +473,7 @@ export class UiSchemaRepository extends Repository { const rootNode = nodes[0]; rootNode.childOptions = { - parentUid: targetParent.get('ancestor') as string, + parentUid: targetParent, type: typeQuery[0]['type'], position: { type: side, @@ -401,41 +481,47 @@ export class UiSchemaRepository extends Repository { }, }; - const insertedNodes = await this.insertNodes(nodes); + const insertedNodes = await this.insertNodes(nodes, options); return await this.getJsonSchema(insertedNodes[0].get('uid')); } - async insertInner(targetUid: string, schema: any, position: 'first' | 'last') { + async insertInner(targetUid: string, schema: any, position: 'first' | 'last', options?: InsertAdjacentOptions) { const nodes = UiSchemaRepository.schemaToSingleNodes(schema); const rootNode = nodes[0]; + rootNode.childOptions = { parentUid: targetUid, type: lodash.get(schema, 'x-node-type', 'properties'), position, }; - const insertedNodes = await this.insertNodes(nodes); + const insertedNodes = await this.insertNodes(nodes, options); return await this.getJsonSchema(insertedNodes[0].get('uid')); } - async insertAdjacent(position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd', target: string, schema: any) { - return await this[`insert${lodash.upperFirst(position)}`](target, schema); + async insertAdjacent( + position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd', + target: string, + schema: any, + options?: InsertAdjacentOptions, + ) { + return await this[`insert${lodash.upperFirst(position)}`](target, schema, options); } - async insertAfterBegin(targetUid: string, schema: any) { - return await this.insertInner(targetUid, schema, 'first'); + async insertAfterBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) { + return await this.insertInner(targetUid, schema, 'first', options); } - async insertBeforeEnd(targetUid: string, schema: any) { - return await this.insertInner(targetUid, schema, 'last'); + async insertBeforeEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) { + return await this.insertInner(targetUid, schema, 'last', options); } - async insertBeforeBegin(targetUid: string, schema: any) { - return await this.insertBeside(targetUid, schema, 'before'); + async insertBeforeBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) { + return await this.insertBeside(targetUid, schema, 'before', options); } - async insertAfterEnd(targetUid: string, schema: any) { - return await this.insertBeside(targetUid, schema, 'after'); + async insertAfterEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) { + return await this.insertBeside(targetUid, schema, 'after', options); } async insertNodes(nodes: SchemaNode[], options?) { @@ -453,7 +539,12 @@ export class UiSchemaRepository extends Repository { try { for (const node of nodes) { - insertedNodes.push(await this.insertSingleNode(node, transaction)); + insertedNodes.push( + await this.insertSingleNode(node, { + ...options, + transaction, + }), + ); } if (handleTransaction) { @@ -497,9 +588,11 @@ export class UiSchemaRepository extends Repository { return node; } - async insertSingleNode(schema: SchemaNode, transaction: Transaction) { + async insertSingleNode(schema: SchemaNode, options: TransactionAble & removeParentOptions) { + const { transaction } = options; + const db = this.database; - const treeCollection = db.getCollection('ui_schema_tree_path'); + const treeCollection = db.getCollection('uiSchemaTreePath'); const uid = schema['x-uid']; const name = schema['name']; @@ -530,6 +623,7 @@ export class UiSchemaRepository extends Repository { } if (childOptions) { + const oldParentUid = await this.findParentUid(uid, transaction); const parentUid = childOptions.parentUid; const isTreeQuery = await db.sequelize.query( @@ -547,6 +641,7 @@ export class UiSchemaRepository extends Repository { // if node is a tree root move tree to new path if (isTree) { + // delete old tree path await db.sequelize.query( `DELETE FROM ${treeTable} WHERE descendant IN (SELECT descendant FROM (SELECT descendant FROM ${treeTable} WHERE ancestor = :uid) as descendantTable ) @@ -561,6 +656,7 @@ export class UiSchemaRepository extends Repository { }, ); + // insert new tree path await db.sequelize.query( `INSERT INTO ${treeTable} (ancestor, descendant, depth) SELECT supertree.ancestor, subtree.descendant, supertree.depth + subtree.depth + 1 @@ -738,6 +834,23 @@ WHERE TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and TreeTable.sort }, transaction, }); + + // move node to new parent + if (oldParentUid !== null && oldParentUid !== parentUid) { + await this.database.emitAsync('uiSchemaMove', savedNode, { + transaction, + oldParentUid, + parentUid, + }); + + if (options.removeParentsIfNoChildren) { + await this.recursivelyRemoveIfNoChildren({ + transaction, + uid: oldParentUid, + breakRemoveOn: options.breakRemoveOn, + }); + } + } } else { // insert root node path await db.sequelize.query( diff --git a/packages/plugin-ui-schema-storage/src/server-hooks/hooks/index.ts b/packages/plugin-ui-schema-storage/src/server-hooks/hooks/index.ts index 5af51a46ee..6f359c5900 100644 --- a/packages/plugin-ui-schema-storage/src/server-hooks/hooks/index.ts +++ b/packages/plugin-ui-schema-storage/src/server-hooks/hooks/index.ts @@ -1,11 +1,13 @@ import { hookFactory } from './factory'; import { removeSchema } from './remove-schema'; import { bindMenuToRole } from './bind-menu-to-row'; +import { removeParentsIfNoChildren } from './remove-parents-if-no-children'; const hooks = [ hookFactory('onCollectionDestroy', 'removeSchema', removeSchema), hookFactory('onCollectionFieldDestroy', 'removeSchema', removeSchema), hookFactory('onSelfCreate', 'bindMenuToRole', bindMenuToRole), + hookFactory('onSelfMove', 'removeParentsIfNoChildren', removeParentsIfNoChildren), ]; export { hooks }; diff --git a/packages/plugin-ui-schema-storage/src/server-hooks/hooks/remove-parents-if-no-children.ts b/packages/plugin-ui-schema-storage/src/server-hooks/hooks/remove-parents-if-no-children.ts new file mode 100644 index 0000000000..ed1be93d9b --- /dev/null +++ b/packages/plugin-ui-schema-storage/src/server-hooks/hooks/remove-parents-if-no-children.ts @@ -0,0 +1,11 @@ +import { UiSchemaRepository } from '../../repository'; + +export async function removeParentsIfNoChildren({ schemaInstance, db, options, params }) { + const { transaction, oldParentUid } = options; + const uiSchemaRepository: UiSchemaRepository = db.getRepository('uiSchemas'); + await uiSchemaRepository.recursivelyRemoveIfNoChildren({ + transaction, + uid: oldParentUid, + breakRemoveOn: params?.breakRemoveOn, + }); +} diff --git a/packages/plugin-ui-schema-storage/src/server-hooks/hooks/remove-schema.ts b/packages/plugin-ui-schema-storage/src/server-hooks/hooks/remove-schema.ts index 5fcd6e9e39..f3b7f29840 100644 --- a/packages/plugin-ui-schema-storage/src/server-hooks/hooks/remove-schema.ts +++ b/packages/plugin-ui-schema-storage/src/server-hooks/hooks/remove-schema.ts @@ -5,10 +5,10 @@ export async function removeSchema({ schemaInstance, options, db, params }) { const uiSchemaRepository: UiSchemaRepository = db.getRepository('uiSchemas'); const uid = schemaInstance.get('uid') as string; - if (params?.removeEmptyParents) { + if (params?.removeParentsIfNoChildren) { await uiSchemaRepository.removeEmptyParents({ uid, - breakComponent: params['breakComponent'], + breakRemoveOn: params['breakRemoveOn'], transaction, }); } else { diff --git a/packages/plugin-ui-schema-storage/src/server-hooks/index.ts b/packages/plugin-ui-schema-storage/src/server-hooks/index.ts index f7def2344b..5e22f12f53 100644 --- a/packages/plugin-ui-schema-storage/src/server-hooks/index.ts +++ b/packages/plugin-ui-schema-storage/src/server-hooks/index.ts @@ -7,7 +7,8 @@ export type HookType = | 'onCollectionDestroy' | 'onCollectionFieldDestroy' | 'onAnyCollectionFieldDestroy' - | 'onSelfCreate'; + | 'onSelfCreate' + | 'onSelfMove'; export class ServerHooks { hooks = new Map>(); @@ -34,6 +35,30 @@ export class ServerHooks { this.db.on('uiSchemas.afterCreateWithAssociations', async (model, options) => { await this.onUiSchemaCreate(model, options); }); + + this.db.on('uiSchemaMove', async (model, options) => { + await this.onUiSchemaMove(model, options); + }); + } + + protected async callSchemaInstanceHooksByType(schemaInstance, options, type: HookType) { + const { transaction } = options; + + const hooks = schemaInstance.getServerHooksByType(type); + + for (const hook of hooks) { + const hookFunc = this.hooks.get(type)?.get(hook['method']); + await hookFunc({ + schemaInstance, + options, + db: this.db, + params: hook['params'], + }); + } + } + + protected async onUiSchemaMove(schemaInstance, options) { + await this.callSchemaInstanceHooksByType(schemaInstance, options, 'onSelfMove'); } protected async onCollectionDestroy(collectionModel, options) { @@ -89,21 +114,7 @@ export class ServerHooks { } protected async onUiSchemaCreate(schemaInstance, options) { - const { transaction } = options; - - const serverHooks = schemaInstance.get('serverHooks') || []; - - const onSelfCreateHooks = serverHooks.filter((serverHook) => serverHook.get('type') === 'onSelfCreate'); - - for (const serverHook of onSelfCreateHooks) { - const hookFunc = this.hooks.get('onSelfCreate')?.get(serverHook.get('method')); - await hookFunc({ - schemaInstance, - options, - db: this.db, - params: serverHook.get('params'), - }); - } + await this.callSchemaInstanceHooksByType(schemaInstance, options, 'onSelfCreate'); } protected async findHooksAndCall(hooksFilter, hooksArgs, transaction) {