@nocobase/flow-engine 2.2.0-alpha.1 → 2.2.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/JSRunner.d.ts +1 -0
- package/lib/JSRunner.js +110 -20
- package/lib/components/FieldModelRenderer.js +44 -31
- package/lib/components/FlowContextSelector.js +24 -3
- package/lib/components/dnd/index.js +9 -2
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +86 -32
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +20 -0
- package/lib/components/variables/VariableHybridInput.d.ts +8 -0
- package/lib/components/variables/VariableHybridInput.js +128 -12
- package/lib/components/variables/types.d.ts +8 -0
- package/lib/flowContext.d.ts +1 -1
- package/lib/flowContext.js +87 -36
- package/lib/flowI18n.js +3 -3
- package/lib/flowSettings.d.ts +5 -1
- package/lib/flowSettings.js +70 -0
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/resources/apiResource.js +2 -1
- package/lib/resources/baseRecordResource.js +6 -17
- package/lib/resources/multiRecordResource.js +13 -3
- package/lib/resources/singleRecordResource.js +7 -2
- package/lib/runjs-context/helpers.js +12 -5
- package/lib/types.d.ts +12 -0
- package/lib/utils/dataSourceDirty.d.ts +20 -0
- package/lib/utils/dataSourceDirty.js +139 -0
- package/lib/utils/dirtyAwareApiClient.d.ts +11 -0
- package/lib/utils/dirtyAwareApiClient.js +378 -0
- package/lib/utils/index.d.ts +1 -1
- package/lib/utils/index.js +11 -11
- package/lib/utils/openViewRouteState.d.ts +28 -0
- package/lib/utils/openViewRouteState.js +125 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +3 -0
- package/lib/utils/parsePathnameToViewParams.js +18 -1
- package/lib/utils/resolveRunJSObjectValues.js +3 -2
- package/lib/utils/runjsModuleLoader.js +0 -30
- package/lib/views/ViewNavigation.js +5 -0
- package/package.json +4 -4
- package/src/JSRunner.ts +112 -25
- package/src/__tests__/JSRunner.test.ts +4 -5
- package/src/__tests__/flowContext.test.ts +131 -0
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +51 -0
- package/src/__tests__/flowI18n.test.ts +11 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowSettings.test.ts +72 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +15 -2
- package/src/components/FieldModelRenderer.tsx +50 -36
- package/src/components/FlowContextSelector.tsx +33 -2
- package/src/components/__tests__/FieldModelRenderer.test.tsx +165 -0
- package/src/components/dnd/index.tsx +11 -2
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +105 -35
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +381 -12
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +28 -0
- package/src/components/variables/VariableHybridInput.tsx +166 -9
- package/src/components/variables/__tests__/VariableHybridInput.test.tsx +178 -0
- package/src/components/variables/types.ts +8 -0
- package/src/flowContext.ts +100 -33
- package/src/flowI18n.ts +8 -3
- package/src/flowSettings.ts +85 -1
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/resources/apiResource.ts +2 -1
- package/src/resources/baseRecordResource.ts +6 -23
- package/src/resources/multiRecordResource.ts +13 -3
- package/src/resources/singleRecordResource.ts +6 -1
- package/src/runjs-context/helpers.ts +12 -6
- package/src/types.ts +14 -0
- package/src/utils/__tests__/dirtyAwareApiClient.test.ts +392 -0
- package/src/utils/__tests__/openViewRouteState.test.ts +40 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +36 -0
- package/src/utils/dataSourceDirty.ts +126 -0
- package/src/utils/dirtyAwareApiClient.ts +430 -0
- package/src/utils/index.ts +10 -9
- package/src/utils/openViewRouteState.ts +107 -0
- package/src/utils/parsePathnameToViewParams.ts +23 -1
- package/src/utils/resolveRunJSObjectValues.ts +5 -2
- package/src/utils/runjsModuleLoader.ts +0 -32
- package/src/views/ViewNavigation.ts +6 -1
- package/src/views/__tests__/ViewNavigation.test.ts +15 -0
- package/lib/utils/safeGlobals.d.ts +0 -28
- package/lib/utils/safeGlobals.js +0 -367
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +0 -38
- package/src/utils/__tests__/safeGlobals.test.ts +0 -106
- package/src/utils/safeGlobals.ts +0 -406
package/src/flowContext.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { ISchema } from '@formily/json-schema';
|
|
11
11
|
import { observable } from '@formily/reactive';
|
|
12
|
-
import { APIClient, RequestOptions } from '@nocobase/sdk';
|
|
12
|
+
import type { APIClient, RequestOptions } from '@nocobase/sdk';
|
|
13
13
|
import type { Router } from '@remix-run/router';
|
|
14
14
|
import axios from 'axios';
|
|
15
15
|
import { MessageInstance } from 'antd/es/message/interface';
|
|
@@ -39,9 +39,11 @@ import {
|
|
|
39
39
|
extractUsedVariablePaths,
|
|
40
40
|
FlowExitException,
|
|
41
41
|
FLOW_ENGINE_NAMESPACE,
|
|
42
|
+
createOpenViewRouteState,
|
|
42
43
|
isCtxDatePathPrefix,
|
|
43
44
|
isCssFile,
|
|
44
45
|
prepareRunJsCode,
|
|
46
|
+
RUNJS_OPEN_VIEW_ROUTE_STATE,
|
|
45
47
|
resolveCtxDatePath,
|
|
46
48
|
resolveDefaultParams,
|
|
47
49
|
resolveExpressions,
|
|
@@ -51,6 +53,7 @@ import { FlowExitAllException } from './utils/exceptions';
|
|
|
51
53
|
import { enqueueVariablesResolve, JSONValue } from './utils/params-resolvers';
|
|
52
54
|
import type { RecordRef } from './utils/serverContextParams';
|
|
53
55
|
import { buildServerContextParams as _buildServerContextParams } from './utils/serverContextParams';
|
|
56
|
+
import { getDirtyAwareApiClient } from './utils/dirtyAwareApiClient';
|
|
54
57
|
import { inferRecordRef } from './utils/variablesParams';
|
|
55
58
|
import { FlowView, FlowViewer } from './views/FlowView';
|
|
56
59
|
import { RunJSContextRegistry, getModelClassName, type RunJSVersion } from './runjs-context/registry';
|
|
@@ -2909,19 +2912,20 @@ export class FlowContext {
|
|
|
2909
2912
|
|
|
2910
2913
|
// 静态值
|
|
2911
2914
|
if ('value' in options) {
|
|
2912
|
-
return options.value;
|
|
2915
|
+
return key === 'api' ? getDirtyAwareApiClient(options.value, currentContext) : options.value;
|
|
2913
2916
|
}
|
|
2914
2917
|
|
|
2915
2918
|
// get 方法
|
|
2916
2919
|
if (options.get) {
|
|
2917
2920
|
if (options.cache === false) {
|
|
2918
|
-
|
|
2921
|
+
const value = options.get(currentContext);
|
|
2922
|
+
return key === 'api' ? getDirtyAwareApiClient(value, currentContext) : value;
|
|
2919
2923
|
}
|
|
2920
2924
|
|
|
2921
2925
|
const cacheKey = options.observable ? '_observableCache' : '_cache';
|
|
2922
2926
|
|
|
2923
2927
|
if (key in this[cacheKey]) {
|
|
2924
|
-
return this[cacheKey][key];
|
|
2928
|
+
return key === 'api' ? getDirtyAwareApiClient(this[cacheKey][key], currentContext) : this[cacheKey][key];
|
|
2925
2929
|
}
|
|
2926
2930
|
|
|
2927
2931
|
if (this._pending[key]) return this._pending[key];
|
|
@@ -2939,7 +2943,7 @@ export class FlowContext {
|
|
|
2939
2943
|
(v) => {
|
|
2940
2944
|
this[cacheKey][key] = v;
|
|
2941
2945
|
delete this._pending[key];
|
|
2942
|
-
return v;
|
|
2946
|
+
return key === 'api' ? getDirtyAwareApiClient(v, currentContext) : v;
|
|
2943
2947
|
},
|
|
2944
2948
|
(err) => {
|
|
2945
2949
|
delete this._pending[key];
|
|
@@ -2951,7 +2955,7 @@ export class FlowContext {
|
|
|
2951
2955
|
|
|
2952
2956
|
// sync 直接缓存
|
|
2953
2957
|
this[cacheKey][key] = result;
|
|
2954
|
-
return result;
|
|
2958
|
+
return key === 'api' ? getDirtyAwareApiClient(result, currentContext) : result;
|
|
2955
2959
|
}
|
|
2956
2960
|
|
|
2957
2961
|
return undefined;
|
|
@@ -3074,7 +3078,7 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
3074
3078
|
this.defineMethod('getModel', (modelName: string, searchInPreviousEngines?: boolean) => {
|
|
3075
3079
|
return this.engine.getModel(modelName, searchInPreviousEngines);
|
|
3076
3080
|
});
|
|
3077
|
-
this.defineMethod('request', (options: RequestOptions)
|
|
3081
|
+
this.defineMethod('request', function (this: FlowContext, options: RequestOptions) {
|
|
3078
3082
|
const app = this.app as { getApiUrl?: (pathname?: string) => string } | undefined;
|
|
3079
3083
|
if (typeof options?.url === 'string' && shouldBypassApiClient(options.url, app)) {
|
|
3080
3084
|
return axios.request(options);
|
|
@@ -3139,6 +3143,37 @@ class BaseFlowModelContext extends BaseFlowEngineContext {
|
|
|
3139
3143
|
declare makeResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
|
|
3140
3144
|
}
|
|
3141
3145
|
|
|
3146
|
+
const OPEN_VIEW_INHERITED_INPUT_ARG_KEYS = [
|
|
3147
|
+
'dataSourceKey',
|
|
3148
|
+
'collectionName',
|
|
3149
|
+
'associationName',
|
|
3150
|
+
'filterByTk',
|
|
3151
|
+
'sourceId',
|
|
3152
|
+
'tabUid',
|
|
3153
|
+
];
|
|
3154
|
+
|
|
3155
|
+
function pickDefinedKeys(source: Record<string, unknown> | null | undefined, keys: string[]) {
|
|
3156
|
+
const res: Record<string, unknown> = {};
|
|
3157
|
+
for (const key of keys) {
|
|
3158
|
+
if (typeof source?.[key] !== 'undefined') {
|
|
3159
|
+
res[key] = source[key];
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
return res;
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
function pickDefinedOpenViewInputArgs(source?: Record<string, unknown> | null) {
|
|
3166
|
+
return pickDefinedKeys(source, OPEN_VIEW_INHERITED_INPUT_ARG_KEYS);
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
function applyDefinedDefaults(target: Record<string, unknown>, defaults: Record<string, unknown>) {
|
|
3170
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
3171
|
+
if (typeof target[key] === 'undefined') {
|
|
3172
|
+
target[key] = value;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3142
3177
|
export class FlowEngineContext extends BaseFlowEngineContext {
|
|
3143
3178
|
// public dataSourceManager: DataSourceManager;
|
|
3144
3179
|
constructor(public engine: FlowEngine) {
|
|
@@ -3398,7 +3433,19 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3398
3433
|
}),
|
|
3399
3434
|
});
|
|
3400
3435
|
this.defineProperty('role', {
|
|
3401
|
-
get: () =>
|
|
3436
|
+
get: () => {
|
|
3437
|
+
const currentRole = this.api?.auth?.role;
|
|
3438
|
+
if (currentRole !== '__union__') {
|
|
3439
|
+
return currentRole;
|
|
3440
|
+
}
|
|
3441
|
+
const roles = this.user?.roles;
|
|
3442
|
+
if (!Array.isArray(roles)) {
|
|
3443
|
+
return [];
|
|
3444
|
+
}
|
|
3445
|
+
return roles
|
|
3446
|
+
.map((role: { name?: string }) => role?.name)
|
|
3447
|
+
.filter((name: string | undefined): name is string => !!name);
|
|
3448
|
+
},
|
|
3402
3449
|
cache: false,
|
|
3403
3450
|
// 注意:使用惰性 meta 工厂,避免在 i18n 尚未注入时提前求值导致无法翻译
|
|
3404
3451
|
meta: Object.assign(() => ({ type: 'string', title: this.t('Current role'), sort: 990 }), {
|
|
@@ -3561,7 +3608,17 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3561
3608
|
doc = {};
|
|
3562
3609
|
}
|
|
3563
3610
|
const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
|
|
3564
|
-
const
|
|
3611
|
+
const browserGlobals: Record<string, any> = {};
|
|
3612
|
+
if (typeof window !== 'undefined') {
|
|
3613
|
+
browserGlobals.window = window;
|
|
3614
|
+
if (typeof navigator !== 'undefined') {
|
|
3615
|
+
browserGlobals.navigator = navigator;
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
if (typeof document !== 'undefined') {
|
|
3619
|
+
browserGlobals.document = document;
|
|
3620
|
+
}
|
|
3621
|
+
const globals: Record<string, any> = { ctx: deprecatedCtx, ...browserGlobals, ...(options?.globals || {}) };
|
|
3565
3622
|
const { timeoutMs } = options || {};
|
|
3566
3623
|
return new JSRunner({ globals, timeoutMs });
|
|
3567
3624
|
});
|
|
@@ -3687,7 +3744,14 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
3687
3744
|
},
|
|
3688
3745
|
});
|
|
3689
3746
|
this.defineMethod('openView', async function (uid: string, options) {
|
|
3690
|
-
const
|
|
3747
|
+
const inheritedInputArgs = {
|
|
3748
|
+
...(typeof this.model?.['getInputArgs'] === 'function'
|
|
3749
|
+
? pickDefinedOpenViewInputArgs(this.model['getInputArgs']())
|
|
3750
|
+
: {}),
|
|
3751
|
+
...pickDefinedOpenViewInputArgs(this.inputArgs),
|
|
3752
|
+
};
|
|
3753
|
+
const opts = { ...(options || {}) };
|
|
3754
|
+
applyDefinedDefaults(opts, inheritedInputArgs);
|
|
3691
3755
|
// NOTE: when custom context is passed, route navigation must be disabled to avoid losing it after refresh.
|
|
3692
3756
|
if (opts.defineProperties || opts.defineMethods) {
|
|
3693
3757
|
opts.navigation = false; // 强制不使用路由导航, 避免刷新页面时丢失上下文
|
|
@@ -3695,15 +3759,6 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
3695
3759
|
let model: FlowModel | null = null;
|
|
3696
3760
|
model = await this.engine.loadModel({ uid });
|
|
3697
3761
|
if (!model) {
|
|
3698
|
-
const pickDefined = (src: Record<string, any>, keys: string[]) => {
|
|
3699
|
-
const res: Record<string, any> = {};
|
|
3700
|
-
for (const k of keys) {
|
|
3701
|
-
if (typeof src?.[k] !== 'undefined') {
|
|
3702
|
-
res[k] = src[k];
|
|
3703
|
-
}
|
|
3704
|
-
}
|
|
3705
|
-
return res;
|
|
3706
|
-
};
|
|
3707
3762
|
model = this.engine.createModel({
|
|
3708
3763
|
uid, // 注意: 新建的 model 应该使用 ${parentModel.uid}-xxx 形式的 uid
|
|
3709
3764
|
use: 'PopupActionModel',
|
|
@@ -3714,7 +3769,7 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
3714
3769
|
popupSettings: {
|
|
3715
3770
|
openView: {
|
|
3716
3771
|
// 仅在创建时持久化一份默认配置;运行时以本次 opts 为准,避免多个 opener 互相覆盖。
|
|
3717
|
-
...
|
|
3772
|
+
...pickDefinedKeys(opts, ['dataSourceKey', 'collectionName', 'associationName', 'mode', 'size']),
|
|
3718
3773
|
},
|
|
3719
3774
|
},
|
|
3720
3775
|
},
|
|
@@ -3734,8 +3789,6 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
3734
3789
|
// 统一语义:为即将打开的外部视图定义一个 PendingView(占位视图)
|
|
3735
3790
|
const pendingType = (opts?.isMobileLayout ? 'embed' : opts?.mode || 'drawer') as any;
|
|
3736
3791
|
const pendingInputArgs = { ...opts, viewUid, navigation: opts.navigation };
|
|
3737
|
-
pendingInputArgs.filterByTk = pendingInputArgs.filterByTk || this.inputArgs?.filterByTk;
|
|
3738
|
-
pendingInputArgs.sourceId = pendingInputArgs.sourceId || this.inputArgs?.sourceId;
|
|
3739
3792
|
|
|
3740
3793
|
const pendingView = {
|
|
3741
3794
|
type: pendingType,
|
|
@@ -3754,17 +3807,10 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
3754
3807
|
} else if (on && typeof on === 'object' && typeof (on as any).eventName === 'string' && (on as any).eventName) {
|
|
3755
3808
|
openEventName = (on as any).eventName;
|
|
3756
3809
|
}
|
|
3757
|
-
await model.dispatchEvent(
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
// ...this.model?.['getInputArgs']?.(), // 避免部分关系字段信息丢失, 仿照 ClickableCollectionField 做法
|
|
3762
|
-
...opts,
|
|
3763
|
-
},
|
|
3764
|
-
{
|
|
3765
|
-
debounce: true,
|
|
3766
|
-
},
|
|
3767
|
-
);
|
|
3810
|
+
await model.dispatchEvent(openEventName, {
|
|
3811
|
+
// navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
|
|
3812
|
+
...opts,
|
|
3813
|
+
});
|
|
3768
3814
|
});
|
|
3769
3815
|
this.defineMethod('getEvents', function (this: BaseFlowModelContext) {
|
|
3770
3816
|
return this.model.getEvents();
|
|
@@ -4558,6 +4604,27 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
4558
4604
|
this.defineProperty('ReactDOM', { value: ReactDOMShim });
|
|
4559
4605
|
|
|
4560
4606
|
setupRunJSLibs(this);
|
|
4607
|
+
this.defineMethod('openView', async function (uid: string, options?: Record<PropertyKey, unknown>) {
|
|
4608
|
+
const delegateOpenView = (
|
|
4609
|
+
delegate as FlowContext & {
|
|
4610
|
+
openView?: (uid: string, options?: Record<PropertyKey, unknown>) => Promise<unknown>;
|
|
4611
|
+
}
|
|
4612
|
+
).openView;
|
|
4613
|
+
|
|
4614
|
+
if (typeof delegateOpenView !== 'function') {
|
|
4615
|
+
throw new Error('ctx.openView is not available in current context.');
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
const routeState = createOpenViewRouteState(options);
|
|
4619
|
+
if (!routeState) {
|
|
4620
|
+
return delegateOpenView(uid, options);
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4623
|
+
return delegateOpenView(uid, {
|
|
4624
|
+
...(options || {}),
|
|
4625
|
+
[RUNJS_OPEN_VIEW_ROUTE_STATE]: routeState,
|
|
4626
|
+
});
|
|
4627
|
+
});
|
|
4561
4628
|
|
|
4562
4629
|
// Convenience: ctx.render(<App />[, container])
|
|
4563
4630
|
// - container defaults to ctx.element if available
|
package/src/flowI18n.ts
CHANGED
|
@@ -64,7 +64,9 @@ export class FlowI18n {
|
|
|
64
64
|
* @private
|
|
65
65
|
*/
|
|
66
66
|
private isTemplate(str: string): boolean {
|
|
67
|
-
|
|
67
|
+
// The closing quote is a backreference to the opening one (group 1) so an embedded quote of a different type — e.g.
|
|
68
|
+
// {{t('… "Post-action event" …')}} — does not terminate the key early.
|
|
69
|
+
return /\{\{\s*t\s*\(\s*(["'`])(?:\\.|(?!\1).)*?\1\s*(?:,\s*.*?)?\s*\)\s*\}\}/.test(str);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -72,9 +74,12 @@ export class FlowI18n {
|
|
|
72
74
|
* @private
|
|
73
75
|
*/
|
|
74
76
|
private compileTemplate(template: string): string {
|
|
77
|
+
// `(["'`])` captures the opening quote; the key allows escaped chars (`\\.`) and any char that is not that same
|
|
78
|
+
// quote (`(?!\1).`), and `\1` closes on the matching quote. This keeps embedded quotes of a different type inside
|
|
79
|
+
// the key instead of truncating it at the first quote of any kind.
|
|
75
80
|
return template.replace(
|
|
76
|
-
/\{\{\s*t\s*\(\s*["'`](
|
|
77
|
-
(match, key, optionsStr) => {
|
|
81
|
+
/\{\{\s*t\s*\(\s*(["'`])((?:\\.|(?!\1).)*?)\1\s*(?:,\s*((?:[^{}]|\{[^}]*\})*?))?\s*\)\s*\}\}/g,
|
|
82
|
+
(match, _quote, key, optionsStr) => {
|
|
78
83
|
try {
|
|
79
84
|
let templateOptions = {};
|
|
80
85
|
if (optionsStr) {
|
package/src/flowSettings.ts
CHANGED
|
@@ -20,7 +20,13 @@ import { FlowRuntimeContext } from './flowContext';
|
|
|
20
20
|
import { FlowEngine, untracked } from '.';
|
|
21
21
|
import { FlowSettingsContextProvider, useFlowSettingsContext } from './hooks/useFlowSettingsContext';
|
|
22
22
|
import type { FlowModel } from './models';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
DynamicFlowSource,
|
|
25
|
+
DynamicFlowSourceProvider,
|
|
26
|
+
ParamObject,
|
|
27
|
+
StepSettingsDialogProps,
|
|
28
|
+
ToolbarItemConfig,
|
|
29
|
+
} from './types';
|
|
24
30
|
import {
|
|
25
31
|
compileUiSchema,
|
|
26
32
|
FlowCancelSaveException,
|
|
@@ -128,6 +134,7 @@ export class FlowSettings {
|
|
|
128
134
|
private engine: FlowEngine;
|
|
129
135
|
#forceEnabled = false; // 强制启用状态,主要用于设计模式下的强制启用
|
|
130
136
|
public toolbarItems: ToolbarItemConfig[] = [];
|
|
137
|
+
private dynamicFlowSourceProviders: DynamicFlowSourceProvider[] = [];
|
|
131
138
|
#emitter: Emitter = new Emitter();
|
|
132
139
|
|
|
133
140
|
constructor(engine: FlowEngine) {
|
|
@@ -463,6 +470,83 @@ export class FlowSettings {
|
|
|
463
470
|
return [...this.toolbarItems];
|
|
464
471
|
}
|
|
465
472
|
|
|
473
|
+
public registerDynamicFlowSourceProvider(provider: DynamicFlowSourceProvider): () => void {
|
|
474
|
+
const existingIndex = this.dynamicFlowSourceProviders.findIndex((item) => item.key === provider.key);
|
|
475
|
+
if (existingIndex !== -1) {
|
|
476
|
+
console.warn(
|
|
477
|
+
`FlowSettings: Dynamic flow source provider with key '${provider.key}' already exists and will be replaced.`,
|
|
478
|
+
);
|
|
479
|
+
this.dynamicFlowSourceProviders[existingIndex] = provider;
|
|
480
|
+
} else {
|
|
481
|
+
this.dynamicFlowSourceProviders.push(provider);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.dynamicFlowSourceProviders.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
|
485
|
+
|
|
486
|
+
return () => {
|
|
487
|
+
const index = this.dynamicFlowSourceProviders.indexOf(provider);
|
|
488
|
+
if (index !== -1) {
|
|
489
|
+
this.dynamicFlowSourceProviders.splice(index, 1);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
public hasDynamicFlowSourceProvider(model: FlowModel): boolean {
|
|
495
|
+
return this.dynamicFlowSourceProviders.some((provider) => {
|
|
496
|
+
try {
|
|
497
|
+
return provider.visible ? provider.visible(model) : true;
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.warn(`FlowSettings: Dynamic flow source provider '${provider.key}' visibility check failed.`, error);
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
public async getDynamicFlowSources(model: FlowModel): Promise<DynamicFlowSource[]> {
|
|
506
|
+
const t = getT(model);
|
|
507
|
+
const selfSource: DynamicFlowSource = {
|
|
508
|
+
key: 'self',
|
|
509
|
+
label: t('Current block'),
|
|
510
|
+
model,
|
|
511
|
+
sort: -1000,
|
|
512
|
+
};
|
|
513
|
+
const sources: DynamicFlowSource[] = [];
|
|
514
|
+
const seenKeys = new Set<string>(['self']);
|
|
515
|
+
const seenModelUids = new Set<string>([model.uid]);
|
|
516
|
+
|
|
517
|
+
for (const provider of this.dynamicFlowSourceProviders) {
|
|
518
|
+
try {
|
|
519
|
+
if (provider.visible && !provider.visible(model)) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const providerSources = await provider.getSources(model);
|
|
524
|
+
for (const source of providerSources || []) {
|
|
525
|
+
if (!source?.key || !source.model) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const key = String(source.key);
|
|
529
|
+
const uid = source.model.uid;
|
|
530
|
+
if (seenKeys.has(key) || seenModelUids.has(uid)) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
seenKeys.add(key);
|
|
534
|
+
seenModelUids.add(uid);
|
|
535
|
+
sources.push({
|
|
536
|
+
...source,
|
|
537
|
+
key,
|
|
538
|
+
label: source.label || key,
|
|
539
|
+
sort: source.sort || 0,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.warn(`FlowSettings: Dynamic flow source provider '${provider.key}' failed.`, error);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return [selfSource, ...sources.sort((a, b) => (a.sort || 0) - (b.sort || 0))];
|
|
548
|
+
}
|
|
549
|
+
|
|
466
550
|
/**
|
|
467
551
|
* 清空所有工具栏项目
|
|
468
552
|
* @example
|
package/src/locale/en-US.json
CHANGED
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"Replace current block with template?": "Replace current block with template?",
|
|
57
57
|
"Replaced with template block": "Replaced with template block",
|
|
58
58
|
"Render failed": "Render failed",
|
|
59
|
+
"Response record": "Response record",
|
|
59
60
|
"Step configuration": "Step configuration",
|
|
60
61
|
"Step parameter configuration": "Step parameter configuration",
|
|
61
62
|
"Step with key {{stepKey}} not found": "Step with key {{stepKey}} not found",
|
package/src/locale/zh-CN.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { APIClient } from '@nocobase/sdk';
|
|
11
11
|
import { FlowContext } from '../flowContext';
|
|
12
|
+
import { getDirtyAwareApiClient } from '../utils/dirtyAwareApiClient';
|
|
12
13
|
import { FlowResource, ResourceError } from './flowResource';
|
|
13
14
|
|
|
14
15
|
export class APIResource<TData = any> extends FlowResource<TData> {
|
|
@@ -33,7 +34,7 @@ export class APIResource<TData = any> extends FlowResource<TData> {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
setAPIClient(api: APIClient) {
|
|
36
|
-
this.api = api;
|
|
37
|
+
this.api = getDirtyAwareApiClient(api, this.context) as APIClient;
|
|
37
38
|
return this;
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -12,7 +12,7 @@ import _ from 'lodash';
|
|
|
12
12
|
import { APIResource } from './apiResource';
|
|
13
13
|
import { FilterItem } from './filterItem';
|
|
14
14
|
import { ResourceError } from './flowResource';
|
|
15
|
-
import {
|
|
15
|
+
import { markDataSourceDirty } from '../utils/dataSourceDirty';
|
|
16
16
|
|
|
17
17
|
export abstract class BaseRecordResource<TData = any> extends APIResource<TData> {
|
|
18
18
|
protected resourceName: string;
|
|
@@ -142,28 +142,11 @@ export abstract class BaseRecordResource<TData = any> extends APIResource<TData>
|
|
|
142
142
|
* Used to coordinate "refresh on active" across view stacks.
|
|
143
143
|
*/
|
|
144
144
|
protected markDataSourceDirty(resourceName?: string) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!resName) return;
|
|
151
|
-
|
|
152
|
-
const affectedResourceNames = new Set<string>([String(resName)]);
|
|
153
|
-
// Optional safety: association resources like "users.profile" may impact parent collection views.
|
|
154
|
-
if (typeof resName === 'string' && resName.includes('.')) {
|
|
155
|
-
affectedResourceNames.add(resName.split('.')[0]);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for (const name of affectedResourceNames) {
|
|
159
|
-
engine.markDataSourceDirty(dataSourceKey, name);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Signal current view to re-evaluate dirty blocks (e.g., same-view sibling refresh).
|
|
163
|
-
// This is emitted on the *current* engine emitter (view-scoped) so it won't affect other views.
|
|
164
|
-
engine.emitter?.emit?.(DATA_SOURCE_DIRTY_EVENT, {
|
|
165
|
-
dataSourceKey,
|
|
166
|
-
resourceNames: Array.from(affectedResourceNames),
|
|
145
|
+
markDataSourceDirty({
|
|
146
|
+
engine: this.context.engine,
|
|
147
|
+
dataSourceKey: this.getDataSourceKey(),
|
|
148
|
+
resourceName: resourceName || this.getResourceName(),
|
|
149
|
+
includePreviousEngines: true,
|
|
167
150
|
});
|
|
168
151
|
}
|
|
169
152
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { observable } from '@formily/reactive';
|
|
11
11
|
import { AxiosRequestConfig } from 'axios';
|
|
12
12
|
import _ from 'lodash';
|
|
13
|
+
import { SKIP_DATA_SOURCE_DIRTY } from '../utils/dirtyAwareApiClient';
|
|
13
14
|
import { BaseRecordResource } from './baseRecordResource';
|
|
14
15
|
|
|
15
16
|
export class MultiRecordResource<TDataItem = any> extends BaseRecordResource<TDataItem[]> {
|
|
@@ -113,7 +114,10 @@ export class MultiRecordResource<TDataItem = any> extends BaseRecordResource<TDa
|
|
|
113
114
|
|
|
114
115
|
async create(data: TDataItem, options?: AxiosRequestConfig & { refresh?: boolean }): Promise<void> {
|
|
115
116
|
const config = this.mergeRequestConfig({ data }, this.createActionOptions, options);
|
|
116
|
-
const res = await this.runAction('create',
|
|
117
|
+
const res = await this.runAction('create', {
|
|
118
|
+
...config,
|
|
119
|
+
[SKIP_DATA_SOURCE_DIRTY]: true,
|
|
120
|
+
});
|
|
117
121
|
this.markDataSourceDirty();
|
|
118
122
|
this.emit('saved', data);
|
|
119
123
|
if (options?.refresh !== false) {
|
|
@@ -146,7 +150,10 @@ export class MultiRecordResource<TDataItem = any> extends BaseRecordResource<TDa
|
|
|
146
150
|
this.updateActionOptions,
|
|
147
151
|
options,
|
|
148
152
|
);
|
|
149
|
-
await this.runAction('update',
|
|
153
|
+
await this.runAction('update', {
|
|
154
|
+
...config,
|
|
155
|
+
[SKIP_DATA_SOURCE_DIRTY]: true,
|
|
156
|
+
});
|
|
150
157
|
this.markDataSourceDirty();
|
|
151
158
|
this.emit('saved', data);
|
|
152
159
|
await this.refresh();
|
|
@@ -172,7 +179,10 @@ export class MultiRecordResource<TDataItem = any> extends BaseRecordResource<TDa
|
|
|
172
179
|
},
|
|
173
180
|
options,
|
|
174
181
|
);
|
|
175
|
-
await this.runAction('destroy',
|
|
182
|
+
await this.runAction('destroy', {
|
|
183
|
+
...config,
|
|
184
|
+
[SKIP_DATA_SOURCE_DIRTY]: true,
|
|
185
|
+
});
|
|
176
186
|
this.markDataSourceDirty();
|
|
177
187
|
const currentPage = this.getPage();
|
|
178
188
|
const lastPage = Math.ceil((this.getCount() - _.castArray(filterByTk).length) / this.getPageSize());
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { AxiosRequestConfig } from 'axios';
|
|
11
11
|
import _ from 'lodash';
|
|
12
|
+
import { SKIP_DATA_SOURCE_DIRTY } from '../utils/dirtyAwareApiClient';
|
|
12
13
|
import { BaseRecordResource } from './baseRecordResource';
|
|
13
14
|
|
|
14
15
|
export class SingleRecordResource<TData = any> extends BaseRecordResource<TData> {
|
|
@@ -43,6 +44,7 @@ export class SingleRecordResource<TData = any> extends BaseRecordResource<TData>
|
|
|
43
44
|
const res = await this.runAction(actionName, {
|
|
44
45
|
...config,
|
|
45
46
|
data: result,
|
|
47
|
+
[SKIP_DATA_SOURCE_DIRTY]: true,
|
|
46
48
|
});
|
|
47
49
|
// Mark as dirty before emitting/refreshing so other views can refresh when activated.
|
|
48
50
|
this.markDataSourceDirty();
|
|
@@ -62,7 +64,10 @@ export class SingleRecordResource<TData = any> extends BaseRecordResource<TData>
|
|
|
62
64
|
},
|
|
63
65
|
options,
|
|
64
66
|
);
|
|
65
|
-
await this.runAction('destroy',
|
|
67
|
+
await this.runAction('destroy', {
|
|
68
|
+
...config,
|
|
69
|
+
[SKIP_DATA_SOURCE_DIRTY]: true,
|
|
70
|
+
});
|
|
66
71
|
this.markDataSourceDirty();
|
|
67
72
|
this.setData(null);
|
|
68
73
|
}
|
|
@@ -46,12 +46,17 @@ export function createJSRunnerWithVersion(this: FlowContext, options?: JSRunnerO
|
|
|
46
46
|
doc = {};
|
|
47
47
|
}
|
|
48
48
|
const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (typeof
|
|
53
|
-
|
|
49
|
+
const browserGlobals: Record<string, any> = {};
|
|
50
|
+
if (typeof window !== 'undefined') {
|
|
51
|
+
browserGlobals.window = window;
|
|
52
|
+
if (typeof navigator !== 'undefined') {
|
|
53
|
+
browserGlobals.navigator = navigator;
|
|
54
|
+
}
|
|
54
55
|
}
|
|
56
|
+
if (typeof document !== 'undefined') {
|
|
57
|
+
browserGlobals.document = document;
|
|
58
|
+
}
|
|
59
|
+
const globals: Record<string, any> = { ctx: deprecatedCtx, ...browserGlobals, ...(options?.globals || {}) };
|
|
55
60
|
// 透传 JSRunnerOptions 其余配置(如 timeoutMs)
|
|
56
61
|
const { timeoutMs } = options || {};
|
|
57
62
|
return new JSRunner({ globals, timeoutMs });
|
|
@@ -59,7 +64,8 @@ export function createJSRunnerWithVersion(this: FlowContext, options?: JSRunnerO
|
|
|
59
64
|
|
|
60
65
|
export function getRunJSScenesForModel(modelClass: string, version: RunJSVersion = 'v1'): string[] {
|
|
61
66
|
const meta = RunJSContextRegistry.getMeta(version, modelClass);
|
|
62
|
-
|
|
67
|
+
const scenes = meta?.scenes;
|
|
68
|
+
return Array.isArray(scenes) ? [...scenes] : [];
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
export function getRunJSScenesForContext(ctx: FlowContext, { version = 'v1' as RunJSVersion } = {}): string[] {
|
package/src/types.ts
CHANGED
|
@@ -612,6 +612,20 @@ export interface ToolbarItemConfig {
|
|
|
612
612
|
sort?: number;
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
export interface DynamicFlowSource {
|
|
616
|
+
key: string;
|
|
617
|
+
label: React.ReactNode;
|
|
618
|
+
model: FlowModel;
|
|
619
|
+
sort?: number;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export interface DynamicFlowSourceProvider {
|
|
623
|
+
key: string;
|
|
624
|
+
sort?: number;
|
|
625
|
+
visible?: (model: FlowModel) => boolean;
|
|
626
|
+
getSources: (model: FlowModel) => DynamicFlowSource[] | Promise<DynamicFlowSource[]>;
|
|
627
|
+
}
|
|
628
|
+
|
|
615
629
|
export interface ApplyFlowCacheEntry {
|
|
616
630
|
status: 'pending' | 'resolved' | 'rejected';
|
|
617
631
|
promise: Promise<any>;
|