diff --git a/packages/core/client/src/flow/models/blocks/details/DetailsItemModel.tsx b/packages/core/client/src/flow/models/blocks/details/DetailsItemModel.tsx index 1e59f0f9ad3..3366218f6e8 100644 --- a/packages/core/client/src/flow/models/blocks/details/DetailsItemModel.tsx +++ b/packages/core/client/src/flow/models/blocks/details/DetailsItemModel.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { FieldModel } from '../../base'; import { DetailsGridModel } from './DetailsGridModel'; import { rebuildFieldSubModel } from '../../../internal/utils/rebuildFieldSubModel'; +import { isToManyAssociationField } from '../../../internal/utils/modelUtils'; /** * 从 record 中取值 @@ -58,6 +59,50 @@ export function getValueWithIndex(record: any, fieldPath: string, idx?: string[] return get(record, path); } +export function getDetailsItemValue(options: { + record: any; + currentObject?: any; + fieldPath: string; + prefixFieldPath?: string; + idx?: string[]; +}) { + const { record, currentObject, fieldPath, prefixFieldPath, idx } = options; + const valueFromRecord = getValueWithIndex(record, fieldPath, idx); + if (valueFromRecord !== undefined) { + return valueFromRecord; + } + if (currentObject == null) { + return valueFromRecord; + } + if (prefixFieldPath && fieldPath === prefixFieldPath) { + return currentObject; + } + const relativeFieldPath = + prefixFieldPath && fieldPath.startsWith(`${prefixFieldPath}.`) + ? fieldPath.slice(prefixFieldPath.length + 1) + : fieldPath; + if (!relativeFieldPath) { + return currentObject; + } + return get(currentObject, relativeFieldPath); +} + +function normalizeDetailsAssociationValue(collectionField: any, value: any) { + if (!isToManyAssociationField(collectionField)) { + return value; + } + if (Array.isArray(value)) { + return value; + } + if (value && typeof value === 'object' && Array.isArray((value as { rows?: unknown[] }).rows)) { + return (value as { rows: unknown[] }).rows; + } + if (value == null) { + return []; + } + return [value]; +} + export class DetailsItemModel extends DisplayItemModel<{ parent: DetailsGridModel; subModels: { field: FieldModel }; @@ -122,6 +167,7 @@ export class DetailsItemModel extends DisplayItemModel<{ const currentObject = this.context.currentObject; const item = this.context.item; const itemOptions = this.context.getPropertyOptions('item'); + const prefixFieldPath = this.context.prefixFieldPath; // 嵌套场景下继续传透,为字段子模型创建 fork const modelForRender = @@ -161,7 +207,16 @@ export class DetailsItemModel extends DisplayItemModel<{ disabled: this.context.pattern === 'readPretty', } : { ...this.context.blockModel.props, ...this.props }; - const value = getValueWithIndex(record, this.fieldPath, idx); + const value = normalizeDetailsAssociationValue( + this.collectionField, + getDetailsItemValue({ + record, + currentObject, + fieldPath: this.fieldPath, + prefixFieldPath, + idx, + }), + ); return ( diff --git a/packages/core/client/src/flow/models/blocks/details/__tests__/DetailsItemModel.value.test.ts b/packages/core/client/src/flow/models/blocks/details/__tests__/DetailsItemModel.value.test.ts new file mode 100644 index 00000000000..fcc4a1233a5 --- /dev/null +++ b/packages/core/client/src/flow/models/blocks/details/__tests__/DetailsItemModel.value.test.ts @@ -0,0 +1,49 @@ +/** + * 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 { describe, expect, it } from 'vitest'; +import { getDetailsItemValue, getValueWithIndex } from '../DetailsItemModel'; + +describe('DetailsItemModel value resolving', () => { + it('reads nested list item from record with index chain', () => { + expect( + getValueWithIndex( + { + users: [{ profile: { nickname: 'A' } }], + }, + 'users.profile.nickname', + ['users:0'], + ), + ).toBe('A'); + }); + + it('falls back to currentObject with relative field path in sub-detail rows', () => { + expect( + getDetailsItemValue({ + record: { unrelated: true }, + currentObject: { nickname: 'B' }, + fieldPath: 'users.nickname', + prefixFieldPath: 'users', + idx: ['users:0'], + }), + ).toBe('B'); + }); + + it('returns currentObject itself when field path equals prefix field path', () => { + const currentObject = { id: 1, nickname: 'C' }; + expect( + getDetailsItemValue({ + record: {}, + currentObject, + fieldPath: 'users', + prefixFieldPath: 'users', + }), + ).toBe(currentObject); + }); +});