mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
Merge branch '2.0' into refactor/field-model
This commit is contained in:
commit
cdb1a46fde
54
packages/core/client/src/flow/actions/confirm.tsx
Normal file
54
packages/core/client/src/flow/actions/confirm.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
12
packages/core/client/src/flow/actions/index.ts
Normal file
12
packages/core/client/src/flow/actions/index.ts
Normal 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';
|
||||
//
|
74
packages/core/client/src/flow/actions/popup.tsx
Normal file
74
packages/core/client/src/flow/actions/popup.tsx
Normal 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 />,
|
||||
});
|
||||
},
|
||||
});
|
@ -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;
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
@ -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();
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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';
|
@ -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={{
|
||||
|
@ -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)}
|
||||
|
@ -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 是流程中的可复用操作单元。
|
||||
|
@ -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';
|
||||
//
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user