feat: uiSchema remove api (#196)

* feat: recursiveRemoveIfNoChildren

* feat: remove with removeParents

* refactor: removeSchema server hook

* fix: server hook test

* feat: insertInner with remove parent

* feat: onSelfMove server hook

* feat: removeParentsIfNoChildren server hook method

* refactor: ui_schema_tree_path to uiSchemaTreePath

* feat: insertAdjacent api params
This commit is contained in:
ChengLei Shao 2022-02-18 12:29:03 +08:00 committed by GitHub
parent 6c381313cb
commit b9bbbc8516
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 602 additions and 91 deletions

View File

@ -111,8 +111,8 @@ describe('server hooks', () => {
field: 'title', field: 'title',
method: 'removeSchema', method: 'removeSchema',
params: { params: {
breakComponent: 'Grid', breakRemoveOn: { 'x-component': 'Grid' },
removeEmptyParents: true, removeParentsIfNoChildren: true,
}, },
}, },
], ],
@ -137,8 +137,8 @@ describe('server hooks', () => {
field: 'intro', field: 'intro',
method: 'removeSchema', method: 'removeSchema',
params: { params: {
breakComponent: 'Grid', breakRemoveOn: { 'x-component': 'Grid' },
removeEmptyParents: true, removeParentsIfNoChildren: true,
}, },
}, },
], ],
@ -188,8 +188,8 @@ describe('server hooks', () => {
collection: 'posts', collection: 'posts',
method: 'removeSchema', method: 'removeSchema',
params: { params: {
breakComponent: 'row', breakRemoveOn: { 'x-component': 'row' },
removeEmptyParents: true, removeParentsIfNoChildren: true,
}, },
}, },
], ],
@ -304,4 +304,91 @@ describe('server hooks', () => {
const role2Menus = await db.getRepository<BelongsToManyRepository>('roles.menuUiSchemas', 'role2').find(); const role2Menus = await db.getRepository<BelongsToManyRepository>('roles.menuUiSchemas', 'role2').find();
expect(role2Menus.length).toEqual(0); 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,
});
});
}); });

View File

@ -274,4 +274,68 @@ describe('server hooks', () => {
}), }),
).toBeDefined(); ).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();
});
}); });

View File

@ -35,7 +35,7 @@ describe('ui_schema repository', () => {
}, },
}); });
repository = db.getCollection('uiSchemas').repository as UiSchemaRepository; repository = db.getCollection('uiSchemas').repository as UiSchemaRepository;
treePathCollection = db.getCollection('ui_schema_tree_path'); treePathCollection = db.getCollection('uiSchemaTreePath');
}); });
it('should be registered', async () => { it('should be registered', async () => {
@ -50,7 +50,7 @@ describe('ui_schema repository', () => {
}; };
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
await repository.insertSingleNode(singleNode, transaction); await repository.insertSingleNode(singleNode, { transaction });
await transaction.commit(); await transaction.commit();
// it should save in ui schema tables // it should save in ui schema tables
@ -76,7 +76,7 @@ describe('ui_schema repository', () => {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
await repository.insertSingleNode(singleNode, transaction); await repository.insertSingleNode(singleNode, { transaction });
const child1: SchemaNode = { const child1: SchemaNode = {
name: 'child1', name: 'child1',
@ -88,7 +88,7 @@ describe('ui_schema repository', () => {
}, },
}; };
await repository.insertSingleNode(child1, transaction); await repository.insertSingleNode(child1, { transaction });
const child11: SchemaNode = { const child11: SchemaNode = {
name: 'child11', name: 'child11',
@ -99,7 +99,7 @@ describe('ui_schema repository', () => {
type: 'test', type: 'test',
}, },
}; };
await repository.insertSingleNode(child11, transaction); await repository.insertSingleNode(child11, { transaction });
await transaction.commit(); await transaction.commit();
expect( expect(
@ -726,4 +726,215 @@ describe('ui_schema repository', () => {
expect(newTree.properties.a1.title).toEqual('new a1 title'); 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,
});
});
}); });

View File

@ -53,10 +53,13 @@ export const uiSchemaActions = {
}, },
async insertAdjacent(ctx: Context, next) { 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); const repository = getRepositoryFromCtx(ctx);
ctx.body = await repository.insertAdjacent(position, resourceIndex, values); ctx.body = await repository.insertAdjacent(position, resourceIndex, values, {
removeParentsIfNoChildren,
breakRemoveOn,
});
await next(); await next();
}, },
@ -68,9 +71,12 @@ export const uiSchemaActions = {
function insertPositionActionBuilder(position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd') { function insertPositionActionBuilder(position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd') {
return async function (ctx: Context, next) { return async function (ctx: Context, next) {
const { resourceIndex, values } = ctx.action.params; const { resourceIndex, values, removeParentsIfNoChildren, breakRemoveOn } = ctx.action.params;
const repository = getRepositoryFromCtx(ctx); const repository = getRepositoryFromCtx(ctx);
ctx.body = await repository.insertAdjacent(position, resourceIndex, values); ctx.body = await repository.insertAdjacent(position, resourceIndex, values, {
removeParentsIfNoChildren,
breakRemoveOn,
});
await next(); await next();
}; };
} }

