diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx
index a70f4edf74..4e3c52018e 100644
--- a/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx
+++ b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx
@@ -28,6 +28,7 @@ import { isVariable } from '../../../variables/utils/isVariable';
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import useServiceOptions, { useAssociationFieldContext } from './hooks';
+import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
export type AssociationSelectProps = RemoteSelectProps
& {
addMode?: 'quickAdd' | 'modalAdd';
@@ -160,17 +161,19 @@ const InternalAssociationSelect = observer(
{addMode === 'modalAdd' && (
- {/* 快捷添加按钮添加的添加的是一个普通的 form 区块(非关系区块),不应该与任何字段有关联,所以在这里把字段相关的上下文给清除掉 */}
-
- {
- return s['x-component'] === 'Action';
- }}
- />
-
+
+ {/* 快捷添加按钮添加的添加的是一个普通的 form 区块(非关系区块),不应该与任何字段有关联,所以在这里把字段相关的上下文给清除掉 */}
+
+ {
+ return s['x-component'] === 'Action';
+ }}
+ />
+
+
)}
diff --git a/packages/core/client/src/schema-component/antd/select/Select.tsx b/packages/core/client/src/schema-component/antd/select/Select.tsx
index 79292ce358..269beecddd 100644
--- a/packages/core/client/src/schema-component/antd/select/Select.tsx
+++ b/packages/core/client/src/schema-component/antd/select/Select.tsx
@@ -17,6 +17,7 @@ import React from 'react';
import { ReadPretty } from './ReadPretty';
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
import { BaseOptionType, DefaultOptionType } from 'antd/es/select';
+import { useCompile } from '../../';
export type SelectProps<
ValueType = any,
@@ -120,6 +121,7 @@ const filterOption = (input, option) => (option?.label ?? '').toLowerCase().incl
const InternalSelect = connect(
(props: SelectProps) => {
const { objectValue, loading, value, rawOptions, defaultValue, ...others } = props;
+ const compile = useCompile();
let mode: any = props.multiple ? 'multiple' : props.mode;
if (mode && !['multiple', 'tags'].includes(mode)) {
mode = undefined;
@@ -172,7 +174,7 @@ const InternalSelect = connect(
);
}}
- {...others}
+ {...compile(others)}
onChange={(changed) => {
props.onChange?.(changed === undefined ? null : changed);
}}
diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useParentRecordVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useParentRecordVariable.ts
index 8ac8f4e9f6..966d75a150 100644
--- a/packages/core/client/src/schema-settings/VariableInput/hooks/useParentRecordVariable.ts
+++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useParentRecordVariable.ts
@@ -10,7 +10,7 @@
import { Schema } from '@formily/json-schema';
import { useTranslation } from 'react-i18next';
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
-import { useCollection } from '../../../data-source';
+import { useCollection, useCollectionField } from '../../../data-source';
import { useCollectionRecord } from '../../../data-source/collection-record/CollectionRecordProvider';
import { useParentCollection } from '../../../data-source/collection/AssociationProvider';
import { useFlag } from '../../../flag-provider/hooks/useFlag';
@@ -59,12 +59,14 @@ export const useCurrentParentRecordContext = () => {
const collection = useCollection();
const { isInSubForm, isInSubTable } = useFlag() || {};
const dataSource = parentCollectionName ? parentDataSource : collection?.dataSource;
+ const associationCollectionField = useCollectionField();
return {
// 当该变量使用在区块数据范围的时候,由于某些区块(如 Table)是在 DataBlockProvider 之前解析 filter 的,
// 导致此时 record.parentRecord 的值还是空的,此时正确的值应该是 record,所以在后面加了 record?.data 来防止这种情况
currentParentRecordCtx: record?.parentRecord?.data || record?.data,
- shouldDisplayCurrentParentRecord: !!record?.parentRecord?.data && !isInSubForm && !isInSubTable,
+ shouldDisplayCurrentParentRecord:
+ !!record?.parentRecord?.data && !isInSubForm && !isInSubTable && associationCollectionField,
// 在后面加上 collection?.name 的原因如上面的变量一样
collectionName: parentCollectionName || collection?.name,
dataSource,
diff --git a/packages/core/database/src/relation-repository/belongs-to-array-repository.ts b/packages/core/database/src/belongs-to-array/belongs-to-array-repository.ts
similarity index 85%
rename from packages/core/database/src/relation-repository/belongs-to-array-repository.ts
rename to packages/core/database/src/belongs-to-array/belongs-to-array-repository.ts
index ba574a2310..375e65829e 100644
--- a/packages/core/database/src/relation-repository/belongs-to-array-repository.ts
+++ b/packages/core/database/src/belongs-to-array/belongs-to-array-repository.ts
@@ -7,14 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { omit } from 'lodash';
+import _ from 'lodash';
import { Transactionable } from 'sequelize/types';
import { Collection } from '../collection';
import { transactionWrapperBuilder } from '../decorators/transaction-decorator';
import { FindOptions } from '../repository';
-import { MultipleRelationRepository } from './multiple-relation-repository';
+import { MultipleRelationRepository } from '../relation-repository/multiple-relation-repository';
import Database from '../database';
import { Model } from '../model';
+import { UpdateAssociationOptions } from '../update-associations';
const transaction = transactionWrapperBuilder(function () {
return this.collection.model.sequelize.transaction();
@@ -68,6 +69,21 @@ export class BelongsToArrayAssociation {
on: this.db.sequelize.literal(`${left}=any(${right})`),
};
}
+
+ async update(instance: Model, value: any, options: UpdateAssociationOptions = {}) {
+ // @ts-ignore
+ await instance.update(
+ {
+ [this.as]: value,
+ },
+ {
+ values: {
+ [this.as]: value,
+ },
+ transaction: options?.transaction,
+ },
+ );
+ }
}
export class BelongsToArrayRepository extends MultipleRelationRepository {
@@ -101,7 +117,7 @@ export class BelongsToArrayRepository extends MultipleRelationRepository {
}
const findOptions = {
- ...omit(options, ['filterByTk', 'where', 'values', 'attributes']),
+ ..._.omit(options, ['filterByTk', 'where', 'values', 'attributes']),
filter: {
$and: [options.filter || {}, addFilter],
},
diff --git a/packages/core/database/src/filter-parser.ts b/packages/core/database/src/filter-parser.ts
index 6f67151914..eaad38fcec 100644
--- a/packages/core/database/src/filter-parser.ts
+++ b/packages/core/database/src/filter-parser.ts
@@ -13,7 +13,7 @@ import { ModelStatic } from 'sequelize';
import { Collection } from './collection';
import { Database } from './database';
import { Model } from './model';
-import { BelongsToArrayAssociation } from './relation-repository/belongs-to-array-repository';
+import { BelongsToArrayAssociation } from './belongs-to-array/belongs-to-array-repository';
const debug = require('debug')('noco-database');
diff --git a/packages/core/database/src/index.ts b/packages/core/database/src/index.ts
index 3d5e177651..cbd7adf116 100644
--- a/packages/core/database/src/index.ts
+++ b/packages/core/database/src/index.ts
@@ -44,7 +44,7 @@ export * from './relation-repository/belongs-to-repository';
export * from './relation-repository/hasmany-repository';
export * from './relation-repository/multiple-relation-repository';
export * from './relation-repository/single-relation-repository';
-export * from './relation-repository/belongs-to-array-repository';
+export * from './belongs-to-array/belongs-to-array-repository';
export * from './repository';
export * from './update-associations';
export { snakeCase } from './utils';
diff --git a/packages/core/database/src/operators/boolean.ts b/packages/core/database/src/operators/boolean.ts
index 805ecf918d..b0130e5120 100644
--- a/packages/core/database/src/operators/boolean.ts
+++ b/packages/core/database/src/operators/boolean.ts
@@ -10,7 +10,26 @@
import { Op } from 'sequelize';
export default {
- $isFalsy() {
+ $isFalsy(value) {
+ if (value === true || value === 'true') {
+ return {
+ [Op.or]: {
+ [Op.is]: null,
+ [Op.eq]: false,
+ },
+ };
+ }
+ return {
+ [Op.eq]: true,
+ };
+ },
+
+ $isTruly(value) {
+ if (value === true || value === 'true') {
+ return {
+ [Op.eq]: true,
+ };
+ }
return {
[Op.or]: {
[Op.is]: null,
@@ -18,10 +37,4 @@ export default {
},
};
},
-
- $isTruly() {
- return {
- [Op.eq]: true,
- };
- },
} as Record;
diff --git a/packages/core/database/src/relation-repository/single-relation-repository.ts b/packages/core/database/src/relation-repository/single-relation-repository.ts
index 710609d1b5..788257ff9f 100644
--- a/packages/core/database/src/relation-repository/single-relation-repository.ts
+++ b/packages/core/database/src/relation-repository/single-relation-repository.ts
@@ -13,6 +13,7 @@ import { Model } from '../model';
import { FindOptions, TargetKey, UpdateOptions } from './types';
import { updateModelByValues } from '../update-associations';
import { RelationRepository, transaction } from './relation-repository';
+import lodash from 'lodash';
interface SetOption extends Transactionable {
tk?: TargetKey;
@@ -100,7 +101,7 @@ export abstract class SingleRelationRepository extends RelationRepository {
}
await updateModelByValues(target, options?.values, {
- ...options,
+ ...lodash.omit(options, 'values'),
transaction,
});
diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts
index e3ba5c8b7f..fd05dc7db1 100644
--- a/packages/core/database/src/repository.ts
+++ b/packages/core/database/src/repository.ts
@@ -38,7 +38,7 @@ import FilterParser from './filter-parser';
import { Model } from './model';
import operators from './operators';
import { OptionsParser } from './options-parser';
-import { BelongsToArrayRepository } from './relation-repository/belongs-to-array-repository';
+import { BelongsToArrayRepository } from './belongs-to-array/belongs-to-array-repository';
import { BelongsToManyRepository } from './relation-repository/belongs-to-many-repository';
import { BelongsToRepository } from './relation-repository/belongs-to-repository';
import { HasManyRepository } from './relation-repository/hasmany-repository';
diff --git a/packages/core/database/src/update-associations.ts b/packages/core/database/src/update-associations.ts
index a0a57f5ecb..bf07a4cd17 100644
--- a/packages/core/database/src/update-associations.ts
+++ b/packages/core/database/src/update-associations.ts
@@ -22,18 +22,7 @@ import { Model } from './model';
import { UpdateGuard } from './update-guard';
import { TargetKey } from './repository';
import Database from './database';
-
-function isUndefinedOrNull(value: any) {
- return typeof value === 'undefined' || value === null;
-}
-
-function isStringOrNumber(value: any) {
- return typeof value === 'string' || typeof value === 'number';
-}
-
-function getKeysByPrefix(keys: string[], prefix: string) {
- return keys.filter((key) => key.startsWith(`${prefix}.`)).map((key) => key.substring(prefix.length + 1));
-}
+import { getKeysByPrefix, isStringOrNumber, isUndefinedOrNull } from './utils';
export function modelAssociations(instance: Model) {
return (instance.constructor).associations;
@@ -51,7 +40,12 @@ export function belongsToManyAssociations(instance: Model): Array
});
}
-export function modelAssociationByKey(instance: Model, key: string): Association {
+export function modelAssociationByKey(
+ instance: Model,
+ key: string,
+): Association & {
+ update?: (instance: Model, value: any, options: UpdateAssociationOptions) => Promise;
+} {
return modelAssociations(instance)[key] as Association;
}
@@ -71,7 +65,7 @@ interface UpdateOptions extends Transactionable {
sourceModel?: Model;
}
-interface UpdateAssociationOptions extends Transactionable, Hookable {
+export interface UpdateAssociationOptions extends Transactionable, Hookable {
updateAssociationValues?: string[];
sourceModel?: Model;
context?: any;
@@ -243,6 +237,10 @@ export async function updateAssociation(
return false;
}
+ if (association.update) {
+ return association.update(instance, value, options);
+ }
+
switch (association.associationType) {
case 'HasOne':
case 'BelongsTo':
diff --git a/packages/core/database/src/utils.ts b/packages/core/database/src/utils.ts
index d088178dd8..ad5438acf6 100644
--- a/packages/core/database/src/utils.ts
+++ b/packages/core/database/src/utils.ts
@@ -116,3 +116,15 @@ export function percent2float(value: string) {
const v = parseInt('1' + '0'.repeat(repeat));
return (parseFloat(value) * v) / (100 * v);
}
+
+export function isUndefinedOrNull(value: any) {
+ return typeof value === 'undefined' || value === null;
+}
+
+export function isStringOrNumber(value: any) {
+ return typeof value === 'string' || typeof value === 'number';
+}
+
+export function getKeysByPrefix(keys: string[], prefix: string) {
+ return keys.filter((key) => key.startsWith(`${prefix}.`)).map((key) => key.substring(prefix.length + 1));
+}
diff --git a/packages/core/test/src/e2e/templatesOfPage.ts b/packages/core/test/src/e2e/templatesOfPage.ts
index 1831801496..9bcd9c1026 100644
--- a/packages/core/test/src/e2e/templatesOfPage.ts
+++ b/packages/core/test/src/e2e/templatesOfPage.ts
@@ -12515,7 +12515,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': 'general.oneToOneBelongsTo',
'x-component-props': {
- multiple: false,
+ multiple: true,
fieldNames: {
label: 'id',
value: 'id',
@@ -12562,7 +12562,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': 'general.oneToOneHasOne',
'x-component-props': {
- multiple: false,
+ multiple: true,
fieldNames: {
label: 'id',
value: 'id',
@@ -12656,7 +12656,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': 'general.manyToOne',
'x-component-props': {
- multiple: false,
+ multiple: true,
fieldNames: {
label: 'id',
value: 'id',
diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/issues.test.ts b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/issues.test.ts
index 2675f72cb2..536d72d1a5 100644
--- a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/issues.test.ts
+++ b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/issues.test.ts
@@ -113,7 +113,7 @@ describe('issues', () => {
expect(res.status).toBe(200);
});
- test('update m2m array field in single realtion collection', async () => {
+ test('update m2m array field in single relation collection, a.b', async () => {
await db.getRepository('collections').create({
values: {
name: 'tags',
@@ -216,4 +216,112 @@ describe('issues', () => {
}
expect(res.status).toBe(200);
});
+
+ test('update m2m array field in single relation collection', async () => {
+ await db.getRepository('collections').create({
+ values: {
+ name: 'tags',
+ fields: [
+ {
+ name: 'id',
+ type: 'bigInt',
+ autoIncrement: true,
+ primaryKey: true,
+ allowNull: false,
+ },
+ {
+ name: 'title',
+ type: 'string',
+ },
+ ],
+ },
+ });
+ await db.getRepository('collections').create({
+ values: {
+ name: 'users',
+ fields: [
+ {
+ name: 'id',
+ type: 'bigInt',
+ autoIncrement: true,
+ primaryKey: true,
+ allowNull: false,
+ },
+ {
+ name: 'username',
+ type: 'string',
+ },
+ {
+ name: 'tags',
+ type: 'belongsToArray',
+ foreignKey: 'tag_ids',
+ target: 'tags',
+ targetKey: 'id',
+ },
+ ],
+ },
+ });
+ await db.getRepository('collections').create({
+ values: {
+ name: 'projects',
+ fields: [
+ {
+ name: 'id',
+ type: 'bigInt',
+ autoIncrement: true,
+ primaryKey: true,
+ allowNull: false,
+ },
+ {
+ name: 'title',
+ type: 'string',
+ },
+ {
+ name: 'users',
+ type: 'belongsTo',
+ foreignKey: 'user_id',
+ target: 'users',
+ },
+ ],
+ },
+ });
+ // @ts-ignore
+ await db.getRepository('collections').load();
+ await db.sync();
+ await db.getRepository('tags').create({
+ values: [{ title: 'a' }, { title: 'b' }, { title: 'c' }],
+ });
+ await db.getRepository('users').create({
+ values: { id: 1, username: 'a' },
+ });
+ let user = await db.getRepository('users').findOne({
+ filterByTk: 1,
+ });
+ expect(user.tag_ids).toEqual(null);
+ await db.getRepository('projects').create({
+ values: { id: 1, title: 'p1', user_id: 1 },
+ });
+ const res = await agent.resource('projects').update({
+ filterByTk: 1,
+ updateAssociationValues: ['users', 'users.tags'],
+ values: {
+ users: {
+ id: 1,
+ tags: [
+ { id: 1, title: 'a' },
+ { id: 2, title: 'b' },
+ ],
+ },
+ },
+ });
+ user = await db.getRepository('users').findOne({
+ filterByTk: 1,
+ });
+ if (db.sequelize.getDialect() === 'postgres') {
+ expect(user.tag_ids).toMatchObject(['1', '2']);
+ } else {
+ expect(user.tag_ids).toMatchObject([1, 2]);
+ }
+ expect(res.status).toBe(200);
+ });
});
diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts
index 7dcc0dd53d..e20b709e05 100644
--- a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts
+++ b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts
@@ -239,11 +239,7 @@ describe('m2m array api, bigInt targetKey', () => {
tags: [{ id: 1 }, { id: 3 }],
},
});
- if (db.sequelize.getDialect() === 'postgres') {
- expect(user.tag_ids).toMatchObject(['1', '3']);
- } else {
- expect(user.tag_ids).toMatchObject([1, 3]);
- }
+ expect(user.tag_ids).toMatchObject([1, 3]);
const user2 = await db.getRepository('users').create({
values: {
id: 4,
@@ -251,11 +247,7 @@ describe('m2m array api, bigInt targetKey', () => {
tags: [1, 3],
},
});
- if (db.sequelize.getDialect() === 'postgres') {
- expect(user2.tag_ids).toMatchObject(['1', '3']);
- } else {
- expect(user2.tag_ids).toMatchObject([1, 3]);
- }
+ expect(user2.tag_ids).toMatchObject([1, 3]);
const user3 = await db.getRepository('users').create({
values: {
id: 5,
@@ -263,11 +255,7 @@ describe('m2m array api, bigInt targetKey', () => {
tags: { id: 1 },
},
});
- if (db.sequelize.getDialect() === 'postgres') {
- expect(user3.tag_ids).toMatchObject(['1']);
- } else {
- expect(user3.tag_ids).toMatchObject([1]);
- }
+ expect(user3.tag_ids).toMatchObject([1]);
});
it('should create target when creating belongsToArray', async () => {
diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/belongs-to-array-field.ts b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/belongs-to-array-field.ts
index 8bf7c1d2fe..b02241ee69 100644
--- a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/belongs-to-array-field.ts
+++ b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/belongs-to-array-field.ts
@@ -54,17 +54,6 @@ export class BelongsToArrayField extends RelationField {
model.set(foreignKey, tks);
};
- init() {
- super.init();
- const { name, ...opts } = this.options;
- this.collection.model.associations[name] = new BelongsToArrayAssociation({
- db: this.database,
- source: this.collection.model,
- as: name,
- ...opts,
- }) as any;
- }
-
checkTargetCollection() {
const { target } = this.options;
if (!target) {
@@ -115,6 +104,13 @@ export class BelongsToArrayField extends RelationField {
return false;
}
this.checkAssociationKeys();
+ const { name, ...opts } = this.options;
+ this.collection.model.associations[name] = new BelongsToArrayAssociation({
+ db: this.database,
+ source: this.collection.model,
+ as: name,
+ ...opts,
+ }) as any;
this.on('beforeSave', this.setForeignKeyArray);
}