mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
fix: improve quickedit
This commit is contained in:
parent
0ca5272a41
commit
9af3b50f08
@ -23,27 +23,28 @@ import {
|
|||||||
import { Button, InputRef, Skeleton } from 'antd';
|
import { Button, InputRef, Skeleton } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
|
import { SkeletonFallback } from '../../../components/SkeletonFallback';
|
||||||
import { DataBlockModel } from '../../base/BlockModel';
|
import { DataBlockModel } from '../../base/BlockModel';
|
||||||
|
|
||||||
export class QuickEditForm extends DataBlockModel {
|
export class QuickEditForm extends DataBlockModel {
|
||||||
form: Form;
|
form: Form;
|
||||||
fieldPath: string;
|
fieldPath: string;
|
||||||
|
|
||||||
declare resource: SingleRecordResource;
|
declare resource: SingleRecordResource;
|
||||||
declare collection: Collection;
|
declare collection: Collection;
|
||||||
|
|
||||||
static async open(options: {
|
static async open(options: {
|
||||||
target: any;
|
target: any;
|
||||||
flowEngine: FlowEngine;
|
|
||||||
dataSourceKey: string;
|
dataSourceKey: string;
|
||||||
collectionName: string;
|
collectionName: string;
|
||||||
fieldPath: string;
|
fieldPath: string;
|
||||||
filterByTk: string;
|
filterByTk: string;
|
||||||
}) {
|
}) {
|
||||||
const { flowEngine, dataSourceKey, collectionName, fieldPath, target, filterByTk } = options;
|
const { target, dataSourceKey, collectionName, fieldPath, filterByTk } = options;
|
||||||
const model = flowEngine.createModel({
|
const model = this.flowEngine.createModel({
|
||||||
use: 'QuickEditForm',
|
use: 'QuickEditForm',
|
||||||
stepParams: {
|
stepParams: {
|
||||||
initial: {
|
propsFlow: {
|
||||||
step1: {
|
step1: {
|
||||||
dataSourceKey,
|
dataSourceKey,
|
||||||
collectionName,
|
collectionName,
|
||||||
@ -52,81 +53,75 @@ export class QuickEditForm extends DataBlockModel {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}) as QuickEditForm;
|
}) as QuickEditForm;
|
||||||
await model.open({ target, filterByTk });
|
|
||||||
options.flowEngine.removeModel(model.uid);
|
await this.flowEngine.context.popover.open({
|
||||||
|
target,
|
||||||
|
placement: 'rightTop',
|
||||||
|
content: (popover) => {
|
||||||
|
return (
|
||||||
|
<FlowModelRenderer
|
||||||
|
fallback={<Skeleton.Input size="small" />}
|
||||||
|
model={model}
|
||||||
|
sharedContext={{ currentView: popover }}
|
||||||
|
extraContext={{ filterByTk }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async open({ target, filterByTk }: { target: any; filterByTk: string }) {
|
render() {
|
||||||
await this.applyFlow('initial', { filterByTk });
|
return (
|
||||||
return new Promise((resolve, reject) => {
|
<form
|
||||||
const inputRef = createRef<InputRef>();
|
style={{ minWidth: '200px' }}
|
||||||
const popover = this.ctx.globals.popover.open({
|
className="quick-edit-form"
|
||||||
target,
|
onSubmit={async (e) => {
|
||||||
content: (
|
e.preventDefault();
|
||||||
<form
|
await this.form.submit();
|
||||||
style={{ minWidth: '200px' }}
|
await this.resource.save(
|
||||||
className="quick-edit-form"
|
{
|
||||||
onSubmit={async (e) => {
|
[this.fieldPath]: this.form.values[this.fieldPath],
|
||||||
e.preventDefault();
|
},
|
||||||
await this.form.submit();
|
{ refresh: false },
|
||||||
await this.resource.save(
|
);
|
||||||
{
|
this.ctx.shared.currentView.close();
|
||||||
[this.fieldPath]: this.form.values[this.fieldPath],
|
}}
|
||||||
},
|
>
|
||||||
{ refresh: false },
|
<FlowEngineProvider engine={this.flowEngine}>
|
||||||
);
|
<FormProvider form={this.form}>
|
||||||
popover.destroy();
|
<FormLayout layout={'vertical'}>
|
||||||
resolve(this.form.values);
|
{this.mapSubModels('fields', (field) => {
|
||||||
}}
|
return (
|
||||||
>
|
<FlowModelRenderer
|
||||||
<FlowEngineProvider engine={this.flowEngine}>
|
model={field}
|
||||||
<FormProvider form={this.form}>
|
sharedContext={{ currentRecord: this.resource.getData() }}
|
||||||
<FormLayout layout={'vertical'}>
|
fallback={<Skeleton paragraph={{ rows: 2 }} />}
|
||||||
{this.mapSubModels('fields', (field) => {
|
/>
|
||||||
return (
|
);
|
||||||
<FlowModelRenderer
|
})}
|
||||||
model={field}
|
</FormLayout>
|
||||||
sharedContext={{ currentRecord: this.resource.getData() }}
|
<FormButtonGroup align="right">
|
||||||
fallback={<Skeleton paragraph={{ rows: 2 }} />}
|
<Button
|
||||||
/>
|
onClick={() => {
|
||||||
);
|
this.ctx.shared.currentView.close();
|
||||||
})}
|
}}
|
||||||
</FormLayout>
|
>
|
||||||
<FormButtonGroup align="right">
|
{this.translate('Cancel')}
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => {
|
<Button type="primary" htmlType="submit">
|
||||||
popover.destroy();
|
{this.translate('Submit')}
|
||||||
reject(null); // 在 close 之后 resolve
|
</Button>
|
||||||
}}
|
</FormButtonGroup>
|
||||||
>
|
</FormProvider>
|
||||||
{this.translate('Cancel')}
|
</FlowEngineProvider>
|
||||||
</Button>
|
</form>
|
||||||
<Button type="primary" htmlType="submit">
|
);
|
||||||
{this.translate('Submit')}
|
|
||||||
</Button>
|
|
||||||
</FormButtonGroup>
|
|
||||||
</FormProvider>
|
|
||||||
</FlowEngineProvider>
|
|
||||||
</form>
|
|
||||||
),
|
|
||||||
onRendered: () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
// 聚焦 Popover 内第一个 input 或 textarea
|
|
||||||
const el = document.querySelector(
|
|
||||||
'.quick-edit-form input, .quick-edit-form textarea, .quick-edit-form select',
|
|
||||||
) as HTMLInputElement | HTMLTextAreaElement | null;
|
|
||||||
el?.focus();
|
|
||||||
}, 200);
|
|
||||||
// inputRef.current.focus();
|
|
||||||
},
|
|
||||||
placement: 'rightTop',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QuickEditForm.registerFlow({
|
QuickEditForm.registerFlow({
|
||||||
key: 'initial',
|
key: 'propsFlow',
|
||||||
|
auto: true,
|
||||||
steps: {
|
steps: {
|
||||||
step1: {
|
step1: {
|
||||||
async handler(ctx, params) {
|
async handler(ctx, params) {
|
||||||
|
@ -164,7 +164,6 @@ export class TableModel extends DataBlockModel<TableModelStructure> {
|
|||||||
try {
|
try {
|
||||||
await QuickEditForm.open({
|
await QuickEditForm.open({
|
||||||
target: ref.current,
|
target: ref.current,
|
||||||
flowEngine: this.flowEngine,
|
|
||||||
dataSourceKey: this.collection.dataSourceKey,
|
dataSourceKey: this.collection.dataSourceKey,
|
||||||
collectionName: this.collection.name,
|
collectionName: this.collection.name,
|
||||||
fieldPath: dataIndex,
|
fieldPath: dataIndex,
|
||||||
|
@ -226,6 +226,7 @@ export class FlowEngine {
|
|||||||
if (this.modelClasses.has(name)) {
|
if (this.modelClasses.has(name)) {
|
||||||
console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`);
|
console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`);
|
||||||
}
|
}
|
||||||
|
(modelClass as typeof FlowModel).flowEngine = this; // 绑定 FlowEngine 实例到 Model 类
|
||||||
this.modelClasses.set(name, modelClass);
|
this.modelClasses.set(name, modelClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,7 +317,7 @@ export class FlowEngine {
|
|||||||
if (uid && this.modelInstances.has(uid)) {
|
if (uid && this.modelInstances.has(uid)) {
|
||||||
return this.modelInstances.get(uid) as T;
|
return this.modelInstances.get(uid) as T;
|
||||||
}
|
}
|
||||||
const modelInstance = new (ModelClass as ModelConstructor<T>)({ ...options, flowEngine: this } as any);
|
const modelInstance = new (ModelClass as ModelConstructor<T>)({ ...options } as any);
|
||||||
|
|
||||||
modelInstance.onInit(options);
|
modelInstance.onInit(options);
|
||||||
|
|
||||||
|
@ -39,11 +39,13 @@ const modelMetas = new WeakMap<typeof FlowModel, FlowModelMeta>();
|
|||||||
const modelFlows = new WeakMap<typeof FlowModel, Map<string, FlowDefinition>>();
|
const modelFlows = new WeakMap<typeof FlowModel, Map<string, FlowDefinition>>();
|
||||||
|
|
||||||
export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
||||||
|
public static flowEngine: FlowEngine;
|
||||||
|
|
||||||
public readonly uid: string;
|
public readonly uid: string;
|
||||||
public sortIndex: number;
|
public sortIndex: number;
|
||||||
public props: IModelComponentProps = {};
|
public props: IModelComponentProps = {};
|
||||||
public stepParams: StepParams = {};
|
public stepParams: StepParams = {};
|
||||||
public flowEngine: FlowEngine;
|
// public flowEngine: FlowEngine;
|
||||||
public parent: ParentFlowModel<Structure>;
|
public parent: ParentFlowModel<Structure>;
|
||||||
public subModels: Structure['subModels'];
|
public subModels: Structure['subModels'];
|
||||||
private _options: FlowModelOptions<Structure>;
|
private _options: FlowModelOptions<Structure>;
|
||||||
@ -72,9 +74,12 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|||||||
private observerDispose: () => void;
|
private observerDispose: () => void;
|
||||||
|
|
||||||
constructor(options: FlowModelOptions<Structure>) {
|
constructor(options: FlowModelOptions<Structure>) {
|
||||||
if (options?.flowEngine?.getModel(options.uid)) {
|
if (!this.flowEngine) {
|
||||||
|
throw new Error('FlowModel must be initialized with a FlowEngine instance.');
|
||||||
|
}
|
||||||
|
if (this.flowEngine.getModel(options.uid)) {
|
||||||
// 此时 new FlowModel 并不创建新实例,而是返回已存在的实例,避免重复创建同一个model实例
|
// 此时 new FlowModel 并不创建新实例,而是返回已存在的实例,避免重复创建同一个model实例
|
||||||
return options.flowEngine.getModel(options.uid);
|
return this.flowEngine.getModel(options.uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.uid) {
|
if (!options.uid) {
|
||||||
@ -85,7 +90,6 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|||||||
this.props = options.props || {};
|
this.props = options.props || {};
|
||||||
this.stepParams = options.stepParams || {};
|
this.stepParams = options.stepParams || {};
|
||||||
this.subModels = {};
|
this.subModels = {};
|
||||||
this.flowEngine = options.flowEngine;
|
|
||||||
this.sortIndex = options.sortIndex || 0;
|
this.sortIndex = options.sortIndex || 0;
|
||||||
this._options = options;
|
this._options = options;
|
||||||
|
|
||||||
@ -132,6 +136,11 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|||||||
return modelMetas.get(this);
|
return modelMetas.get(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get flowEngine() {
|
||||||
|
// 取静态属性 flowEngine
|
||||||
|
return (this.constructor as typeof FlowModel).flowEngine;
|
||||||
|
}
|
||||||
|
|
||||||
private createSubModels(subModels: Record<string, CreateSubModelOptions | CreateSubModelOptions[]>) {
|
private createSubModels(subModels: Record<string, CreateSubModelOptions | CreateSubModelOptions[]>) {
|
||||||
Object.entries(subModels || {}).forEach(([key, value]) => {
|
Object.entries(subModels || {}).forEach(([key, value]) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
@ -162,7 +171,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|||||||
* @param {FlowEngine} flowEngine FlowEngine实例
|
* @param {FlowEngine} flowEngine FlowEngine实例
|
||||||
*/
|
*/
|
||||||
setFlowEngine(flowEngine: FlowEngine): void {
|
setFlowEngine(flowEngine: FlowEngine): void {
|
||||||
this.flowEngine = flowEngine;
|
// this.flowEngine = flowEngine;
|
||||||
}
|
}
|
||||||
|
|
||||||
static define(meta: FlowModelMeta) {
|
static define(meta: FlowModelMeta) {
|
||||||
|
@ -40,8 +40,6 @@ export function useDrawer() {
|
|||||||
// 支持 content 为函数,传递 currentDrawer
|
// 支持 content 为函数,传递 currentDrawer
|
||||||
const content = typeof config.content === 'function' ? config.content(currentDrawer) : config.content;
|
const content = typeof config.content === 'function' ? config.content(currentDrawer) : config.content;
|
||||||
|
|
||||||
console.log('useDrawer open', config, content);
|
|
||||||
|
|
||||||
const drawer = (
|
const drawer = (
|
||||||
<DrawerComponent
|
<DrawerComponent
|
||||||
key={`drawer-${uuid}`}
|
key={`drawer-${uuid}`}
|
||||||
|
@ -13,57 +13,101 @@ import usePatchElement from './usePatchElement';
|
|||||||
|
|
||||||
let uuid = 0;
|
let uuid = 0;
|
||||||
|
|
||||||
|
// 独立 PopoverComponent,参考 DrawerComponent 实现
|
||||||
|
const PopoverComponent = React.forwardRef<any, any>(({ afterClose, content, placement, rect, ...props }, ref) => {
|
||||||
|
const [visible, setVisible] = React.useState(true);
|
||||||
|
const [config, setConfig] = React.useState({ content, placement, rect, ...props });
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
destroy: () => setVisible(false),
|
||||||
|
update: (newConfig: any) => setConfig((prev) => ({ ...prev, ...newConfig })),
|
||||||
|
close: (result?: any) => setVisible(false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 关闭后触发 afterClose
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
afterClose?.();
|
||||||
|
}
|
||||||
|
}, [visible, afterClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
arrow={false}
|
||||||
|
open={visible}
|
||||||
|
trigger={['click']}
|
||||||
|
destroyTooltipOnHide
|
||||||
|
content={config.content}
|
||||||
|
placement={config.placement}
|
||||||
|
getPopupContainer={() => document.body}
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
setVisible(nextOpen);
|
||||||
|
if (!nextOpen) {
|
||||||
|
afterClose?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...config}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: (config.rect?.top ?? 0) + window.scrollY,
|
||||||
|
left: (config.rect?.left ?? 0) + window.scrollX,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export function usePopover() {
|
export function usePopover() {
|
||||||
const holderRef = React.useRef(null);
|
const holderRef = React.useRef<any>(null);
|
||||||
|
|
||||||
const open = (config) => {
|
const open = (config) => {
|
||||||
uuid += 1;
|
uuid += 1;
|
||||||
const { target, placement = 'rightTop', content, onRendered, ...rest } = config;
|
const { target, placement = 'rightTop', content, ...rest } = config;
|
||||||
const popoverRef = React.createRef<{ destroy: () => void; update: (config: any) => void }>();
|
const popoverRef = React.createRef<any>();
|
||||||
|
|
||||||
// 计算目标位置
|
// 计算目标位置
|
||||||
const rect = target?.getBoundingClientRect?.() ?? { top: 0, left: 0 };
|
const rect = target?.getBoundingClientRect?.() ?? { top: 0, left: 0 };
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
let closeFunc: (() => void) | undefined;
|
let closeFunc: (() => void) | undefined;
|
||||||
const PopoverComponent = (props) => {
|
let resolvePromise: (value?: any) => void;
|
||||||
const [open, setOpen] = React.useState(true);
|
const promise = new Promise((resolve) => {
|
||||||
React.useEffect(() => {
|
resolvePromise = resolve;
|
||||||
onRendered?.();
|
});
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
arrow={false}
|
|
||||||
open={open}
|
|
||||||
trigger={['click']}
|
|
||||||
destroyTooltipOnHide
|
|
||||||
onOpenChange={(open) => setOpen(open)}
|
|
||||||
content={content}
|
|
||||||
placement={placement}
|
|
||||||
getPopupContainer={() => document.body}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: rect.top + window.scrollY,
|
|
||||||
left: rect.left + window.scrollX,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const popover = <PopoverComponent key={`popover-${uuid}`} ref={popoverRef} />;
|
// 构造 currentPopover 实例
|
||||||
// eslint-disable-next-line prefer-const
|
const currentPopover = {
|
||||||
|
destroy: () => popoverRef.current?.destroy(),
|
||||||
|
update: (newConfig) => popoverRef.current?.update(newConfig),
|
||||||
|
close: (result?: any) => {
|
||||||
|
resolvePromise?.(result);
|
||||||
|
popoverRef.current?.close(result);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const children = typeof content === 'function' ? content(currentPopover) : content;
|
||||||
|
|
||||||
|
const popover = (
|
||||||
|
<PopoverComponent
|
||||||
|
key={`popover-${uuid}`}
|
||||||
|
ref={popoverRef}
|
||||||
|
content={children}
|
||||||
|
placement={placement}
|
||||||
|
rect={rect}
|
||||||
|
afterClose={() => {
|
||||||
|
closeFunc?.();
|
||||||
|
config.onClose?.();
|
||||||
|
resolvePromise?.(config.result);
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
closeFunc = holderRef.current?.patchElement(popover);
|
closeFunc = holderRef.current?.patchElement(popover);
|
||||||
|
|
||||||
return {
|
return Object.assign(promise, currentPopover);
|
||||||
destroy: () => closeFunc?.(),
|
|
||||||
// update: (newConfig) => ... // 可选:实现更新逻辑
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const api = React.useMemo(() => ({ open }), []);
|
const api = React.useMemo(() => ({ open }), []);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user