View File

@ -1,5 +1,5 @@
export default { export default {
name: 'ui_schema_tree_path', name: 'uiSchemaTreePath',
autoGenId: false, autoGenId: false,
timestamps: false, timestamps: false,
fields: [ fields: [

View File

@ -1,5 +1,11 @@
import { MagicAttributeModel } from '@nocobase/database'; 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 }; export { UiSchemaModel };

View File

@ -9,10 +9,20 @@ interface GetJsonSchemaOptions {
transaction?: Transaction; 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']; const nodeKeys = ['properties', 'definitions', 'patternProperties', 'additionalProperties', 'items'];
export class UiSchemaRepository extends Repository { export class UiSchemaRepository extends Repository {
get uiSchemasTableName() { get uiSchemasTableName() {
if (this.database.sequelize.getDialect() === 'postgres') { if (this.database.sequelize.getDialect() === 'postgres') {
return `"${this.model.tableName}"`; return `"${this.model.tableName}"`;
@ -21,7 +31,7 @@ export class UiSchemaRepository extends Repository {
} }
get uiSchemaTreePathTableName() { 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') { if (this.database.sequelize.getDialect() === 'postgres') {
return `"${model.tableName}"`; 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 NodeInfo.type as type, NodeInfo.async as async, ParentPath.ancestor as parent, ParentPath.sort as sort
FROM ${this.uiSchemaTreePathTableName} as TreePath FROM ${this.uiSchemaTreePathTableName} as TreePath
LEFT JOIN ${this.uiSchemasTableName} as SchemaTable ON SchemaTable.uid = TreePath.descendant LEFT JOIN ${this.uiSchemasTableName} as SchemaTable ON SchemaTable.uid = TreePath.descendant
LEFT JOIN ${ LEFT JOIN ${this.uiSchemaTreePathTableName} as NodeInfo ON NodeInfo.descendant = SchemaTable.uid and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0
this.uiSchemaTreePathTableName LEFT JOIN ${this.uiSchemaTreePathTableName} as ParentPath ON (ParentPath.descendant = SchemaTable.uid AND ParentPath.depth = 1)
} 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)`; WHERE TreePath.ancestor = :ancestor AND (NodeInfo.async = false or TreePath.depth = 1)`;
const nodes = await db.sequelize.query(rawSql, { const nodes = await db.sequelize.query(rawSql, {
@ -194,7 +200,7 @@ export class UiSchemaRepository extends Repository {
} }
treeCollection() { treeCollection() {
return this.database.getCollection('ui_schema_tree_path'); return this.database.getCollection('uiSchemaTreePath');
} }
async patch(newSchema: any, options?) { 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 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: { filter: {
descendant: uid, descendant: uid,
depth: 1, depth: 1,
@ -267,29 +293,38 @@ export class UiSchemaRepository extends Repository {
transaction, 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) { if (!parent) {
return null; return null;
} }
const countResult = await db.sequelize.query( const parentChildrenCount = await this.childrenCount(parent, transaction);
`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'];
if (parentChildrenCount == 1) { if (parentChildrenCount == 1) {
const schema = await db.getRepository('uiSchemas').findOne({ const schema = await db.getRepository('uiSchemas').findOne({
filter: { filter: {
uid: parent.get('ancestor') as string, uid: parent,
}, },
transaction, transaction,
}); });
@ -300,15 +335,13 @@ export class UiSchemaRepository extends Repository {
return null; return null;
} }
async removeEmptyParents(options: TransactionAble & { uid: string; breakComponent?: string }) { async removeEmptyParents(options: TransactionAble & { uid: string; breakRemoveOn?: BreakRemoveOnType }) {
const { transaction, uid, breakComponent } = options; const { transaction, uid, breakRemoveOn } = options;
const removeParent = async (nodeUid: string) => { const removeParent = async (nodeUid: string) => {
const parent = await this.isSingleChild(nodeUid, transaction); const parent = await this.isSingleChild(nodeUid, transaction);
const nodeComponentType = parent ? parent.get('x-component') : null; if (parent && !this.breakOnMatched(parent, breakRemoveOn)) {
if ((parent && !breakComponent) || (parent && breakComponent != nodeComponentType)) {
await removeParent(parent.get('uid') as string); await removeParent(parent.get('uid') as string);
} else { } else {
await this.remove(nodeUid, { transaction }); await this.remove(nodeUid, { transaction });
@ -318,7 +351,49 @@ export class UiSchemaRepository extends Repository {
await removeParent(uid); 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 handleTransaction: boolean = true;
let transaction; let transaction;
@ -332,6 +407,14 @@ export class UiSchemaRepository extends Repository {
const treePathTable = this.uiSchemaTreePathTableName; const treePathTable = this.uiSchemaTreePathTableName;
try { try {
if (options?.removeParentsIfNoChildren) {
await this.removeEmptyParents({ transaction, uid, breakRemoveOn: options.breakRemoveOn });
if (handleTransaction) {
await transaction.commit();
}
return;
}
await this.database.sequelize.query( await this.database.sequelize.query(
`DELETE FROM ${this.uiSchemasTableName} WHERE uid IN ( `DELETE FROM ${this.uiSchemasTableName} WHERE uid IN (
SELECT descendant FROM ${treePathTable} WHERE ancestor = :uid 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') { async insertBeside(targetUid: string, schema: any, side: 'before' | 'after', options?: InsertAdjacentOptions) {
const targetParent = await this.treeCollection().repository.findOne({ const targetParent = await this.findParentUid(targetUid);
filter: {
descendant: targetUid,
depth: 1,
},
});
const db = this.database; const db = this.database;
const treeTable = this.uiSchemaTreePathTableName; const treeTable = this.uiSchemaTreePathTableName;
const typeQuery = await db.sequelize.query(`SELECT type from ${treeTable} WHERE ancestor = :uid AND depth = 0;`, { const typeQuery = await db.sequelize.query(`SELECT type from ${treeTable} WHERE ancestor = :uid AND depth = 0;`, {
type: 'SELECT', type: 'SELECT',
replacements: { replacements: {
@ -393,7 +473,7 @@ export class UiSchemaRepository extends Repository {
const rootNode = nodes[0]; const rootNode = nodes[0];
rootNode.childOptions = { rootNode.childOptions = {
parentUid: targetParent.get('ancestor') as string, parentUid: targetParent,
type: typeQuery[0]['type'], type: typeQuery[0]['type'],
position: { position: {
type: side, 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')); 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 nodes = UiSchemaRepository.schemaToSingleNodes(schema);
const rootNode = nodes[0]; const rootNode = nodes[0];
rootNode.childOptions = { rootNode.childOptions = {
parentUid: targetUid, parentUid: targetUid,
type: lodash.get(schema, 'x-node-type', 'properties'), type: lodash.get(schema, 'x-node-type', 'properties'),
position, position,
}; };
const insertedNodes = await this.insertNodes(nodes); const insertedNodes = await this.insertNodes(nodes, options);
return await this.getJsonSchema(insertedNodes[0].get('uid')); return await this.getJsonSchema(insertedNodes[0].get('uid'));
} }
async insertAdjacent(position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd', target: string, schema: any) { async insertAdjacent(
return await this[`insert${lodash.upperFirst(position)}`](target, schema); 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) { async insertAfterBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
return await this.insertInner(targetUid, schema, 'first'); return await this.insertInner(targetUid, schema, 'first', options);
} }
async insertBeforeEnd(targetUid: string, schema: any) { async insertBeforeEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
return await this.insertInner(targetUid, schema, 'last'); return await this.insertInner(targetUid, schema, 'last', options);
} }
async insertBeforeBegin(targetUid: string, schema: any) { async insertBeforeBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
return await this.insertBeside(targetUid, schema, 'before'); return await this.insertBeside(targetUid, schema, 'before', options);
} }
async insertAfterEnd(targetUid: string, schema: any) { async insertAfterEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
return await this.insertBeside(targetUid, schema, 'after'); return await this.insertBeside(targetUid, schema, 'after', options);
} }
async insertNodes(nodes: SchemaNode[], options?) { async insertNodes(nodes: SchemaNode[], options?) {
@ -453,7 +539,12 @@ export class UiSchemaRepository extends Repository {
try { try {
for (const node of nodes) { for (const node of nodes) {
insertedNodes.push(await this.insertSingleNode(node, transaction)); insertedNodes.push(
await this.insertSingleNode(node, {
...options,
transaction,
}),
);
} }
if (handleTransaction) { if (handleTransaction) {
@ -497,9 +588,11 @@ export class UiSchemaRepository extends Repository {
return node; return node;
} }
async insertSingleNode(schema: SchemaNode, transaction: Transaction) { async insertSingleNode(schema: SchemaNode, options: TransactionAble & removeParentOptions) {
const { transaction } = options;
const db = this.database; const db = this.database;
const treeCollection = db.getCollection('ui_schema_tree_path'); const treeCollection = db.getCollection('uiSchemaTreePath');
const uid = schema['x-uid']; const uid = schema['x-uid'];
const name = schema['name']; const name = schema['name'];
@ -530,6 +623,7 @@ export class UiSchemaRepository extends Repository {
} }
if (childOptions) { if (childOptions) {
const oldParentUid = await this.findParentUid(uid, transaction);
const parentUid = childOptions.parentUid; const parentUid = childOptions.parentUid;
const isTreeQuery = await db.sequelize.query( 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 node is a tree root move tree to new path
if (isTree) { if (isTree) {
// delete old tree path
await db.sequelize.query( await db.sequelize.query(
`DELETE FROM ${treeTable} `DELETE FROM ${treeTable}
WHERE descendant IN (SELECT descendant FROM (SELECT descendant FROM ${treeTable} WHERE ancestor = :uid) as descendantTable ) 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( await db.sequelize.query(
`INSERT INTO ${treeTable} (ancestor, descendant, depth) `INSERT INTO ${treeTable} (ancestor, descendant, depth)
SELECT supertree.ancestor, subtree.descendant, supertree.depth + subtree.depth + 1 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, 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 { } else {
// insert root node path // insert root node path
await db.sequelize.query( await db.sequelize.query(

View File

@ -1,11 +1,13 @@
import { hookFactory } from './factory'; import { hookFactory } from './factory';
import { removeSchema } from './remove-schema'; import { removeSchema } from './remove-schema';
import { bindMenuToRole } from './bind-menu-to-row'; import { bindMenuToRole } from './bind-menu-to-row';
import { removeParentsIfNoChildren } from './remove-parents-if-no-children';
const hooks = [ const hooks = [
hookFactory('onCollectionDestroy', 'removeSchema', removeSchema), hookFactory('onCollectionDestroy', 'removeSchema', removeSchema),
hookFactory('onCollectionFieldDestroy', 'removeSchema', removeSchema), hookFactory('onCollectionFieldDestroy', 'removeSchema', removeSchema),
hookFactory('onSelfCreate', 'bindMenuToRole', bindMenuToRole), hookFactory('onSelfCreate', 'bindMenuToRole', bindMenuToRole),
hookFactory('onSelfMove', 'removeParentsIfNoChildren', removeParentsIfNoChildren),
]; ];
export { hooks }; export { hooks };

View File

@ -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,
});
}

View File

@ -5,10 +5,10 @@ export async function removeSchema({ schemaInstance, options, db, params }) {
const uiSchemaRepository: UiSchemaRepository = db.getRepository('uiSchemas'); const uiSchemaRepository: UiSchemaRepository = db.getRepository('uiSchemas');
const uid = schemaInstance.get('uid') as string; const uid = schemaInstance.get('uid') as string;
if (params?.removeEmptyParents) { if (params?.removeParentsIfNoChildren) {
await uiSchemaRepository.removeEmptyParents({ await uiSchemaRepository.removeEmptyParents({
uid, uid,
breakComponent: params['breakComponent'], breakRemoveOn: params['breakRemoveOn'],
transaction, transaction,
}); });
} else { } else {

View File

@ -7,7 +7,8 @@ export type HookType =
| 'onCollectionDestroy' | 'onCollectionDestroy'
| 'onCollectionFieldDestroy' | 'onCollectionFieldDestroy'
| 'onAnyCollectionFieldDestroy' | 'onAnyCollectionFieldDestroy'
| 'onSelfCreate'; | 'onSelfCreate'
| 'onSelfMove';
export class ServerHooks { export class ServerHooks {
hooks = new Map<HookType, Map<string, any>>(); hooks = new Map<HookType, Map<string, any>>();
@ -34,6 +35,30 @@ export class ServerHooks {
this.db.on('uiSchemas.afterCreateWithAssociations', async (model, options) => { this.db.on('uiSchemas.afterCreateWithAssociations', async (model, options) => {
await this.onUiSchemaCreate(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) { protected async onCollectionDestroy(collectionModel, options) {
@ -89,21 +114,7 @@ export class ServerHooks {
} }
protected async onUiSchemaCreate(schemaInstance, options) { protected async onUiSchemaCreate(schemaInstance, options) {
const { transaction } = options; await this.callSchemaInstanceHooksByType(schemaInstance, options, 'onSelfCreate');
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'),
});
}
} }
protected async findHooksAndCall(hooksFilter, hooksArgs, transaction) { protected async findHooksAndCall(hooksFilter, hooksArgs, transaction) {