Refactor/2.0 association (#7136)

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: code improve
This commit is contained in:
Katherine 2025-06-30 20:03:12 +08:00 committed by GitHub
parent a33a91a091
commit 28baf20755
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 207 additions and 149 deletions

View File

@ -46,24 +46,37 @@ export const titleField = defineAction({
},
},
defaultParams: (ctx: any) => {
const { target } = ctx.model.collectionField.options;
const collectionManager = ctx.model.collectionField.collection.collectionManager;
const targetCollection = collectionManager.getCollection(target);
const targetCollection = ctx.model.targetCollection;
const filterKey = getUniqueKeyFromCollection(targetCollection.options as any);
return {
label: ctx.model.props.fieldNames?.label || targetCollection.options.titleField || filterKey,
};
},
handler(ctx: any, params) {
const { target } = ctx.model.collectionField.options;
const collectionManager = ctx.model.collectionField.collection.collectionManager;
ctx.model.setStepParams;
const targetCollection = collectionManager.getCollection(target);
async handler(ctx: any, params) {
const target = ctx.model.collectionField.target;
const targetCollection = ctx.model.collectionField.targetCollection;
console.log(ctx.model.collectionField);
const filterKey = getUniqueKeyFromCollection(targetCollection.options as any);
const label = params.label || targetCollection.options.titleField || filterKey;
const newFieldNames = {
value: filterKey,
label: params.label || targetCollection.options.titleField || filterKey,
label,
};
ctx.model.setComponentProps({ fieldNames: newFieldNames });
const targetCollectionField = targetCollection.getField(label);
const use = targetCollectionField.getFirstSubclassNameOf('ReadPrettyFieldModel') || 'ReadPrettyFieldModel';
const model = ctx.model.setSubModel('field', {
use,
stepParams: {
default: {
step1: {
dataSourceKey: ctx.model.collectionField.dataSourceKey,
collectionName: target,
fieldPath: newFieldNames.label,
},
},
},
});
await model.applyAutoFlows();
},
});

View File

