diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 0f72f8e920..f9f5b45ea6 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -1352,7 +1352,12 @@ export const useAssociationNames = (dataSource?: string) => { appends.add(path); appends.add(`${path}.parent` + '(recursively=true)'); } else { - appends.add(path); + if (s['x-component-props']?.sortArr) { + const sort = s['x-component-props']?.sortArr; + appends.add(`${path}(sort=${sort})`); + } else { + appends.add(path); + } } if (['Nester', 'SubTable', 'PopoverNester'].includes(s['x-component-props']?.mode)) { updateAssociationValues.add(path); diff --git a/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx index a525d6ef86..b0d8f27441 100644 --- a/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx @@ -11,6 +11,7 @@ import { } from '../../../../schema-component/antd/form-item/FormItem.Settings'; import { useCollectionField } from '../../../../data-source'; import { useFormBlockType } from '../../../../block-provider'; +import { setDefaultSortingRules } from '../SubTable/subTablePopoverComponentFieldSettings'; const allowMultiple: any = { name: 'allowMultiple', @@ -131,5 +132,6 @@ export const subformComponentFieldSettings = new SchemaSettings({ }; }, }, + setDefaultSortingRules, ], }); diff --git a/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx index 046d9342d8..ea5df9ac14 100644 --- a/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx @@ -1,6 +1,7 @@ import { Field } from '@formily/core'; -import { useField, useFieldSchema } from '@formily/react'; +import { useField, useFieldSchema, ISchema } from '@formily/react'; import { useTranslation } from 'react-i18next'; +import { ArrayItems } from '@formily/antd-v5'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useFieldComponentName } from '../../../../common/useFieldComponentName'; import { @@ -10,6 +11,7 @@ import { useIsFieldReadPretty, } from '../../../../schema-component'; import { isSubMode } from '../../../../schema-component/antd/association-field/util'; +import { useCollectionManager_deprecated, useSortFields } from '../../../../collection-manager'; const fieldComponent: any = { name: 'fieldComponent', @@ -85,7 +87,130 @@ const allowSelectExistingRecord = { }; }, }; + +export const setDefaultSortingRules = { + name: 'SetDefaultSortingRules', + type: 'modal', + useComponentProps() { + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const association = fieldSchema['x-collection-field']; + const { target } = getCollectionJoinField(association) || {}; + const sortFields = useSortFields(target); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const defaultSort = field.componentProps.sortArr || []; + const sort = defaultSort?.map((item: string) => { + return item?.startsWith('-') + ? { + field: item.substring(1), + direction: 'desc', + } + : { + field: item, + direction: 'asc', + }; + }); + + return { + title: t('Set default sorting rules'), + components: { ArrayItems }, + schema: { + type: 'object', + title: t('Set default sorting rules'), + properties: { + sort: { + type: 'array', + default: sort, + 'x-component': 'ArrayItems', + 'x-decorator': 'FormItem', + items: { + type: 'object', + properties: { + space: { + type: 'void', + 'x-component': 'Space', + properties: { + sort: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.SortHandle', + }, + field: { + type: 'string', + enum: sortFields, + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + style: { + width: 260, + }, + }, + }, + direction: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Radio.Group', + 'x-component-props': { + optionType: 'button', + }, + enum: [ + { + label: t('ASC'), + value: 'asc', + }, + { + label: t('DESC'), + value: 'desc', + }, + ], + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + }, + }, + properties: { + add: { + type: 'void', + title: t('Add sort field'), + 'x-component': 'ArrayItems.Addition', + }, + }, + }, + }, + } as ISchema, + onSubmit: ({ sort }) => { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); + field.componentProps.sortArr = sortArr; + fieldSchema['x-component-props'].sortArr = sortArr; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': fieldSchema['x-component-props'], + }, + }); + }, + }; + }, + useVisible() { + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const fieldSchema = useFieldSchema(); + const association = fieldSchema['x-collection-field']; + const { interface: targetInterface } = getCollectionJoinField(association) || {}; + const readPretty = useIsFieldReadPretty(); + return readPretty && ['m2m', 'o2m'].includes(targetInterface); + }, +}; export const subTablePopoverComponentFieldSettings = new SchemaSettings({ name: 'fieldSettings:component:SubTable', - items: [fieldComponent, allowSelectExistingRecord], + items: [fieldComponent, allowSelectExistingRecord, setDefaultSortingRules], }); diff --git a/packages/core/database/src/__tests__/repository/find.test.ts b/packages/core/database/src/__tests__/repository/find.test.ts index 13af2511f2..88dba71c57 100644 --- a/packages/core/database/src/__tests__/repository/find.test.ts +++ b/packages/core/database/src/__tests__/repository/find.test.ts @@ -1,6 +1,7 @@ import { mockDatabase } from '../index'; import Database from '@nocobase/database'; import { Collection } from '../../collection'; +import qs from 'qs'; describe('find with associations', () => { let db: Database; @@ -412,6 +413,64 @@ describe('find with associations', () => { expect(findResult[0].get('name')).toEqual('u1'); }); + + it('should find with associations with sort params', async () => { + const User = db.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'name' }, + { + type: 'hasMany', + name: 'posts', + }, + ], + }); + + const Post = db.collection({ + name: 'posts', + fields: [ + { type: 'string', name: 'title' }, + { type: 'belongsTo', name: 'user' }, + ], + }); + + await db.sync(); + + await User.repository.create({ + values: [ + { + name: 'u1', + posts: [ + { + title: 'u1p1', + }, + { + title: 'u1p2', + }, + ], + }, + { + name: 'u2', + posts: [ + { + title: 'u2p1', + }, + { + title: 'u2p2', + }, + ], + }, + ], + }); + + const appendArgs = [`posts(${qs.stringify({ sort: ['-id'] })})`]; + const users = await User.repository.find({ + appends: appendArgs, + }); + + expect(users[0].get('name')).toEqual('u1'); + expect(users[0].get('posts')[0].get('title')).toEqual('u1p2'); + }); }); describe('repository find', () => { diff --git a/packages/core/database/src/eager-loading/eager-loading-tree.ts b/packages/core/database/src/eager-loading/eager-loading-tree.ts index a0ea7b640d..fafb73699f 100644 --- a/packages/core/database/src/eager-loading/eager-loading-tree.ts +++ b/packages/core/database/src/eager-loading/eager-loading-tree.ts @@ -257,11 +257,26 @@ export class EagerLoadingTree { const association = node.association; const associationType = association.associationType; + let params: any = {}; + + const otherFindOptions = lodash.pick(node.includeOption, ['sort']) || {}; + + const collection = this.db.modelCollection.get(node.model); + + if (collection && !lodash.isEmpty(otherFindOptions)) { + const parser = new OptionsParser(otherFindOptions, { + collection, + }); + + params = parser.toSequelizeParams(); + } + if (associationType == 'HasOne' || associationType == 'HasMany') { const foreignKey = association.foreignKey; const foreignKeyValues = node.parent.instances.map((instance) => instance.get(association.sourceKey)); let where: any = { [foreignKey]: foreignKeyValues }; + if (node.where) { where = { [Op.and]: [where, node.where], @@ -271,7 +286,7 @@ export class EagerLoadingTree { const findOptions = { where, attributes: node.attributes, - order: orderOption(association), + order: params.order || orderOption(association), transaction, }; @@ -358,7 +373,7 @@ export class EagerLoadingTree { }, }, ], - order: orderOption(association), + order: params.order || orderOption(association), }); } }