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) => { defaultParams: (ctx: any) => {
const { target } = ctx.model.collectionField.options; const targetCollection = ctx.model.targetCollection;
const collectionManager = ctx.model.collectionField.collection.collectionManager;
const targetCollection = collectionManager.getCollection(target);
const filterKey = getUniqueKeyFromCollection(targetCollection.options as any); const filterKey = getUniqueKeyFromCollection(targetCollection.options as any);
return { return {
label: ctx.model.props.fieldNames?.label || targetCollection.options.titleField || filterKey, label: ctx.model.props.fieldNames?.label || targetCollection.options.titleField || filterKey,
}; };
}, },
handler(ctx: any, params) { async handler(ctx: any, params) {
const { target } = ctx.model.collectionField.options; const target = ctx.model.collectionField.target;
const collectionManager = ctx.model.collectionField.collection.collectionManager; const targetCollection = ctx.model.collectionField.targetCollection;
ctx.model.setStepParams; console.log(ctx.model.collectionField);
const targetCollection = collectionManager.getCollection(target);
const filterKey = getUniqueKeyFromCollection(targetCollection.options as any); const filterKey = getUniqueKeyFromCollection(targetCollection.options as any);
const label = params.label || targetCollection.options.titleField || filterKey;
const newFieldNames = { const newFieldNames = {
value: filterKey, value: filterKey,
label: params.label || targetCollection.options.titleField || filterKey, label,
}; };
ctx.model.setComponentProps({ fieldNames: newFieldNames }); 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 { connect, mapProps, mapReadPretty } from '@formily/react';
import { Select } from 'antd'; import { Select } from 'antd';
import React from 'react'; import React from 'react';
import { FlowModelRenderer, useFlowEngine, useFlowModel, reactive } from '@nocobase/flow-engine'; import { useFlowModel, FlowModel } from '@nocobase/flow-engine';
import { useCompile } from '../../../../../schema-component';
import { tval } from '@nocobase/utils/client'; import { tval } from '@nocobase/utils/client';
import { AssociationFieldEditableFieldModel } from './AssociationFieldEditableFieldModel'; import { AssociationFieldEditableFieldModel } from './AssociationFieldEditableFieldModel';
@ -34,52 +33,19 @@ function toValue(record: any | any[], fieldNames, multiple = false) {
return convert(record); return convert(record);
} }
const modelCache = new Map<string, any>();
function LabelByField(props) { function LabelByField(props) {
const { option, fieldNames } = props; const { option, fieldNames } = props;
const cacheKey = option[fieldNames.value] + option[fieldNames.label]; const currentModel = useFlowModel();
const currentModel: any = useFlowModel(); const field = currentModel.subModels.field as FlowModel;
const flowEngine = useFlowEngine(); const key = option[fieldNames.value];
if (modelCache.has(cacheKey)) { const fieldModel = field.createFork({}, key);
return option[fieldNames.label] ? <FlowModelRenderer model={modelCache.get(cacheKey)} /> : tval('N/A'); fieldModel.setSharedContext({
} value: option?.[fieldNames.label],
const collectionManager = currentModel.collectionField.collection.collectionManager; currentRecord: option,
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],
}); });
model.setParent(currentModel.parent); return <span key={option[fieldNames.value]}>{option[fieldNames.label] ? fieldModel.render() : tval('N/A')}</span>;
modelCache.set(cacheKey, model);
return (
<span key={option[fieldNames.value]}>
{option[fieldNames.label] ? <FlowModelRenderer model={model} uid={option[fieldNames.value]} /> : tval('N/A')}
</span>
);
} }
function LazySelect(props) { function LazySelect(props) {

View File

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

View File

@ -13,20 +13,3 @@ import { ReadPrettyFieldModel } from '../ReadPrettyFieldModel';
export class AssociationReadPrettyFieldModel extends ReadPrettyFieldModel { export class AssociationReadPrettyFieldModel extends ReadPrettyFieldModel {
targetCollection; 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 React from 'react';
import { castArray } from 'lodash';
import { Button } from 'antd'; import { Button } from 'antd';
import { tval } from '@nocobase/utils/client'; import { tval } from '@nocobase/utils/client';
import { AssociationReadPrettyFieldModel } from './AssociationReadPrettyFieldModel'; 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'; import { getUniqueKeyFromCollection } from '../../../../../collection-manager/interfaces/utils';
const LinkToggleWrapper = ({ enableLink, children, currentRecord, ...props }) => { const LinkToggleWrapper = ({ enableLink, children, currentRecord, ...props }) => {
@ -46,70 +47,43 @@ export class AssociationSelectReadPrettyFieldModel extends AssociationReadPretty
set onClick(fn) { set onClick(fn) {
this.setProps({ ...this.props, onClick: fn }); this.setProps({ ...this.props, onClick: fn });
} }
private fieldModelCache: Record<string, FlowModel> = {};
@reactive @reactive
public render() { public render() {
const { fieldNames, enableLink = true } = this.props; const { fieldNames, enableLink = true } = this.props;
const value = this.getValue(); const value = this.getValue();
if (!this.collectionField || !value) { if (!value) return null;
return;
const arrayValue = castArray(value);
const field = this.subModels.field as FlowModel;
return (
<>
{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 { target } = this.collectionField?.options || {};
const collectionManager = this.collectionField.collection.collectionManager; const content = v?.[fieldNames.label] ? fieldModel.render() : this.flowEngine.translate('N/A');
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 ( return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}> <React.Fragment key={index}>
{value.map((v, idx) => { {index > 0 && ', '}
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}> <LinkToggleWrapper enableLink={enableLink} {...this.props} currentRecord={v}>
<FlowEngineProvider engine={this.flowEngine}> {content}
{v?.[fieldNames.label] ? mol.render() : this.flowEngine.translate('N/A')}
</FlowEngineProvider>
</LinkToggleWrapper> </LinkToggleWrapper>
</React.Fragment> </React.Fragment>
); );
})} })}
</div> </>
);
}
return (
<LinkToggleWrapper enableLink={enableLink} {...this.props} currentRecord={value}>
<FlowEngineProvider engine={this.flowEngine}>{model.render()}</FlowEngineProvider>
</LinkToggleWrapper>
); );
} }
} }
@ -123,16 +97,32 @@ AssociationSelectReadPrettyFieldModel.registerFlow({
fieldNames: { fieldNames: {
use: 'titleField', use: 'titleField',
title: tval('Title field'), title: tval('Title field'),
handler(ctx, params) { async handler(ctx, params) {
const { target } = ctx.model.collectionField.options; const { target } = ctx.model.collectionField;
const collectionManager = ctx.model.collectionField.collection.collectionManager; const collectionManager = ctx.model.collectionField.collection.collectionManager;
const targetCollection = collectionManager.getCollection(target); const targetCollection = collectionManager.getCollection(target);
const filterKey = getUniqueKeyFromCollection(targetCollection.options as any); const filterKey = getUniqueKeyFromCollection(targetCollection.options as any);
const label = params.label || targetCollection.options.titleField || filterKey;
const newFieldNames = { const newFieldNames = {
value: filterKey, 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 }); 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: { 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 React from 'react';
import { reactive } from '@nocobase/flow-engine';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel'; import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
import { Checkbox } from 'antd'; import { Checkbox } from 'antd';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
export class CheckboxReadPrettyFieldModel extends ReadPrettyFieldModel { export class CheckboxReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['checkbox']; public static readonly supportedFieldInterfaces = ['checkbox'];
@reactive
public render() { public render() {
const value = this.getValue(); const value = this.getValue();
const { prefix = '', suffix = '', showUnchecked } = this.props; const { prefix = '', suffix = '', showUnchecked } = this.props;

View File

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

View File

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

View File

@ -7,7 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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 { cx, css } from '@emotion/css';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel'; import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
@ -19,6 +20,7 @@ const JSONClassName = css`
export class JsonReadPrettyFieldModel extends ReadPrettyFieldModel { export class JsonReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['json']; public static readonly supportedFieldInterfaces = ['json'];
@reactive
public render() { public render() {
const value = this.getValue(); const value = this.getValue();
const { space, style, className } = this.props; 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 React from 'react';
import * as math from 'mathjs'; import * as math from 'mathjs';
import { reactive } from '@nocobase/flow-engine';
import { isNum } from '@formily/shared'; import { isNum } from '@formily/shared';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel'; import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
@ -13,6 +23,7 @@ const toValue = (value: any, callback: (v: number) => number) => {
}; };
export class PercentReadPrettyFieldModel extends ReadPrettyFieldModel { export class PercentReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['percent']; public static readonly supportedFieldInterfaces = ['percent'];
@reactive
public render() { public render() {
const value = this.getValue(); const value = this.getValue();
const { prefix = '', suffix = '' } = this.props; const { prefix = '', suffix = '' } = this.props;

View File

@ -8,23 +8,46 @@
*/ */
import React from 'react'; import React from 'react';
import { tval } from '@nocobase/utils/client';
import { reactive } from '@nocobase/flow-engine';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel'; import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
import { MarkdownReadPretty } from '../EditableField/MarkdownEditableFieldModel/index';
const lineHeight142 = { lineHeight: '1.42' };
export class RichTextReadPrettyFieldModel extends ReadPrettyFieldModel { export class RichTextReadPrettyFieldModel extends ReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = ['richText']; public static readonly supportedFieldInterfaces = ['richText'];
@reactive
public render() { public render() {
const { textOnly = true } = this.props;
const value = this.getValue(); const value = this.getValue();
const html = ( return <MarkdownReadPretty textOnly={textOnly} value={value} />;
<div }
style={lineHeight142} }
dangerouslySetInnerHTML={{
__html: value,
}}
/>
);
return <div>{html}</div>; 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 './IconReadPrettyFieldModel';
export * from './JsonReadPrettyFieldModel'; export * from './JsonReadPrettyFieldModel';
export * from './AssociationFieldModel'; export * from './AssociationFieldModel';
export * from './MarkdownReadPrettyFieldModel';