fix: sort by association collection (#3058)
* fix: sort by association collection * fix: test * chore: test * chore: create sortfield if hasmany sortable set true * fix: drag sort * chore: update sort by options * chore: sync collection after update sortBy * chore: test * chore: test * chore: test * fix: sort by * fix: model.syncSortByField * fix: hasmany only * fix: sortBy --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
635dcfdbd5
commit
0acd7b6dd3
@ -1,9 +1,157 @@
|
||||
import { mockServer, MockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
import { Database } from '@nocobase/database';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
import { Collection, Database } from '@nocobase/database';
|
||||
import { waitSecond } from '@nocobase/test';
|
||||
|
||||
describe('sort action', () => {
|
||||
describe('associations', () => {
|
||||
let api: MockServer;
|
||||
|
||||
let UserCollection: Collection;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = mockServer();
|
||||
|
||||
registerActions(api);
|
||||
|
||||
UserCollection = api.db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'posts',
|
||||
},
|
||||
|
||||
{ type: 'sort', name: 'sort' },
|
||||
],
|
||||
});
|
||||
|
||||
api.db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'sort', name: 'sort' },
|
||||
{ type: 'belongsTo', name: 'user' },
|
||||
],
|
||||
});
|
||||
|
||||
await api.db.sync();
|
||||
|
||||
for (let index = 1; index < 5; index++) {
|
||||
await UserCollection.repository.create({
|
||||
values: {
|
||||
name: `u${index}`,
|
||||
posts: [
|
||||
{
|
||||
title: `u${index}p1`,
|
||||
},
|
||||
{
|
||||
title: `u${index}p2`,
|
||||
},
|
||||
{
|
||||
title: `u${index}p3`,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
return api.destroy();
|
||||
});
|
||||
|
||||
it('should not move association items when association not sortable', async () => {
|
||||
const u1 = await api.db.getRepository('users').findOne({
|
||||
filter: {
|
||||
name: 'u1',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await api.agent().resource('users.posts', u1.get('id')).move({
|
||||
sourceId: 1,
|
||||
targetId: 3,
|
||||
});
|
||||
|
||||
expect(response.status).not.toEqual(200);
|
||||
});
|
||||
|
||||
it('should move association item', async () => {
|
||||
UserCollection.setField('posts', {
|
||||
sortable: true,
|
||||
type: 'hasMany',
|
||||
});
|
||||
|
||||
await api.db.sync({
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
force: false,
|
||||
});
|
||||
|
||||
const PostCollection = api.db.getCollection('posts');
|
||||
|
||||
const sortFieldName = `${UserCollection.model.associations.posts.foreignKey}Sort`;
|
||||
expect(PostCollection.fields.get(sortFieldName)).toBeDefined();
|
||||
|
||||
const u1 = await api.db.getRepository('users').findOne({
|
||||
filter: {
|
||||
name: 'u1',
|
||||
},
|
||||
});
|
||||
|
||||
await api
|
||||
.agent()
|
||||
.resource('users.posts', u1.get('id'))
|
||||
.create({
|
||||
values: {
|
||||
title: 'u1p4',
|
||||
},
|
||||
});
|
||||
|
||||
const u1p4 = await api.db.getRepository('posts').findOne({
|
||||
filter: {
|
||||
title: 'u1p4',
|
||||
},
|
||||
});
|
||||
|
||||
// should move by association sort field
|
||||
await api
|
||||
.agent()
|
||||
.resource('users.posts', u1.get('id'))
|
||||
.move({
|
||||
sourceId: 1,
|
||||
targetId: u1p4.get('id'),
|
||||
});
|
||||
|
||||
const u1Posts = await api
|
||||
.agent()
|
||||
.resource('users.posts', u1.get('id'))
|
||||
.list({
|
||||
fields: ['title'],
|
||||
sort: [sortFieldName],
|
||||
});
|
||||
|
||||
expect(u1Posts.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 'u1p2',
|
||||
},
|
||||
{
|
||||
title: 'u1p3',
|
||||
},
|
||||
{
|
||||
title: 'u1p4',
|
||||
},
|
||||
{
|
||||
title: 'u1p1',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('same scope', () => {
|
||||
let api: MockServer;
|
||||
|
||||
@ -250,7 +398,7 @@ describe('sort action', () => {
|
||||
|
||||
const beforeUpdatedAts = await getUpdatedAts();
|
||||
|
||||
await sleep(1000);
|
||||
await waitSecond(1000);
|
||||
|
||||
await api.agent().resource('tests').move({
|
||||
sourceId: moveItemId,
|
||||
@ -301,7 +449,7 @@ describe('sort action', () => {
|
||||
});
|
||||
|
||||
const beforeUpdated = t1.get('updatedAt');
|
||||
await sleep(1000);
|
||||
await waitSecond(1000);
|
||||
|
||||
await api
|
||||
.agent()
|
||||
|
@ -20,6 +20,7 @@ function findArgs(ctx: Context) {
|
||||
}
|
||||
}
|
||||
const { tree, fields, filter, appends, except, sort } = params;
|
||||
|
||||
return { tree, filter, fields, appends, except, sort };
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,14 @@
|
||||
import { Op, Model } from 'sequelize';
|
||||
import { Model, Op } from 'sequelize';
|
||||
|
||||
import { Context } from '..';
|
||||
import { Collection, TargetKey, Repository, SortField } from '@nocobase/database';
|
||||
import {
|
||||
BelongsToManyRepository,
|
||||
Collection,
|
||||
HasManyRepository,
|
||||
Repository,
|
||||
SortField,
|
||||
TargetKey,
|
||||
} from '@nocobase/database';
|
||||
import { getRepositoryFromParams } from '../utils';
|
||||
|
||||
export async function move(ctx: Context, next) {
|
||||
@ -9,24 +16,39 @@ export async function move(ctx: Context, next) {
|
||||
|
||||
const { sourceId, targetId, sortField, targetScope, sticky, method } = ctx.action.params;
|
||||
|
||||
if (repository instanceof Repository) {
|
||||
const sortAbleCollection = new SortAbleCollection(repository.collection, sortField);
|
||||
if (repository instanceof BelongsToManyRepository) {
|
||||
throw new Error("Sorting association as 'belongs-to-many' type is not supported.");
|
||||
}
|
||||
|
||||
if (sourceId && targetId) {
|
||||
await sortAbleCollection.move(sourceId, targetId, {
|
||||
insertAfter: method === 'insertAfter',
|
||||
});
|
||||
}
|
||||
|
||||
// change scope
|
||||
if (sourceId && targetScope) {
|
||||
await sortAbleCollection.changeScope(sourceId, targetScope, method);
|
||||
}
|
||||
|
||||
if (sourceId && sticky) {
|
||||
await sortAbleCollection.sticky(sourceId);
|
||||
if (repository instanceof HasManyRepository) {
|
||||
const hasManyField = repository.sourceCollection.getField(repository.associationName);
|
||||
if (!hasManyField.options.sortable) {
|
||||
throw new Error(
|
||||
`association ${hasManyField.options.name} in ${repository.sourceCollection.name} is not sortable`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sortAbleCollection = new SortAbleCollection(
|
||||
repository instanceof Repository ? repository.collection : repository.targetCollection,
|
||||
repository instanceof Repository ? sortField : `${repository.association.foreignKey}Sort`,
|
||||
);
|
||||
|
||||
if (sourceId && targetId) {
|
||||
await sortAbleCollection.move(sourceId, targetId, {
|
||||
insertAfter: method === 'insertAfter',
|
||||
});
|
||||
}
|
||||
|
||||
// change scope
|
||||
if (sourceId && targetScope) {
|
||||
await sortAbleCollection.changeScope(sourceId, targetScope, method);
|
||||
}
|
||||
|
||||
if (sourceId && sticky) {
|
||||
await sortAbleCollection.sticky(sourceId);
|
||||
}
|
||||
|
||||
ctx.body = 'ok';
|
||||
await next();
|
||||
}
|
||||
|
@ -81,9 +81,9 @@ export const TableBlockProvider = (props) => {
|
||||
const record = useRecord();
|
||||
|
||||
const collection = getCollection(props.collection);
|
||||
const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
|
||||
const { treeTable, dragSortBy } = fieldSchema?.['x-decorator-props'] || {};
|
||||
if (props.dragSort) {
|
||||
params['sort'] = ['sort'];
|
||||
params['sort'] = dragSortBy || ['sort'];
|
||||
}
|
||||
let childrenColumnName = 'children';
|
||||
if (collection?.tree && treeTable !== false) {
|
||||
|
@ -2,6 +2,7 @@ import { ArrayItems } from '@formily/antd-v5';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient } from '../../../api-client';
|
||||
import { useFormBlockContext, useTableBlockContext } from '../../../block-provider';
|
||||
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
@ -66,6 +67,7 @@ export const TableBlockDesigner = () => {
|
||||
},
|
||||
[dn, field.decoratorProps, fieldSchema, service],
|
||||
);
|
||||
const api = useAPIClient();
|
||||
return (
|
||||
// fix https://nocobase.height.app/T-2259
|
||||
<RecordProvider parent={record} record={{}}>
|
||||
@ -95,10 +97,20 @@ export const TableBlockDesigner = () => {
|
||||
<SchemaSettings.SwitchItem
|
||||
title={t('Enable drag and drop sorting')}
|
||||
checked={field.decoratorProps.dragSort}
|
||||
onChange={(dragSort) => {
|
||||
onChange={async (dragSort) => {
|
||||
if (dragSort && collectionField) {
|
||||
const { data } = await api.resource('collections.fields', collectionField.collectionName).update({
|
||||
filterByTk: collectionField.name,
|
||||
values: {
|
||||
sortable: true,
|
||||
},
|
||||
});
|
||||
const sortBy = data?.data?.[0]?.sortBy;
|
||||
fieldSchema['x-decorator-props'].dragSortBy = sortBy;
|
||||
}
|
||||
field.decoratorProps.dragSort = dragSort;
|
||||
fieldSchema['x-decorator-props'].dragSort = dragSort;
|
||||
service.run({ ...service.params?.[0], sort: 'sort' });
|
||||
service.run({ ...service.params?.[0], sort: fieldSchema['x-decorator-props'].dragSortBy });
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
|
@ -150,6 +150,20 @@ export class HasManyField extends RelationField {
|
||||
|
||||
this.database.referenceMap.addReference(this.reference(association));
|
||||
|
||||
// add sort field if association is sortable
|
||||
if (this.options.sortable) {
|
||||
const targetCollection = database.modelCollection.get(this.TargetModel);
|
||||
const sortFieldName = `${this.options.foreignKey}Sort`;
|
||||
|
||||
targetCollection.setField(sortFieldName, {
|
||||
type: 'sort',
|
||||
hidden: true,
|
||||
scopeKey: this.options.foreignKey,
|
||||
});
|
||||
|
||||
this.options.sortBy = sortFieldName;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -82,12 +82,13 @@ export class SortField extends Field {
|
||||
const queryInterface = this.collection.db.sequelize.getQueryInterface();
|
||||
|
||||
if (scopeKey) {
|
||||
const scopeField = this.collection.getField(scopeKey);
|
||||
if (!scopeField) {
|
||||
const scopeAttribute = this.collection.model.rawAttributes[scopeKey];
|
||||
|
||||
if (!scopeAttribute) {
|
||||
throw new Error(`can not find scope field ${scopeKey} for collection ${this.collection.name}`);
|
||||
}
|
||||
|
||||
scopeKey = this.collection.model.rawAttributes[scopeKey].field;
|
||||
scopeKey = scopeAttribute.field;
|
||||
}
|
||||
|
||||
const quotedOrderField = queryInterface.quoteIdentifier(orderField);
|
||||
|
@ -20,6 +20,24 @@ describe('collections repository', () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should create collection with sortable option', async () => {
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
sortable: true,
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
expect(db.getCollection('posts').getField('sort')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should create through table when pending fields', async () => {
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
|
@ -17,11 +17,14 @@ describe('hasMany field options', () => {
|
||||
values: {
|
||||
name: 'tests',
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'foos',
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
});
|
||||
|
||||
@ -29,6 +32,56 @@ describe('hasMany field options', () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should create fields with sortable option', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
target: 'foos',
|
||||
sortable: true,
|
||||
foreignKey: 'test_id',
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
await field.reload();
|
||||
expect(field.get('sortable')).toBe(true);
|
||||
expect(field.get('sortBy')).toBe('test_idSort');
|
||||
});
|
||||
|
||||
it('should update field with sortable option', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
target: 'foos',
|
||||
foreignKey: 'test_id',
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
await field.reload();
|
||||
|
||||
expect(field.get('sortBy')).toBe(undefined);
|
||||
|
||||
await Field.repository.update({
|
||||
values: {
|
||||
sortable: true,
|
||||
},
|
||||
filter: {
|
||||
key: field.get('key'),
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
await field.reload();
|
||||
|
||||
expect(field.get('sortBy')).toBe('test_idSort');
|
||||
const collection = db.getCollection('foos');
|
||||
const columns = await db.sequelize.getQueryInterface().describeTable(collection.getTableNameWithSchema());
|
||||
expect(columns).toHaveProperty(collection.model.rawAttributes['test_idSort'].field);
|
||||
});
|
||||
|
||||
it('should generate the foreignKey randomly', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
|
@ -0,0 +1,60 @@
|
||||
import { MockServer } from '@nocobase/test';
|
||||
import { createApp } from '../index';
|
||||
|
||||
describe('collections repository', () => {
|
||||
let app: MockServer;
|
||||
let agent;
|
||||
let db;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
db = app.db;
|
||||
|
||||
agent = app.agent();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should update belongs to many field sortable option', async () => {
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'posts',
|
||||
target: 'posts',
|
||||
foreignKey: 'userId',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
name: 'posts',
|
||||
},
|
||||
});
|
||||
|
||||
const postField = await db.getRepository('fields').findOne({
|
||||
filter: {
|
||||
name: 'posts',
|
||||
},
|
||||
});
|
||||
|
||||
// update has many field
|
||||
const response = await agent.resource('collections.fields', 'users').update({
|
||||
values: {
|
||||
sortable: true,
|
||||
},
|
||||
filter: {
|
||||
key: postField.get('key'),
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.body.data[0].sortable).toBe(true);
|
||||
expect(response.body.data[0].sortBy).toBe('userIdSort');
|
||||
});
|
||||
});
|
@ -9,7 +9,6 @@ export async function createApp(
|
||||
const app = mockServer({
|
||||
acl: false,
|
||||
...options,
|
||||
// plugins: ['error-handler', 'collection-manager', 'ui-schema-storage'],
|
||||
});
|
||||
|
||||
options.beforePlugin && options.beforePlugin(app);
|
||||
|
@ -73,6 +73,9 @@ export const beforeInitOptions = {
|
||||
}
|
||||
setTargetKey(database, model);
|
||||
setSourceKey(database, model);
|
||||
if (model.get('sortable') && model.get('type') === 'hasMany') {
|
||||
model.set('sortBy', model.get('foreignKey') + 'Sort');
|
||||
}
|
||||
},
|
||||
hasOne(model: Model, { database }) {
|
||||
const defaults = {
|
||||
|
@ -25,6 +25,7 @@ export class CollectionModel extends MagicAttributeModel {
|
||||
...this.get(),
|
||||
fields: [],
|
||||
};
|
||||
|
||||
if (this.db.hasCollection(name)) {
|
||||
collection = this.db.getCollection(name);
|
||||
|
||||
|
@ -48,6 +48,20 @@ export class FieldModel extends MagicAttributeModel {
|
||||
return field;
|
||||
}
|
||||
|
||||
async syncSortByField(options: Transactionable) {
|
||||
const collectionName = this.get('collectionName');
|
||||
const collection = this.db.getCollection(collectionName);
|
||||
await this.load(options);
|
||||
await collection.sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
// @ts-ignore
|
||||
transaction: options.transaction,
|
||||
});
|
||||
}
|
||||
|
||||
async migrate({ isNew, ...options }: MigrateOptions = {}) {
|
||||
let field;
|
||||
try {
|
||||
|
@ -13,12 +13,12 @@ import {
|
||||
beforeDestroyForeignKey,
|
||||
beforeInitOptions,
|
||||
} from './hooks';
|
||||
import { beforeCreateForValidateField } from './hooks/beforeCreateForValidateField';
|
||||
import { beforeCreateForViewCollection } from './hooks/beforeCreateForViewCollection';
|
||||
import { CollectionModel, FieldModel } from './models';
|
||||
import collectionActions from './resourcers/collections';
|
||||
import viewResourcer from './resourcers/views';
|
||||
import sqlResourcer from './resourcers/sql';
|
||||
import { beforeCreateForValidateField } from './hooks/beforeCreateForValidateField';
|
||||
import viewResourcer from './resourcers/views';
|
||||
|
||||
export class CollectionManagerPlugin extends Plugin {
|
||||
public schema: string;
|
||||
@ -152,6 +152,10 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
throw new Error('cant update field without a reverseField key');
|
||||
}
|
||||
}
|
||||
// todo: 目前只支持一对多
|
||||
if (model.get('sortable') && model.get('type') === 'hasMany') {
|
||||
model.set('sortBy', model.get('foreignKey') + 'Sort');
|
||||
}
|
||||
});
|
||||
|
||||
this.app.db.on('fields.afterUpdate', async (model: FieldModel, { context, transaction }) => {
|
||||
@ -180,6 +184,10 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
if (prevOnDelete != currentOnDelete) {
|
||||
await model.syncReferenceCheckOption({ transaction });
|
||||
}
|
||||
|
||||
if (model.get('type') === 'hasMany' && model.get('sortable') && model.get('sortBy')) {
|
||||
await model.syncSortByField({ transaction });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.db.on('fields.afterSaveWithAssociations', async (model: FieldModel, { context, transaction }) => {
|
||||
|
Loading…
Reference in New Issue
Block a user