Refactor/block model (#7137)

* refactor: block model flow

* fix: bug

* refactor: block title

* fix: bug

* refactor: code improve

* fix: bug
This commit is contained in:
Katherine 2025-07-01 01:25:01 +11:00 committed by GitHub
parent bb3d9a78ec
commit a06623ba9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 225 additions and 70 deletions

View File

@ -6,13 +6,16 @@
* 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 { tval } from '@nocobase/utils/client';
import { APIResource, BaseRecordResource, Collection, DefaultStructure, FlowModel } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
import { BlockItemCard } from '../common/BlockItemCard';
export class BlockModel<T = DefaultStructure> extends FlowModel<T> {
decoratorProps: Record<string, any> = {};
setDecoratorProps(props) {
this.decoratorProps = { ...this.decoratorProps, ...props };
}
renderComponent() {
throw new Error('renderComponent method must be implemented in subclasses of BlockModel');
@ -20,10 +23,92 @@ export class BlockModel<T = DefaultStructure> extends FlowModel<T> {
}
render() {
return <Card {...this.decoratorProps}>{this.renderComponent()}</Card>;
return <BlockItemCard {...this.decoratorProps}>{this.renderComponent()}</BlockItemCard>;
}
}
export const HeightMode = {
DEFAULT: 'defaultHeight',
SPECIFY_VALUE: 'specifyValue',
FULL_HEIGHT: 'fullHeight',
};
BlockModel.registerFlow({
key: 'blockProps',
title: tval('Basic configuration'),
auto: true,
steps: {
editBlockTitleAndDescription: {
title: tval('Edit block title & description'),
uiSchema: {
title: {
'x-component': 'Input',
'x-decorator': 'FormItem',
title: tval('Title'),
},
description: {
'x-component': 'Input.TextArea',
'x-decorator': 'FormItem',
title: tval('Description'),
},
},
handler(ctx, params) {
const title = ctx.globals.flowEngine.translate(params.title);
const description = ctx.globals.flowEngine.translate(params.description);
ctx.model.setDecoratorProps({ title: title, description: description });
},
},
setBlockHeight: {
title: tval('Set block height'),
uiSchema: {
heightMode: {
type: 'string',
enum: [
{ label: tval('Default'), value: HeightMode.DEFAULT },
{ label: tval('Specify height'), value: HeightMode.SPECIFY_VALUE },
{ label: tval('Full height'), value: HeightMode.FULL_HEIGHT },
],
required: true,
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
height: {
title: tval('Height'),
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'NumberPicker',
'x-component-props': {
addonAfter: 'px',
},
'x-validator': [
{
minimum: 40,
},
],
'x-reactions': {
dependencies: ['heightMode'],
fulfill: {
state: {
hidden: '{{ $deps[0]==="fullHeight"||$deps[0]==="defaultHeight"}}',
value: '{{$deps[0]!=="specifyValue"?null:$self.value}}',
},
},
},
},
},
defaultParams: () => {
return {
heightMode: HeightMode.DEFAULT,
};
},
handler(ctx, params) {
ctx.model.setProps('heightMode', params.heightMode);
ctx.model.setProps('height', params.height);
},
},
},
});
export class DataBlockModel<T = DefaultStructure> extends BlockModel<T> {
resource: APIResource;
collection: Collection;

View File

@ -8,20 +8,12 @@
*/
import { PlusOutlined } from '@ant-design/icons';
import { Input } from '@formily/antd-v5';
import { uid } from '@formily/shared';
import {
AddBlockButton,
FlowModel,
FlowModelRenderer,
FlowSettingsButton,
FlowsFloatContextMenu,
useStepSettingContext,
} from '@nocobase/flow-engine';
import { AddBlockButton, FlowModel, FlowModelRenderer, FlowSettingsButton } from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
import { Alert, Space } from 'antd';
import { Space } from 'antd';
import _ from 'lodash';
import React, { useState } from 'react';
import React from 'react';
import { Grid } from '../../components/Grid';
import JsonEditor from '../../components/JsonEditor';
import { SkeletonFallback } from '../../components/SkeletonFallback';

View File

@ -0,0 +1,47 @@
/**
* 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 { theme, Card } from 'antd';
import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant';
import { MarkdownReadPretty } from '../fields/EditableField/MarkdownEditableFieldModel';
export const BlockItemCard = (props) => {
const { t } = useTranslation();
const { token } = theme.useToken();
const { title: blockTitle, description, children } = props;
const title = (blockTitle || description) && (
<div style={{ padding: '8px 0px 8px' }}>
<span> {t(blockTitle, { ns: NAMESPACE_UI_SCHEMA })}</span>
{description && (
<MarkdownReadPretty
value={t(description, { ns: NAMESPACE_UI_SCHEMA })}
style={{
overflowWrap: 'break-word',
whiteSpace: 'normal',
fontWeight: 400,
color: token.colorTextDescription,
borderRadius: '4px',
}}
/>
)}
</div>
);
return (
<Card
title={title}
style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
bodyStyle={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
>
{children}
</Card>
);
};

View File

@ -12,7 +12,6 @@ import { createForm, Form } from '@formily/core';
import { FormProvider } from '@formily/react';
import { AddActionButton, AddFieldButton, FlowModelRenderer, SingleRecordResource } from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
import { Card } from 'antd';
import React from 'react';
import { DataBlockModel } from '../../base/BlockModel';
import { EditableFieldModel } from '../../fields/EditableField/EditableFieldModel';
@ -21,54 +20,51 @@ export class FormModel extends DataBlockModel {
form: Form;
declare resource: SingleRecordResource;
render() {
console.log(this);
renderComponent() {
return (
<Card>
<FormProvider form={this.form}>
<FormLayout layout={'vertical'}>
{this.mapSubModels('fields', (field) => (
<FlowModelRenderer
model={field}
showFlowSettings={{ showBorder: false }}
sharedContext={{ currentRecord: this.resource.getData() }}
/>
))}
</FormLayout>
<AddFieldButton
buildCreateModelOptions={({ defaultOptions, fieldPath }) => ({
use: defaultOptions.use,
stepParams: {
default: {
step1: {
dataSourceKey: this.collection.dataSourceKey,
collectionName: this.collection.name,
fieldPath,
},
<FormProvider form={this.form}>
<FormLayout layout={'vertical'}>
{this.mapSubModels('fields', (field) => (
<FlowModelRenderer
model={field}
showFlowSettings={{ showBorder: false }}
sharedContext={{ currentRecord: this.resource.getData() }}
/>
))}
</FormLayout>
<AddFieldButton
buildCreateModelOptions={({ defaultOptions, fieldPath }) => ({
use: defaultOptions.use,
stepParams: {
default: {
step1: {
dataSourceKey: this.collection.dataSourceKey,
collectionName: this.collection.name,
fieldPath,
},
},
})}
subModelKey="fields"
model={this}
collection={this.collection}
subModelBaseClass="EditableFieldModel"
onSubModelAdded={async (model: EditableFieldModel) => {
const params = model.getStepParams('default', 'step1');
this.addAppends(params?.fieldPath, !!this.ctx.shared?.currentFlow?.extra?.filterByTk);
}}
/>
<FormButtonGroup style={{ marginTop: 16 }}>
{this.mapSubModels('actions', (action) => (
<FlowModelRenderer
model={action}
showFlowSettings={{ showBackground: false, showBorder: false }}
sharedContext={{ currentRecord: this.resource.getData() }}
/>
))}
<AddActionButton model={this} subModelBaseClass="FormActionModel" />
</FormButtonGroup>
</FormProvider>
</Card>
},
})}
subModelKey="fields"
model={this}
collection={this.collection}
subModelBaseClass="EditableFieldModel"
onSubModelAdded={async (model: EditableFieldModel) => {
const params = model.getStepParams('default', 'step1');
this.addAppends(params?.fieldPath, !!this.ctx.shared?.currentFlow?.extra?.filterByTk);
}}
/>
<FormButtonGroup style={{ marginTop: 16 }}>
{this.mapSubModels('actions', (action) => (
<FlowModelRenderer
model={action}
showFlowSettings={{ showBackground: false, showBorder: false }}
sharedContext={{ currentRecord: this.resource.getData() }}
/>
))}
<AddActionButton model={this} subModelBaseClass="FormActionModel" />
</FormButtonGroup>
</FormProvider>
);
}
}

View File

@ -20,11 +20,11 @@ import {
useFlowEngine,
} from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
import { Card, Space, Spin, Table } from 'antd';
import { Space, Spin, Table } from 'antd';
import classNames from 'classnames';
import { t } from 'i18next';
import _ from 'lodash';
import React, { useRef } from 'react';
import { BlockItemCard } from '../../common/BlockItemCard';
import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel';
import { QuickEditForm } from '../form/QuickEditForm';

View File

@ -9,6 +9,7 @@
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Select } from 'antd';
import React from 'react';
import { castArray } from 'lodash';
import { useFlowModel, FlowModel } from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
import { AssociationFieldEditableFieldModel } from './AssociationFieldEditableFieldModel';
@ -82,7 +83,42 @@ const AssociationSelect = connect(
},
),
mapReadPretty((props) => {
return props.value;
const currentModel: any = useFlowModel();
const { fieldNames, value } = props;
if (!value) {
return;
}
const field = currentModel.subModels.field as FlowModel;
const key = value?.[fieldNames.value];
const fieldModel = field.createFork({}, key);
fieldModel.setSharedContext({
value: value?.[fieldNames.label],
currentRecord: value,
});
const arrayValue = castArray(value);
return (
<>
{arrayValue.map((v, index) => {
const key = `${index}`;
const fieldModel = field.createFork({}, key);
fieldModel.setSharedContext({
index,
value: v?.[fieldNames.label],
currentRecord: v,
});
const content = v?.[fieldNames.label] ? fieldModel.render() : tval('N/A');
return (
<React.Fragment key={index}>
{index > 0 && ', '}
{content}
</React.Fragment>
);
})}
</>
);
}),
);
@ -160,7 +196,7 @@ AssociationSelectEditableFieldModel.registerFlow({
steps: {
step1: {
async handler(ctx, params) {
const { target } = ctx.model.collectionField.options;
const { target } = ctx.model.collectionField;
const apiClient = ctx.app.apiClient;
const response = await apiClient.request({
url: `/${target}:list`,
@ -244,9 +280,7 @@ AssociationSelectEditableFieldModel.registerFlow({
async handler(ctx, params) {
try {
const collectionField = ctx.model.collectionField;
const collectionManager = collectionField.collection.collectionManager;
const targetCollection = collectionManager.getCollection(collectionField.options.target);
const targetCollection = ctx.model.collectionField.targetCollection;
const labelFieldName = ctx.model.field.componentProps.fieldNames.label;
const targetLabelField = targetCollection.getField(labelFieldName);

View File

@ -119,7 +119,7 @@ EditableFieldModel.registerFlow({
if (collectionField.enum.length) {
ctx.model.setDataSource(collectionField.enum);
}
const validator = collectionField.options.uiSchema?.['x-validator'];
const validator = collectionField.uiSchema?.['x-validator'];
if (validator) {
ctx.model.setValidator(validator);
}

View File

@ -23,6 +23,7 @@ export const MarkdownReadPretty = (props) => {
<div
className={` ${markdownClass} nb-markdown nb-markdown-default nb-markdown-table`}
dangerouslySetInnerHTML={{ __html: html }}
style={props.style}
/>
);

View File

@ -12,7 +12,7 @@ import { customAlphabet as Alphabet } from 'nanoid';
import { EditableFieldModel } from './EditableFieldModel';
export class NanoIDEditableFieldModel extends EditableFieldModel {
static supportedFieldInterfaces = ['nanoID'];
static supportedFieldInterfaces = ['nanoid'];
get component() {
return [Input, {}];
@ -22,7 +22,7 @@ export class NanoIDEditableFieldModel extends EditableFieldModel {
NanoIDEditableFieldModel.registerFlow({
key: 'initialValue',
auto: true,
sort: 5,
sort: 1000,
steps: {
initialValue: {
handler(ctx, params) {