mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-09 15:39:24 +08:00
feat: adjacency list
This commit is contained in:
parent
4fbad75ea9
commit
30a8678036
@ -23,8 +23,8 @@ function totalPage(total, pageSize): number {
|
||||
}
|
||||
|
||||
function findArgs(params: ActionParams) {
|
||||
const { fields, filter, appends, except, sort } = params;
|
||||
return { filter, fields, appends, except, sort };
|
||||
const { tree, fields, filter, appends, except, sort } = params;
|
||||
return { tree, filter, fields, appends, except, sort };
|
||||
}
|
||||
|
||||
async function listWithPagination(ctx: Context) {
|
||||
|
@ -84,7 +84,7 @@ const getSchema = (schema, category, compile): ISchema => {
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: () => useCreateCollection(),
|
||||
useAction: () => useCreateCollection(schema),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -188,16 +188,19 @@ const useDefaultCollectionFields = (values) => {
|
||||
return defaults;
|
||||
};
|
||||
|
||||
const useCreateCollection = () => {
|
||||
const useCreateCollection = (schema?: any) => {
|
||||
const form = useForm();
|
||||
const { refreshCM } = useCollectionManager();
|
||||
const ctx = useActionContext();
|
||||
const { refresh } = useResourceActionContext();
|
||||
const { resource } = useResourceContext();
|
||||
const { resource, collection } = useResourceContext();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
const values = cloneDeep(form.values);
|
||||
if (schema?.events?.beforeSubmit) {
|
||||
schema.events.beforeSubmit(values);
|
||||
}
|
||||
const fields = useDefaultCollectionFields(values);
|
||||
if (values.autoCreateReverseField) {
|
||||
} else {
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
recordPickerSelector,
|
||||
recordPickerViewer,
|
||||
relationshipType,
|
||||
reverseFieldProperties,
|
||||
reverseFieldProperties
|
||||
} from './properties';
|
||||
import { IField } from './types';
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './calendar';
|
||||
export * from './general';
|
||||
export * from './tree';
|
||||
|
||||
|
@ -0,0 +1,72 @@
|
||||
import { getConfigurableProperties } from './properties';
|
||||
import { ICollectionTemplate } from './types';
|
||||
|
||||
export const tree: ICollectionTemplate = {
|
||||
name: 'tree',
|
||||
title: '{{t("Tree collection")}}',
|
||||
order: 3,
|
||||
color: 'blue',
|
||||
default: {
|
||||
tree: 'adjacencyList',
|
||||
fields: [
|
||||
{
|
||||
interface: 'integer',
|
||||
name: 'parentId',
|
||||
type: 'bigInt',
|
||||
isForeignKey: true,
|
||||
uiSchema: {
|
||||
type: 'number',
|
||||
title: '{{t("Parent ID")}}',
|
||||
'x-component': 'InputNumber',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'm2o',
|
||||
type: 'belongsTo',
|
||||
name: 'parent',
|
||||
foreignKey: 'parentId',
|
||||
uiSchema: {
|
||||
title: '{{t("Parent")}}',
|
||||
'x-component': 'RecordPicker',
|
||||
'x-component-props': {
|
||||
// mode: 'tags',
|
||||
multiple: false,
|
||||
fieldNames: {
|
||||
label: 'id',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'o2m',
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
foreignKey: 'parentId',
|
||||
uiSchema: {
|
||||
title: '{{t("Children")}}',
|
||||
'x-component': 'RecordPicker',
|
||||
'x-component-props': {
|
||||
// mode: 'tags',
|
||||
multiple: true,
|
||||
fieldNames: {
|
||||
label: 'id',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
events: {
|
||||
beforeSubmit(values) {
|
||||
if (Array.isArray(values?.fields)) {
|
||||
values?.fields.map((f) => {
|
||||
f.target = values.name;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
configurableProperties: getConfigurableProperties('title', 'name', 'inherits', 'category', 'moreOptions'),
|
||||
};
|
@ -9,6 +9,7 @@ export interface ICollectionTemplate {
|
||||
order?: number;
|
||||
/** 默认配置 */
|
||||
default?: CollectionOptions;
|
||||
events?: any;
|
||||
/** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */
|
||||
configurableProperties?: Record<string, ISchema>;
|
||||
/** 当前模板可用的字段类型 */
|
||||
|
123
packages/core/database/src/__tests__/tree.test.ts
Normal file
123
packages/core/database/src/__tests__/tree.test.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { Database } from '../database';
|
||||
import { mockDatabase } from './';
|
||||
|
||||
describe('sort', function () {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should be auto completed', () => {
|
||||
const collection = db.collection({
|
||||
name: 'categories',
|
||||
tree: 'adjacency-list',
|
||||
fields: [
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'parent',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(collection.getField('parent').options.target).toBe('categories');
|
||||
expect(collection.getField('parent').options.foreignKey).toBe('parentId');
|
||||
expect(collection.getField('children').options.target).toBe('categories');
|
||||
expect(collection.getField('children').options.foreignKey).toBe('parentId');
|
||||
});
|
||||
|
||||
it('should be tree', async () => {
|
||||
db.collection({
|
||||
name: 'categories',
|
||||
tree: 'adjacency-list',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'description',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'parent',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
},
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
const values = [
|
||||
{
|
||||
name: '1',
|
||||
children: [
|
||||
{
|
||||
name: '1-1',
|
||||
children: [
|
||||
{
|
||||
name: '1-1-1',
|
||||
children: [
|
||||
{
|
||||
name: '1-1-1-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '2',
|
||||
children: [
|
||||
{
|
||||
name: '2-1',
|
||||
children: [
|
||||
{
|
||||
name: '2-1-1',
|
||||
children: [
|
||||
{
|
||||
name: '2-1-1-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await db.getRepository('categories').create({
|
||||
values,
|
||||
});
|
||||
|
||||
const instances = await db.getRepository('categories').find({
|
||||
filter: {
|
||||
parentId: null,
|
||||
},
|
||||
tree: true,
|
||||
fields: ['id', 'name'],
|
||||
appends: ['parent', 'children'],
|
||||
sort: 'id',
|
||||
});
|
||||
|
||||
expect(instances.map((i) => i.toJSON())).toMatchObject(values);
|
||||
|
||||
const instance = await db.getRepository('categories').findOne({
|
||||
filterByTk: 1,
|
||||
tree: true,
|
||||
fields: ['id', 'name'],
|
||||
});
|
||||
|
||||
expect(instance.toJSON()).toMatchObject(values[0]);
|
||||
});
|
||||
});
|
@ -7,7 +7,7 @@ import {
|
||||
QueryInterfaceDropTableOptions,
|
||||
SyncOptions,
|
||||
Transactionable,
|
||||
Utils,
|
||||
Utils
|
||||
} from 'sequelize';
|
||||
import { Database } from './database';
|
||||
import { Field, FieldOptions } from './fields';
|
||||
@ -37,6 +37,8 @@ export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'>
|
||||
*/
|
||||
magicAttribute?: string;
|
||||
|
||||
tree?: string;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@ -83,6 +85,7 @@ export class Collection<
|
||||
|
||||
this.db.modelCollection.set(this.model, this);
|
||||
this.db.tableNameCollectionMap.set(this.model.tableName, this);
|
||||
this.treeHook();
|
||||
|
||||
if (!options.inherits) {
|
||||
this.setFields(options.fields);
|
||||
@ -92,6 +95,51 @@ export class Collection<
|
||||
this.setSortable(options.sortable);
|
||||
}
|
||||
|
||||
treeHook() {
|
||||
if (!this.options.tree) {
|
||||
return;
|
||||
}
|
||||
this.on('field.beforeAdd', (name, opts, { collection }) => {
|
||||
if (!collection.options.tree) {
|
||||
return;
|
||||
}
|
||||
if (name === 'parent' || name === 'children') {
|
||||
opts.target = collection.name;
|
||||
opts.foreignKey = 'parentId';
|
||||
}
|
||||
});
|
||||
this.model.afterFind(async (instances, options: any) => {
|
||||
if (!options.tree) {
|
||||
return;
|
||||
}
|
||||
const arr: Model[] = Array.isArray(instances) ? instances : [instances];
|
||||
let index = 0;
|
||||
for (const instance of arr) {
|
||||
const opts = {
|
||||
...lodash.pick(options, ['tree', 'fields', 'appends', 'except', 'sort']),
|
||||
};
|
||||
let __index = `${index++}`;
|
||||
if (options.parentIndex) {
|
||||
__index = `${options.parentIndex}.${__index}`;
|
||||
}
|
||||
instance.setDataValue('__index', __index);
|
||||
const children = await this.repository.find({
|
||||
filter: {
|
||||
parentId: instance.id,
|
||||
},
|
||||
transaction: options.transaction,
|
||||
...opts,
|
||||
// @ts-ignore
|
||||
parentIndex: `${__index}.children`,
|
||||
context: options.context,
|
||||
});
|
||||
if (children?.length > 0) {
|
||||
instance.setDataValue('children', children);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkOptions(options: CollectionOptions) {
|
||||
checkIdentifier(options.name);
|
||||
this.checkTableName();
|
||||
@ -231,6 +279,7 @@ export class Collection<
|
||||
this.checkFieldType(name, options);
|
||||
|
||||
const { database } = this.context;
|
||||
this.emit('field.beforeAdd', name, options, { collection: this });
|
||||
|
||||
const field = database.buildField(
|
||||
{ name, ...options },
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
Op,
|
||||
Transactionable,
|
||||
UpdateOptions as SequelizeUpdateOptions,
|
||||
WhereOperators,
|
||||
WhereOperators
|
||||
} from 'sequelize';
|
||||
import { Collection } from './collection';
|
||||
import { Database } from './database';
|
||||
@ -106,6 +106,7 @@ export interface CommonFindOptions extends Transactionable {
|
||||
except?: Except;
|
||||
sort?: Sort;
|
||||
context?: any;
|
||||
tree?: boolean;
|
||||
}
|
||||
|
||||
export type FindOneOptions = Omit<FindOptions, 'limit'>;
|
||||
|
@ -99,7 +99,7 @@ export function afterCreateForForeignKeyField(db: Database) {
|
||||
});
|
||||
}
|
||||
|
||||
return async (model, { transaction, context }) => {
|
||||
const hook = async (model, { transaction, context }) => {
|
||||
// skip if no app context
|
||||
if (!context) {
|
||||
return;
|
||||
@ -172,4 +172,12 @@ export function afterCreateForForeignKeyField(db: Database) {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return async (model, options) => {
|
||||
try {
|
||||
await hook(model, options);
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
beforeCreateForChildrenCollection,
|
||||
beforeCreateForReverseField,
|
||||
beforeDestroyForeignKey,
|
||||
beforeInitOptions,
|
||||
beforeInitOptions
|
||||
} from './hooks';
|
||||
|
||||
import { InheritedCollection } from '@nocobase/database';
|
||||
|
Loading…
x
Reference in New Issue
Block a user