@ -9,8 +9,7 @@
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Select } from 'antd';
import React from 'react';
import { FlowModelRenderer, useFlowEngine, useFlowModel, reactive } from '@nocobase/flow-engine';
import { useCompile } from '../../../../../schema-component';
import { useFlowModel, FlowModel } from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
import { AssociationFieldEditableFieldModel } from './AssociationFieldEditableFieldModel';
@ -34,52 +33,19 @@ function toValue(record: any | any[], fieldNames, multiple = false) {
return convert(record);
}
const modelCache = new Map<string, any>();
function LabelByField(props) {
const { option, fieldNames } = props;
const cacheKey = option[fieldNames.value] + option[fieldNames.label];
const currentModel: any = useFlowModel();
const flowEngine = useFlowEngine();
if (modelCache.has(cacheKey)) {
return option[fieldNames.label] ? <FlowModelRenderer model={modelCache.get(cacheKey)} /> : tval('N/A');
}
const collectionManager = currentModel.collectionField.collection.collectionManager;
const target = currentModel.collectionField?.options?.target;
const targetCollection = collectionManager.getCollection(target);
const targetLabelField = targetCollection.getField(fieldNames.label);
const fieldClasses = Array.from(flowEngine.filterModelClassByParent('ReadPrettyFieldModel').values()).sort(
(a, b) => (a.meta?.sort || 0) - (b.meta?.sort || 0),
);
const fieldClass = fieldClasses.find(
(cls) => cls.supportedFieldInterfaces?.includes(targetLabelField?.options?.interface),
);
const model = flowEngine.createModel({
use: fieldClass?.name || 'ReadPrettyFieldModel',
stepParams: {
default: {
step1: {
dataSourceKey: currentModel.collectionField.collection.dataSourceKey,
collectionName: target,
fieldPath: fieldNames.label,
},
},
},
});
model.setSharedContext({
...currentModel.getSharedContext(),
value: option[fieldNames.label],
const currentModel = useFlowModel();
const field = currentModel.subModels.field as FlowModel;
const key = option[fieldNames.value];
const fieldModel = field.createFork({}, key);
fieldModel.setSharedContext({
value: option?.[fieldNames.label],
currentRecord: option,
});
model.setParent(currentModel.parent);
modelCache.set(cacheKey, model);
return (
<span key={option[fieldNames.value]}>
{option[fieldNames.label] ? <FlowModelRenderer model={model} uid={option[fieldNames.value]} /> : tval('N/A')}
</span>
);
return <span key={option[fieldNames.value]}>{option[fieldNames.label] ? fieldModel.render() : tval('N/A')}</span>;
}
function LazySelect(props) {

View File

@ -11,13 +11,14 @@ import { Input } from '@formily/antd-v5';
import React from 'react';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { EditableFieldModel } from '../EditableFieldModel';
import { useParseMarkdown } from './util';
import { useParseMarkdown, convertToText } from './util';
import { useMarkdownStyles } from './style';
const MarkdownReadPretty = (props) => {
export const MarkdownReadPretty = (props) => {
const { textOnly } = props;
const markdownClass = useMarkdownStyles();
const { html = '' } = useParseMarkdown(props.value);
const text = convertToText(html);
const value = (
<div
className={` ${markdownClass} nb-markdown nb-markdown-default nb-markdown-table`}
@ -25,7 +26,7 @@ const MarkdownReadPretty = (props) => {
/>
);
return value;
return <>{textOnly ? text : value}</>;
};
const Markdown: any = connect(

View File

@ -13,20 +13,3 @@ import { ReadPrettyFieldModel } from '../ReadPrettyFieldModel';
export class AssociationReadPrettyFieldModel extends ReadPrettyFieldModel {
targetCollection;
}
AssociationReadPrettyFieldModel.registerFlow({
key: 'AssociationReadPrettyFieldDefault',
auto: true,
sort: 150,
steps: {
step1: {
handler(ctx, params) {
const { collectionField } = ctx.model;
const { target } = collectionField?.options || {};
const collectionManager = collectionField.collection.collectionManager;
const targetCollection = collectionManager.getCollection(target);
ctx.model.targetCollection = targetCollection;
},
},
},
});

View File

@ -8,10 +8,11 @@
*/
import React from 'react';
import { castArray } from 'lodash';
import { Button } from 'antd';
import { tval } from '@nocobase/utils/client';
import { AssociationReadPrettyFieldModel } from './AssociationReadPrettyFieldModel';
import { FlowEngineProvider, reactive } from '@nocobase/flow-engine';
import { reactive, FlowModel } from '@nocobase/flow-engine';
import { getUniqueKeyFromCollection } from '../../../../../collection-manager/interfaces/utils';
const LinkToggleWrapper = ({ enableLink, children, currentRecord, ...props }) => {
@ -46,70 +47,43 @@ export class AssociationSelectReadPrettyFieldModel extends AssociationReadPretty
set onClick(fn) {
this.setProps({ ...this.props, onClick: fn });
}
private fieldModelCache: Record<string, FlowModel> = {};
@reactive
public render() {
const { fieldNames, enableLink = true } = this.props;
const value = this.getValue();
if (!this.collectionField || !value) {
return;
}
const { target } = this.collectionField?.options || {};
const collectionManager = this.collectionField.collection.collectionManager;
const targetCollection = collectionManager.getCollection(target);
const targetLabelField = targetCollection.getField(fieldNames.label);
const fieldClasses = Array.from(this.flowEngine.filterModelClassByParent('ReadPrettyFieldModel').values())?.sort(
(a, b) => (a.meta?.sort || 0) - (b.meta?.sort || 0),
);
const fieldInterfaceName = targetLabelField?.options?.interface;
const fieldClass = fieldClasses.find((fieldClass) => {
return fieldClass.supportedFieldInterfaces?.includes(fieldInterfaceName);
});
const model = this.flowEngine.createModel({
use: fieldClass?.name || 'ReadPrettyFieldModel',
stepParams: {
default: {
step1: {
dataSourceKey: this.collectionField.collection.dataSourceKey,
collectionName: target,
fieldPath: fieldNames.label,
},
},
},
props: {
dataSource: targetLabelField.enum,
...targetLabelField.getComponentProps(),
},
});
model.setSharedContext({
...this.ctx.shared,
value: value?.[fieldNames.label],
currentRecord: value,
});
model.setParent(this.parent);
if (Array.isArray(value)) {
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
{value.map((v, idx) => {
const mol = model.createFork({}, `${idx}`);
mol.setSharedContext({ ...this.ctx.shared, index: idx, value: v?.[fieldNames.label], currentRecord: v });
return (
<React.Fragment key={idx}>
{idx > 0 && <span style={{ color: 'rgb(170, 170, 170)' }}>,</span>}
<LinkToggleWrapper enableLink={enableLink} {...this.props} currentRecord={v}>
<FlowEngineProvider engine={this.flowEngine}>
{v?.[fieldNames.label] ? mol.render() : this.flowEngine.translate('N/A')}
</FlowEngineProvider>
</LinkToggleWrapper>
</React.Fragment>
);
})}
</div>
);
}
if (!value) return null;
const arrayValue = castArray(value);
const field = this.subModels.field as FlowModel;
return (
<LinkToggleWrapper enableLink={enableLink} {...this.props} currentRecord={value}>
<FlowEngineProvider engine={this.flowEngine}>{model.render()}</FlowEngineProvider>
</LinkToggleWrapper>
<>
{arrayValue.map((v, index) => {
const key = `${index}`;
let fieldModel = this.fieldModelCache[v?.[fieldNames.label]];
if (!fieldModel) {
fieldModel = field.createFork({}, key);
fieldModel.setSharedContext({
index,
value: v?.[fieldNames.label],
currentRecord: v,
});
this.fieldModelCache[v?.[fieldNames.label]] = fieldModel;
}
const content = v?.[fieldNames.label] ? fieldModel.render() : this.flowEngine.translate('N/A');
return (
<React.Fragment key={index}>
{index > 0 && ', '}
<LinkToggleWrapper enableLink={enableLink} {...this.props} currentRecord={v}>
{content}
</LinkToggleWrapper>
</React.Fragment>
);
})}
</>
);
}
}
@ -123,16 +97,32 @@ AssociationSelectReadPrettyFieldModel.registerFlow({
fieldNames: {
use: 'titleField',
title: tval('Title field'),
handler(ctx, params) {
const { target } = ctx.model.collectionField.options;
async handler(ctx, params) {
const { target } = ctx.model.collectionField;
const collectionManager = ctx.model.collectionField.collection.collectionManager;
const targetCollection = collectionManager.getCollection(target);
const filterKey = getUniqueKeyFromCollection(targetCollection.options as any);
const label = params.label || targetCollection.options.titleField || filterKey;
const newFieldNames = {
value: filterKey,
label: params.label || targetCollection.options.titleField || filterKey,
label,
};
const targetCollectionField = targetCollection.getField(label);
const use = targetCollectionField.getFirstSubclassNameOf('ReadPrettyFieldModel') || 'ReadPrettyFieldModel';
ctx.model.setProps({ fieldNames: newFieldNames });
const model = ctx.model.setSubModel('field', {
use,
stepParams: {
default: {
step1: {
dataSourceKey: ctx.model.collectionField.dataSourceKey,
collectionName: target,
fieldPath: newFieldNames.label,
},
},
},
});
await model.applyAutoFlows();
},
},
enableLink: {

View File

@ -1,10 +1,21 @@
/**
* 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 React from 'react';
import { reactive } from '@nocobase/flow-engine';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
import { Checkbox } from 'antd';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
export class CheckboxReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['checkbox'];
@reactive
public render() {
const value = this.getValue();
const { prefix = '', suffix = '', showUnchecked } = this.props;

View File

@ -10,10 +10,12 @@
import React from 'react';
import { ColorPicker } from 'antd';
import { css } from '@emotion/css';
import { reactive } from '@nocobase/flow-engine';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
export class ColorReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['color'];
@reactive
public render() {
const value = this.getValue();

View File

@ -8,11 +8,13 @@
*/
import React from 'react';
import { reactive } from '@nocobase/flow-engine';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
import { Icon } from '../../../../icon';
export class IconReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['icon'];
@reactive
public render() {
const value = this.getValue();

View File

@ -7,7 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React, { useMemo } from 'react';
import React from 'react';
import { reactive } from '@nocobase/flow-engine';
import { cx, css } from '@emotion/css';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
@ -19,6 +20,7 @@ const JSONClassName = css`
export class JsonReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['json'];
@reactive
public render() {
const value = this.getValue();
const { space, style, className } = this.props;

View File

@ -0,0 +1,53 @@
/**
* 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 React from 'react';
import { reactive } from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
import { MarkdownReadPretty } from '../EditableField/MarkdownEditableFieldModel/index';
export class MarkdownReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['markdown'];
@reactive
public render() {
const { textOnly = true } = this.props;
const value = this.getValue();
return <MarkdownReadPretty textOnly={textOnly} value={value} />;
}
}
MarkdownReadPrettyFieldModel.registerFlow({
key: 'displayMode',
title: tval('Specific properties'),
auto: true,
sort: 200,
steps: {
displayMode: {
uiSchema: {
textOnly: {
type: 'string',
enum: [
{ label: tval('Text only'), value: true },
{ label: tval('Html'), value: false },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
},
title: tval('Display mode'),
defaultParams: {
textOnly: true,
},
handler(ctx, params) {
ctx.model.setProps({ textOnly: params.textOnly });
},
},
},
});

View File

@ -1,5 +1,15 @@
/**
* 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 React from 'react';
import * as math from 'mathjs';
import { reactive } from '@nocobase/flow-engine';
import { isNum } from '@formily/shared';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
@ -13,6 +23,7 @@ const toValue = (value: any, callback: (v: number) => number) => {
};
export class PercentReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['percent'];
@reactive
public render() {
const value = this.getValue();
const { prefix = '', suffix = '' } = this.props;

View File

@ -8,23 +8,46 @@
*/
import React from 'react';
import { tval } from '@nocobase/utils/client';
import { reactive } from '@nocobase/flow-engine';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
const lineHeight142 = { lineHeight: '1.42' };
import { MarkdownReadPretty } from '../EditableField/MarkdownEditableFieldModel/index';
export class RichTextReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['richText'];
@reactive
public render() {
const { textOnly = true } = this.props;
const value = this.getValue();
const html = (
<div
style={lineHeight142}
dangerouslySetInnerHTML={{
__html: value,
}}
/>
);
return <div>{html}</div>;
return <MarkdownReadPretty textOnly={textOnly} value={value} />;
}
}
RichTextReadPrettyFieldModel.registerFlow({
key: 'displayMode',
title: tval('Specific properties'),
auto: true,
sort: 200,
steps: {
displayMode: {
uiSchema: {
textOnly: {
type: 'string',
enum: [
{ label: tval('Text only'), value: true },
{ label: tval('Html'), value: false },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
},
title: tval('Display mode'),
defaultParams: {
textOnly: true,
},
handler(ctx, params) {
ctx.model.setProps({ textOnly: params.textOnly });
},
},
},
});

View File

@ -19,3 +19,4 @@ export * from './ColorReadPrettyFieldModel';
export * from './IconReadPrettyFieldModel';
export * from './JsonReadPrettyFieldModel';
export * from './AssociationFieldModel';
export * from './MarkdownReadPrettyFieldModel';