mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-09 23:49:27 +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) {
|
function findArgs(params: ActionParams) {
|
||||||
const { fields, filter, appends, except, sort } = params;
|
const { tree, fields, filter, appends, except, sort } = params;
|
||||||
return { filter, fields, appends, except, sort };
|
return { tree, filter, fields, appends, except, sort };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listWithPagination(ctx: Context) {
|
async function listWithPagination(ctx: Context) {
|
||||||
|
@ -84,7 +84,7 @@ const getSchema = (schema, category, compile): ISchema => {
|
|||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
useAction: () => useCreateCollection(),
|
useAction: () => useCreateCollection(schema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -188,16 +188,19 @@ const useDefaultCollectionFields = (values) => {
|
|||||||
return defaults;
|
return defaults;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useCreateCollection = () => {
|
const useCreateCollection = (schema?: any) => {
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const { refreshCM } = useCollectionManager();
|
const { refreshCM } = useCollectionManager();
|
||||||
const ctx = useActionContext();
|
const ctx = useActionContext();
|
||||||
const { refresh } = useResourceActionContext();
|
const { refresh } = useResourceActionContext();
|
||||||
const { resource } = useResourceContext();
|
const { resource, collection } = useResourceContext();
|
||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
await form.submit();
|
await form.submit();
|
||||||
const values = cloneDeep(form.values);
|
const values = cloneDeep(form.values);
|
||||||
|
if (schema?.events?.beforeSubmit) {
|
||||||
|
schema.events.beforeSubmit(values);
|
||||||
|
}
|
||||||
const fields = useDefaultCollectionFields(values);
|
const fields = useDefaultCollectionFields(values);
|
||||||
if (values.autoCreateReverseField) {
|
if (values.autoCreateReverseField) {
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
recordPickerSelector,
|
recordPickerSelector,
|
||||||
recordPickerViewer,
|
recordPickerViewer,
|
||||||
relationshipType,
|
relationshipType,
|
||||||
reverseFieldProperties,
|
reverseFieldProperties
|
||||||
} from './properties';
|
} from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from './calendar';
|
export * from './calendar';
|
||||||
export * from './general';
|
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;
|
order?: number;
|
||||||
/** 默认配置 */
|
/** 默认配置 */
|
||||||
default?: CollectionOptions;
|
default?: CollectionOptions;
|
||||||
|
events?: any;
|
||||||
/** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */
|
/** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */
|
||||||
configurableProperties?: Record<string, ISchema>;
|
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,
|
QueryInterfaceDropTableOptions,
|
||||||
SyncOptions,
|
SyncOptions,
|
||||||
Transactionable,
|
Transactionable,
|
||||||
Utils,
|
Utils
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import { Field, FieldOptions } from './fields';
|
import { Field, FieldOptions } from './fields';
|
||||||
@ -37,6 +37,8 @@ export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'>
|
|||||||
*/
|
*/
|
||||||
magicAttribute?: string;
|
magicAttribute?: string;
|
||||||
|
|
||||||
|
tree?: string;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +85,7 @@ export class Collection<
|
|||||||
|
|
||||||
this.db.modelCollection.set(this.model, this);
|
this.db.modelCollection.set(this.model, this);
|
||||||
this.db.tableNameCollectionMap.set(this.model.tableName, this);
|
this.db.tableNameCollectionMap.set(this.model.tableName, this);
|
||||||
|
this.treeHook();
|
||||||
|
|
||||||
if (!options.inherits) {
|
if (!options.inherits) {
|
||||||
this.setFields(options.fields);
|
this.setFields(options.fields);
|
||||||
@ -92,6 +95,51 @@ export class Collection<
|
|||||||
this.setSortable(options.sortable);
|
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) {
|
private checkOptions(options: CollectionOptions) {
|
||||||
checkIdentifier(options.name);
|
checkIdentifier(options.name);
|
||||||
this.checkTableName();
|
this.checkTableName();
|
||||||
@ -231,6 +279,7 @@ export class Collection<
|
|||||||
this.checkFieldType(name, options);
|
this.checkFieldType(name, options);
|
||||||
|
|
||||||
const { database } = this.context;
|
const { database } = this.context;
|
||||||
|
this.emit('field.beforeAdd', name, options, { collection: this });
|
||||||
|
|
||||||
const field = database.buildField(
|
const field = database.buildField(
|
||||||
{ name, ...options },
|
{ name, ...options },
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
Op,
|
Op,
|
||||||
Transactionable,
|
Transactionable,
|
||||||
UpdateOptions as SequelizeUpdateOptions,
|
UpdateOptions as SequelizeUpdateOptions,
|
||||||
WhereOperators,
|
WhereOperators
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { Collection } from './collection';
|
import { Collection } from './collection';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
@ -106,6 +106,7 @@ export interface CommonFindOptions extends Transactionable {
|
|||||||
except?: Except;
|
except?: Except;
|
||||||
sort?: Sort;
|
sort?: Sort;
|
||||||
context?: any;
|
context?: any;
|
||||||
|
tree?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FindOneOptions = Omit<FindOptions, 'limit'>;
|
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
|
// skip if no app context
|
||||||
if (!context) {
|
if (!context) {
|
||||||
return;
|
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,
|
beforeCreateForChildrenCollection,
|
||||||
beforeCreateForReverseField,
|
beforeCreateForReverseField,
|
||||||
beforeDestroyForeignKey,
|
beforeDestroyForeignKey,
|
||||||
beforeInitOptions,
|
beforeInitOptions
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
|
|
||||||
import { InheritedCollection } from '@nocobase/database';
|
import { InheritedCollection } from '@nocobase/database';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user