From 3f68c5b83629c731515c91fdb1a0ddfe4cda2dad Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 24 Jan 2025 21:30:45 +0800 Subject: [PATCH 1/3] fix: incorrect variable display in association field quick-add form (#6119) * fix: incorrect variable display in association field quick-add form * fix: current popup record * fix: bug --- lerna.json | 4 +-- .../VariablePopupRecordProvider.tsx | 1 + .../antd/action/Action.Container.tsx | 5 ++-- .../association-field/AssociationSelect.tsx | 25 +++++++++++-------- .../hooks/useParentRecordVariable.ts | 6 +++-- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lerna.json b/lerna.json index c4f90695b5..f96b8bba3f 100644 --- a/lerna.json +++ b/lerna.json @@ -2,9 +2,7 @@ "version": "1.4.30", "npmClient": "yarn", "useWorkspaces": true, - "npmClientArgs": [ - "--ignore-engines" - ], + "npmClientArgs": ["--ignore-engines"], "command": { "version": { "forcePublish": true, diff --git a/packages/core/client/src/modules/variable/variablesProvider/VariablePopupRecordProvider.tsx b/packages/core/client/src/modules/variable/variablesProvider/VariablePopupRecordProvider.tsx index aebdcb3d72..965fa39018 100644 --- a/packages/core/client/src/modules/variable/variablesProvider/VariablePopupRecordProvider.tsx +++ b/packages/core/client/src/modules/variable/variablesProvider/VariablePopupRecordProvider.tsx @@ -9,6 +9,7 @@ import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; +import { useForm } from '@formily/react'; import { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider'; import { Collection } from '../../../data-source/collection/Collection'; import { useCollection } from '../../../data-source/collection/CollectionProvider'; diff --git a/packages/core/client/src/schema-component/antd/action/Action.Container.tsx b/packages/core/client/src/schema-component/antd/action/Action.Container.tsx index dfd2dd83df..54137e4251 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Container.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Container.tsx @@ -12,17 +12,18 @@ import React from 'react'; import { useActionContext } from '.'; import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider'; import { ComposedActionDrawer } from './types'; +import { ActionDrawer } from './Action.Drawer'; const PopupLevelContext = React.createContext(0); export const ActionContainer: ComposedActionDrawer = observer( (props: any) => { - const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext(); + const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext() || {}; const { openMode = defaultOpenMode } = useActionContext(); const popupLevel = React.useContext(PopupLevelContext); const currentLevel = popupLevel + 1; - const Component = getComponentByOpenMode(openMode); + const Component = getComponentByOpenMode(openMode) || ActionDrawer; return ( 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 42bdb76639..923632535f 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 @@ -27,6 +27,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'; @@ -159,17 +160,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-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, From 7c0d424173719cc660434e5f5670508b7feabff7 Mon Sep 17 00:00:00 2001 From: YANG QIA <2013xile@gmail.com> Date: Fri, 24 Jan 2025 22:41:42 +0800 Subject: [PATCH 2/3] fix(m2m-array): updating many to many (array) fields in a subform is not working (#6136) * fix(m2m-array): updating many to many (array) fields in a subform is not working * test: add tests * fix: build * fix: test --- .../belongs-to-array-repository.ts | 22 +++- packages/core/database/src/filter-parser.ts | 2 +- packages/core/database/src/index.ts | 2 +- .../single-relation-repository.ts | 3 +- packages/core/database/src/repository.ts | 2 +- .../core/database/src/update-associations.ts | 26 ++--- packages/core/database/src/utils.ts | 12 ++ .../src/server/__tests__/issues.test.ts | 110 +++++++++++++++++- .../__tests__/m2m-array-bigint-api.test.ts | 18 +-- .../src/server/belongs-to-array-field.ts | 18 ++- 10 files changed, 167 insertions(+), 48 deletions(-) rename packages/core/database/src/{relation-repository => belongs-to-array}/belongs-to-array-repository.ts (85%) 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/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/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 af33944c43..a3a312cb43 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('m2m array api, bigInt targetKey', () => { 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('m2m array api, bigInt targetKey', () => { } 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 d14c755277..91dbc58fdf 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); } From a00dcb78cbb70d74440ee73b690da3e3d8b352b2 Mon Sep 17 00:00:00 2001 From: Katherine Date: Fri, 24 Jan 2025 23:00:35 +0800 Subject: [PATCH 3/3] refactor: optimize filter component in filter form to match filterable settings (#6110) * fix: display checkbox field as select in filter form * fix: bug * chore: filterCollectionffield * refactor: code improve * fix: bug * fix: bug * fix: isTruly and isFalsy * fix: bug * fix: bug * fix: bug * fix: bug --------- Co-authored-by: chenos --- .../FilterFormBlockProvider.tsx | 39 +++--- .../interfaces/properties/operators.ts | 45 ++++++- .../filter-blocks/FilterCollectionField.tsx | 119 ++++++++++++++++++ .../Select/selectComponentFieldSettings.tsx | 8 +- .../schema-component/antd/select/Select.tsx | 4 +- .../core/database/src/operators/boolean.ts | 27 ++-- packages/core/test/src/e2e/templatesOfPage.ts | 6 +- 7 files changed, 216 insertions(+), 32 deletions(-) create mode 100644 packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx diff --git a/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx b/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx index 99d983810c..f6eacc4ede 100644 --- a/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx @@ -10,10 +10,11 @@ import { useFieldSchema } from '@formily/react'; import React from 'react'; import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps'; -import { DatePickerProvider, ActionBarProvider } from '../schema-component'; +import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component'; import { DefaultValueProvider } from '../schema-settings'; import { CollectOperators } from './CollectOperators'; import { FormBlockProvider } from './FormBlockProvider'; +import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField'; export const FilterFormBlockProvider = withDynamicSchemaProps((props) => { const filedSchema = useFieldSchema(); @@ -21,22 +22,24 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => { const deprecatedOperators = filedSchema['x-filter-operators'] || {}; return ( - - - - false}> - - - - - + + + + + false}> + + + + + + ); }); diff --git a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts index a79128316b..ce0b6d411f 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts @@ -164,8 +164,49 @@ export const time = [ ]; export const boolean = [ - { label: '{{t("Yes")}}', value: '$isTruly', selected: true, noValue: true }, - { label: '{{t("No")}}', value: '$isFalsy', noValue: true }, + { + label: '{{t("Yes")}}', + value: '$isTruly', + selected: true, + noValue: true, + schema: { + 'x-component': 'Select', + 'x-component-props': { + multiple: false, + options: [ + { + label: '{{t("Yes")}}', + value: true, + }, + { + label: '{{t("No")}}', + value: false, + }, + ], + }, + }, + }, + { + label: '{{t("No")}}', + value: '$isFalsy', + noValue: true, + schema: { + 'x-component': 'Select', + 'x-component-props': { + multiple: false, + options: [ + { + label: '{{t("Yes")}}', + value: true, + }, + { + label: '{{t("No")}}', + value: false, + }, + ], + }, + }, + }, ]; export const tableoid = [ diff --git a/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx b/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx new file mode 100644 index 0000000000..28f47ad979 --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx @@ -0,0 +1,119 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Field } from '@formily/core'; +import { connect, Schema, useField, useFieldSchema } from '@formily/react'; +import { untracked } from '@formily/reactive'; +import { merge } from '@formily/shared'; +import { concat } from 'lodash'; +import React, { useEffect } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; +import { useDynamicComponentProps } from '../../../hoc/withDynamicSchemaProps'; +import { ErrorFallback, useCompile, useComponent } from '../../../schema-component'; +import { useIsAllowToSetDefaultValue } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; +import { useCollectionManager_deprecated } from '../../../'; +import { + CollectionFieldProvider, + useCollectionField, +} from '../../../data-source/collection-field/CollectionFieldProvider'; + +type Props = { + component: any; + children?: React.ReactNode; +}; + +const setFieldProps = (field: Field, key: string, value: any) => { + untracked(() => { + if (field[key] === undefined) { + field[key] = value; + } + }); +}; + +const setRequired = (field: Field, fieldSchema: Schema, uiSchema: Schema) => { + if (typeof fieldSchema['required'] === 'undefined') { + field.required = !!uiSchema['required']; + } +}; + +/** + * TODO: 初步适配 + * @internal + */ +export const FilterCollectionFieldInternalField: React.FC = (props: Props) => { + const compile = useCompile(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { getInterface } = useCollectionManager_deprecated(); + const { uiSchema: uiSchemaOrigin, defaultValue, interface: collectionInterface } = useCollectionField(); + const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue(); + const targetInterface = getInterface(collectionInterface); + const operator = targetInterface?.filterable?.operators?.find( + (v, index) => v.value === fieldSchema['x-filter-operator'] || index === 0, + ); + const Component = useComponent( + operator?.schema?.['x-component'] || + fieldSchema['x-component-props']?.['component'] || + uiSchemaOrigin?.['x-component'] || + 'Input', + ); + const ctx = useFormBlockContext(); + const dynamicProps = useDynamicComponentProps(uiSchemaOrigin?.['x-use-component-props'], props); + // TODO: 初步适配 + useEffect(() => { + if (!uiSchemaOrigin) { + return; + } + const uiSchema = compile(uiSchemaOrigin); + setFieldProps(field, 'content', uiSchema['x-content']); + setFieldProps(field, 'title', uiSchema.title); + setFieldProps(field, 'description', uiSchema.description); + if (ctx?.form) { + const defaultVal = isAllowToSetDefaultValue() ? fieldSchema.default || defaultValue : undefined; + defaultVal !== null && defaultVal !== undefined && setFieldProps(field, 'initialValue', defaultVal); + } + + if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) { + const concatSchema = concat([], uiSchema['x-validator'] || [], fieldSchema['x-validator'] || []); + field.validator = concatSchema; + } + if (fieldSchema['x-disabled'] === true) { + field.disabled = true; + } + if (fieldSchema['x-read-pretty'] === true) { + field.readPretty = true; + } + setRequired(field, fieldSchema, uiSchema); + // @ts-ignore + field.dataSource = uiSchema.enum; + const originalProps = + compile({ ...(operator?.schema?.['x-component-props'] || {}), ...(uiSchema['x-component-props'] || {}) }) || {}; + + field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {}); + }, [uiSchemaOrigin]); + + if (!uiSchemaOrigin) return null; + + return ; +}; + +export const FilterCollectionField = connect((props) => { + const fieldSchema = useFieldSchema(); + const field = useField(); + return ( + console.log(err)}> + + + + + ); +}); + +FilterCollectionField.displayName = 'FilterCollectionField'; diff --git a/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx index 41357ea3c4..8565ef3e6b 100644 --- a/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx @@ -421,7 +421,13 @@ export const filterSelectComponentFieldSettings = new SchemaSettings({ return isSelectFieldMode && !isFieldReadPretty; }, }, - getAllowMultiple({ title: 'Allow multiple selection' }), + { + ...getAllowMultiple({ title: 'Allow multiple selection' }), + useVisible() { + const field = useField(); + return field.componentProps.multiple !== false; + }, + }, { ...titleField, useVisible: useIsAssociationField, 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/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/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',