Merge branch '2.0' into refactor/field-model

This commit is contained in:
katherinehhh 2025-06-21 21:54:12 +08:00
commit cdb1a46fde
18 changed files with 292 additions and 57 deletions

View File

@ -0,0 +1,54 @@
/**
* 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 { defineAction } from '@nocobase/flow-engine';
export const confirm = defineAction({
name: 'confirm',
title: '二次确认',
uiSchema: {
enable: {
type: 'boolean',
title: 'Enable secondary confirmation',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
title: {
type: 'string',
title: 'Title',
default: 'Delete record',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
content: {
type: 'string',
title: 'Content',
default: 'Are you sure you want to delete it?',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
},
defaultParams: {
enable: true,
title: 'Delete record',
content: 'Are you sure you want to delete it?',
},
async handler(ctx, params) {
if (params.enable) {
const confirmed = await ctx.globals.modal.confirm({
title: params.title,
content: params.content,
});
if (!confirmed) {
ctx.exit();
}
}
},
});

View File

@ -0,0 +1,12 @@
/**
* 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.
*/
export * from './confirm';
export * from './popup';
//

View File

@ -0,0 +1,74 @@
/**
* 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 { defineAction } from '@nocobase/flow-engine';
import React from 'react';
import { FlowPage } from '../FlowPage';
export const popup = defineAction({
name: 'popup',
title: '弹窗配置',
uiSchema: {
mode: {
type: 'string',
title: '打开方式',
enum: [
{ label: 'Drawer', value: 'drawer' },
{ label: 'Modal', value: 'modal' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
size: {
type: 'string',
title: '弹窗尺寸',
enum: [
{ label: '小', value: 'small' },
{ label: '中', value: 'medium' },
{ label: '大', value: 'large' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
},
defaultParams: {
mode: 'drawer',
size: 'medium',
},
handler(ctx, params) {
// eslint-disable-next-line prefer-const
let currentDrawer: any;
function DrawerContent() {
return (
<div>
<FlowPage
parentId={ctx.model.uid}
sharedContext={{
...params.sharedContext,
currentDrawer,
}}
/>
</div>
);
}
const sizeToWidthMap: Record<string, number> = {
small: 480,
medium: 800,
large: 1200,
};
currentDrawer = ctx.globals[params.mode || 'drawer'].open({
title: '命令式 Drawer',
width: sizeToWidthMap[params.size || 'medium'],
content: <DrawerContent />,
});
},
});

View File

@ -10,11 +10,12 @@
import { DataSource, DataSourceManager, FlowModel } from '@nocobase/flow-engine';
import _ from 'lodash';
import { Plugin } from '../application/Plugin';
import * as actions from './actions';
import { FlowEngineRunner } from './FlowEngineRunner';
import { MockFlowModelRepository } from './FlowModelRepository';
import { FlowRoute } from './FlowPage';
import * as models from './models';
import { DateTimeFormat } from './flowSetting/DateTimeFormat';
import * as models from './models';
export class PluginFlowEngine extends Plugin {
async load() {
@ -25,8 +26,9 @@ export class PluginFlowEngine extends Plugin {
([, ModelClass]) => typeof ModelClass === 'function' && ModelClass.prototype instanceof FlowModel,
),
);
console.log('Registering flow models:', Object.keys(filteredModels));
// console.log('Registering flow models:', Object.keys(filteredModels));
this.flowEngine.registerModels(filteredModels);
this.flowEngine.registerActions(actions);
const dataSourceManager = new DataSourceManager();
this.flowEngine.context['flowEngine'] = this.flowEngine;
this.flowEngine.context['app'] = this.app;

View File

@ -9,7 +9,6 @@
import { ButtonProps } from 'antd';
import { GlobalActionModel } from '../base/ActionModel';
import { openModeAction } from '../../actions/openModeAction';
export class AddNewActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
@ -27,6 +26,15 @@ AddNewActionModel.registerFlow({
eventName: 'click',
},
steps: {
open: openModeAction,
popup: {
use: 'popup',
defaultParams(ctx) {
return {
sharedContext: {
parentBlockModel: ctx.shared?.currentBlockModel,
},
};
},
},
},
});

View File

@ -9,9 +9,9 @@
import { MultiRecordResource } from '@nocobase/flow-engine';
import { ButtonProps } from 'antd';
import { GlobalActionModel } from '../base/ActionModel';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
import { GlobalActionModel } from '../base/ActionModel';
export class BulkDeleteActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
@ -27,7 +27,9 @@ BulkDeleteActionModel.registerFlow({
eventName: 'click',
},
steps: {
secondaryConfirmationAction,
confirm: {
use: 'confirm',
},
delete: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.resource) {
@ -43,6 +45,5 @@ BulkDeleteActionModel.registerFlow({
ctx.globals.message.success('Selected records deleted successfully.');
},
},
refreshOnCompleteAction,
},
});

View File

@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import type { ButtonProps } from 'antd/es/button';
import { RecordActionModel } from '../base/ActionModel';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
import { MultiRecordResource } from '@nocobase/flow-engine';
import type { ButtonProps } from 'antd/es/button';
import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
import { RecordActionModel } from '../base/ActionModel';
export class DeleteActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
@ -27,7 +27,9 @@ DeleteActionModel.registerFlow({
eventName: 'click',
},
steps: {
secondaryConfirmation: secondaryConfirmationAction,
confirm: {
use: 'confirm',
},
delete: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.resource) {
@ -43,6 +45,5 @@ DeleteActionModel.registerFlow({
ctx.globals.message.success('Record deleted successfully.');
},
},
refresh: refreshOnCompleteAction,
},
});

View File

@ -9,7 +9,6 @@
import { ButtonProps } from 'antd';
import { GlobalActionModel } from '../base/ActionModel';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
export class RefreshActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
@ -25,15 +24,14 @@ RefreshActionModel.registerFlow({
eventName: 'click',
},
steps: {
secondaryConfirmationAction,
refresh: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.resource) {
const currentResource = ctx.shared?.currentBlockModel?.resource;
if (!currentResource) {
ctx.globals.message.error('No resource selected for refresh.');
return;
}
const currentBlockModel = ctx.shared.currentBlockModel;
await currentBlockModel.resource.refresh();
await currentResource.refresh();
},
},
},

View File

@ -9,7 +9,6 @@
import type { ButtonProps } from 'antd/es/button';
import { RecordActionModel } from '../base/ActionModel';
import { openModeAction } from '../../actions/openModeAction';
export class ViewActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
@ -25,6 +24,16 @@ ViewActionModel.registerFlow({
eventName: 'click',
},
steps: {
open: openModeAction,
popup: {
use: 'popup',
defaultParams(ctx) {
return {
sharedContext: {
parentRecord: ctx.extra.currentRecord,
parentBlockModel: ctx.shared?.currentBlockModel,
},
};
},
},
},
});

View File

@ -7,12 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Collection, DefaultStructure, FlowModel, FlowResource } from '@nocobase/flow-engine';
import { APIResource, Collection, DefaultStructure, FlowModel } from '@nocobase/flow-engine';
export class BlockModel<T = DefaultStructure> extends FlowModel<T> {}
export class DataBlockModel<T = DefaultStructure> extends BlockModel<T> {
resource: FlowResource;
resource: APIResource;
collection: Collection;
}

View File

@ -9,6 +9,8 @@
import { ButtonProps } from 'antd';
import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel';
import { FormModel } from './FormModel';
export class FormActionModel extends ActionModel {}
@ -32,14 +34,15 @@ FormSubmitActionModel.registerFlow({
ctx.globals.message.error('No resource selected for submission.');
return;
}
const currentBlockModel = ctx.shared.currentBlockModel;
const currentResource = ctx.shared.currentBlockModel.resource;
const currentBlockModel = ctx.shared.currentBlockModel as FormModel;
await currentBlockModel.form.submit();
const values = currentBlockModel.form.values;
await currentBlockModel.resource.save(values);
await currentBlockModel.form.reset();
// currentResource.refresh();
ctx.shared.parentBlockModel?.resource?.refresh();
const parentBlockModel = ctx.shared.parentBlockModel as DataBlockModel;
if (parentBlockModel) {
parentBlockModel.resource.refresh();
}
if (ctx.shared.currentDrawer) {
ctx.shared.currentDrawer.destroy();
}

View File

@ -1,26 +0,0 @@
/**
* 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.
*/
export * from './SelectFieldModel';
export * from './InputNumberFieldModel';
export * from './PercentFieldModel';
export * from './PasswordFieldModel';
export * from './CheckboxFieldModel';
export * from './RadioGroupFieldModel';
export * from './TextareaFieldModel';
export * from './ColorFieldModel';
export * from './IconFieldModel';
export * from './RichTextFieldModel';
export * from './DateTimeFieldModel';
export * from './TimeFieldModel';
export * from './NanoIDFieldModel';
export * from './JsonFieldModel';
export * from './CheckboxGroupField';
export * from './MarkdownFieldModel';
export * from './AssociationSelectFieldModel';

View File

@ -131,6 +131,7 @@ export class TableModel extends DataBlockModel<S> {
},
selectedRowKeys: this.resource.getSelectedRows().map((row) => row.id),
}}
scroll={{ x: 'max-content' }}
dataSource={this.resource.getData()}
columns={this.getColumns()}
pagination={{

View File

@ -8,7 +8,76 @@
*/
import { Dropdown, DropdownProps, Input, Menu, Spin, Empty } from 'antd';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo, useRef } from 'react';
const useAutoPlacement = (visible: boolean) => {
const heightRef = useRef(0);
const [placement, setPlacement] = useState<'bottom' | 'top'>('bottom');
const [placementReady, setPlacementReady] = useState(false);
// 动态高度计算
useEffect(() => {
const handler = (e: MouseEvent) => {
const { clientY } = e;
const h = Math.max(clientY, window.innerHeight - clientY);
heightRef.current = h;
};
window.addEventListener('mousemove', handler);
return () => {
window.removeEventListener('mousemove', handler);
};
}, []);
// 智能位置计算
useEffect(() => {
if (!visible) {
setPlacementReady(false);
return;
}
const updatePlacement = (e: MouseEvent) => {
const { clientY } = e;
const availableSpaceBelow = window.innerHeight - clientY;
const availableSpaceAbove = clientY;
// 如果下方空间不足,且上方空间更大,则向上显示
if (availableSpaceBelow < 300 && availableSpaceAbove > availableSpaceBelow) {
setPlacement('top');
} else {
setPlacement('bottom');
}
setPlacementReady(true);
// 只计算一次
window.removeEventListener('mousemove', updatePlacement);
};
window.addEventListener('mousemove', updatePlacement);
// 兜底:如果没有鼠标移动事件,使用默认位置
const fallbackTimer = setTimeout(() => {
window.removeEventListener('mousemove', updatePlacement);
setPlacement('bottom');
setPlacementReady(true);
}, 50);
return () => {
window.removeEventListener('mousemove', updatePlacement);
clearTimeout(fallbackTimer);
};
}, [visible]);
const maxHeight = useMemo(() => heightRef.current - 40, [visible]);
return {
maxHeight,
placement,
placementReady,
};
};
// 菜单项类型定义
export type Item = {
@ -36,6 +105,8 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
const [rootLoading, setRootLoading] = useState(false);
const [searchValues, setSearchValues] = useState<Record<string, string>>({});
const { maxHeight: dropdownMaxHeight, placement, placementReady } = useAutoPlacement(menuVisible);
const getKeyPath = (path: string[], key: string) => [...path, key].join('/');
const handleLoadChildren = async (keyPath: string, loader: () => Item[] | Promise<Item[]>) => {
@ -251,6 +322,8 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
return (
<Dropdown
{...props}
placement={placement}
open={menuVisible && placementReady}
dropdownRender={() =>
rootLoading && rootItems.length === 0 ? (
<Menu
@ -261,9 +334,22 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
disabled: true,
},
]}
style={{
maxHeight: dropdownMaxHeight,
overflowY: 'auto',
}}
/>
) : (
<Menu {...menu} onClick={() => {}} items={resolveItems(rootItems)} />
<Menu
{...menu}
onClick={() => {}}
items={resolveItems(rootItems)}
style={{
maxHeight: dropdownMaxHeight,
overflowY: 'auto',
...menu?.style,
}}
/>
)
}
onOpenChange={(visible) => setMenuVisible(visible)}

View File

@ -58,6 +58,12 @@ export class FlowEngine {
return this._applyFlowCache;
}
registerActions(actions: Record<string, ActionDefinition>): void {
for (const [, definition] of Object.entries(actions)) {
this.registerAction(definition);
}
}
/**
* Action
* Action

View File

@ -7,7 +7,9 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
export * from './flowResource';
export * from './apiResource';
export * from './singleRecordResource';
export * from './baseRecordResource';
export * from './flowResource';
export * from './multiRecordResource';
export * from './singleRecordResource';
//

View File

@ -29,7 +29,9 @@ export class SingleRecordResource<TData = any> extends BaseRecordResource<TData>
...options,
data,
});
await this.refresh();
if (this.request.params.filterByTk) {
await this.refresh();
}
}
async destroy(): Promise<void> {

View File

@ -184,6 +184,8 @@ export type FlowExtraContext = Record<string, any>;
export interface ParamsContext<TModel extends FlowModel = FlowModel> {
model: TModel;
globals: Record<string, any>;
shared: Record<string, any>;
extra: Record<string, any>; // Extra context passed to applyFlow
app: any;
}