@nocobase/flow-engine 2.0.0-beta.9 → 2.0.1
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/BlockScopedFlowEngine.js +0 -1
- package/lib/FlowDefinition.d.ts +2 -0
- package/lib/JSRunner.d.ts +6 -0
- package/lib/JSRunner.js +32 -2
- package/lib/ViewScopedFlowEngine.js +3 -0
- package/lib/acl/Acl.js +13 -3
- package/lib/components/FlowContextSelector.js +155 -10
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
- package/lib/components/variables/VariableInput.js +9 -4
- package/lib/components/variables/VariableTag.js +46 -39
- package/lib/components/variables/utils.d.ts +7 -0
- package/lib/components/variables/utils.js +42 -2
- package/lib/data-source/index.d.ts +7 -27
- package/lib/data-source/index.js +81 -51
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +163 -22
- package/lib/flowContext.d.ts +230 -7
- package/lib/flowContext.js +2267 -148
- package/lib/flowEngine.d.ts +21 -0
- package/lib/flowEngine.js +56 -8
- package/lib/flowI18n.js +6 -4
- package/lib/flowSettings.js +17 -11
- package/lib/index.d.ts +7 -1
- package/lib/index.js +21 -0
- package/lib/locale/en-US.json +9 -2
- package/lib/locale/index.d.ts +14 -0
- package/lib/locale/zh-CN.json +8 -1
- package/lib/models/CollectionFieldModel.d.ts +1 -0
- package/lib/models/CollectionFieldModel.js +3 -2
- package/lib/models/flowModel.js +12 -1
- package/lib/provider.js +5 -5
- package/lib/resources/baseRecordResource.d.ts +5 -0
- package/lib/resources/baseRecordResource.js +24 -0
- package/lib/resources/multiRecordResource.d.ts +1 -0
- package/lib/resources/multiRecordResource.js +11 -4
- package/lib/resources/singleRecordResource.js +2 -0
- package/lib/resources/sqlResource.d.ts +4 -3
- package/lib/resources/sqlResource.js +8 -3
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/base.js +706 -41
- package/lib/runjs-context/contributions.d.ts +33 -0
- package/lib/runjs-context/contributions.js +88 -0
- package/lib/runjs-context/helpers.js +12 -1
- package/lib/runjs-context/setup.js +6 -0
- package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
- package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
- package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
- package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
- package/lib/runjs-context/snippets/index.d.ts +11 -1
- package/lib/runjs-context/snippets/index.js +61 -40
- package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
- package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
- package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
- package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
- package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
- package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
- package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
- package/lib/runjsLibs.d.ts +28 -0
- package/lib/runjsLibs.js +532 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
- package/lib/scheduler/ModelOperationScheduler.js +25 -21
- package/lib/types.d.ts +27 -0
- package/lib/utils/associationObjectVariable.d.ts +2 -2
- package/lib/utils/createCollectionContextMeta.js +1 -0
- package/lib/utils/createEphemeralContext.js +2 -2
- package/lib/utils/dateVariable.d.ts +16 -0
- package/lib/utils/dateVariable.js +380 -0
- package/lib/utils/exceptions.d.ts +7 -0
- package/lib/utils/exceptions.js +10 -0
- package/lib/utils/index.d.ts +8 -3
- package/lib/utils/index.js +45 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/resolveModuleUrl.d.ts +58 -0
- package/lib/utils/resolveModuleUrl.js +65 -0
- package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
- package/lib/utils/resolveRunJSObjectValues.js +61 -0
- package/lib/utils/runjsModuleLoader.d.ts +58 -0
- package/lib/utils/runjsModuleLoader.js +422 -0
- package/lib/utils/runjsTemplateCompat.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +743 -0
- package/lib/utils/runjsValue.d.ts +29 -0
- package/lib/utils/runjsValue.js +275 -0
- package/lib/utils/safeGlobals.d.ts +18 -8
- package/lib/utils/safeGlobals.js +164 -17
- package/lib/utils/schema-utils.d.ts +10 -0
- package/lib/utils/schema-utils.js +61 -0
- package/lib/views/createViewMeta.d.ts +0 -7
- package/lib/views/createViewMeta.js +19 -70
- package/lib/views/index.d.ts +1 -2
- package/lib/views/index.js +4 -3
- package/lib/views/useDialog.js +7 -2
- package/lib/views/useDrawer.js +7 -2
- package/lib/views/usePage.d.ts +4 -0
- package/lib/views/usePage.js +43 -6
- package/lib/views/usePopover.js +4 -1
- package/lib/views/viewEvents.d.ts +17 -0
- package/lib/views/viewEvents.js +90 -0
- package/package.json +4 -4
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/JSRunner.ts +44 -2
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +64 -0
- package/src/__tests__/createViewMeta.popup.test.ts +62 -1
- package/src/__tests__/flowContext.test.ts +693 -1
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
- package/src/__tests__/flowRuntimeContext.test.ts +2 -1
- package/src/__tests__/flowSettings.open.test.tsx +123 -19
- package/src/__tests__/runjsContext.test.ts +10 -7
- package/src/__tests__/runjsContextImplementations.test.ts +34 -3
- package/src/__tests__/runjsContextRuntime.test.ts +3 -3
- package/src/__tests__/runjsContributions.test.ts +89 -0
- package/src/__tests__/runjsExternalLibs.test.ts +242 -0
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/__tests__/runjsLocales.test.ts +4 -1
- package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
- package/src/__tests__/runjsSnippets.test.ts +40 -3
- package/src/acl/Acl.tsx +3 -3
- package/src/components/FlowContextSelector.tsx +208 -12
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
- package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
- package/src/components/variables/VariableInput.tsx +12 -4
- package/src/components/variables/VariableTag.tsx +54 -45
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
- package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
- package/src/components/variables/__tests__/utils.test.ts +81 -3
- package/src/components/variables/utils.ts +67 -6
- package/src/data-source/index.ts +85 -110
- package/src/executor/FlowExecutor.ts +200 -23
- package/src/executor/__tests__/flowExecutor.test.ts +66 -0
- package/src/flowContext.ts +2986 -211
- package/src/flowEngine.ts +59 -8
- package/src/flowI18n.ts +7 -5
- package/src/flowSettings.ts +18 -12
- package/src/index.ts +14 -1
- package/src/locale/en-US.json +9 -2
- package/src/locale/zh-CN.json +8 -1
- package/src/models/CollectionFieldModel.tsx +3 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
- package/src/models/__tests__/flowModel.test.ts +20 -4
- package/src/models/flowModel.tsx +13 -1
- package/src/provider.tsx +7 -6
- package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
- package/src/resources/__tests__/sqlResource.test.ts +60 -0
- package/src/resources/baseRecordResource.ts +31 -0
- package/src/resources/multiRecordResource.ts +11 -4
- package/src/resources/singleRecordResource.ts +3 -0
- package/src/resources/sqlResource.ts +11 -6
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/base.ts +715 -44
- package/src/runjs-context/contributions.ts +88 -0
- package/src/runjs-context/helpers.ts +11 -1
- package/src/runjs-context/setup.ts +6 -0
- package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
- package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
- package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
- package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
- package/src/runjs-context/snippets/index.ts +75 -41
- package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
- package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
- package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
- package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
- package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
- package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
- package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
- package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
- package/src/runjsLibs.ts +622 -0
- package/src/scheduler/ModelOperationScheduler.ts +27 -21
- package/src/types.ts +38 -1
- package/src/utils/__tests__/dateVariable.test.ts +101 -0
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
- package/src/utils/__tests__/runjsValue.test.ts +44 -0
- package/src/utils/__tests__/safeGlobals.test.ts +57 -2
- package/src/utils/__tests__/utils.test.ts +95 -0
- package/src/utils/associationObjectVariable.ts +2 -2
- package/src/utils/createCollectionContextMeta.ts +1 -0
- package/src/utils/createEphemeralContext.ts +5 -4
- package/src/utils/dateVariable.ts +397 -0
- package/src/utils/exceptions.ts +11 -0
- package/src/utils/index.ts +37 -3
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/resolveModuleUrl.ts +91 -0
- package/src/utils/resolveRunJSObjectValues.ts +46 -0
- package/src/utils/runjsModuleLoader.ts +553 -0
- package/src/utils/runjsTemplateCompat.ts +828 -0
- package/src/utils/runjsValue.ts +287 -0
- package/src/utils/safeGlobals.ts +188 -17
- package/src/utils/schema-utils.ts +79 -0
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
- package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
- package/src/views/createViewMeta.ts +22 -75
- package/src/views/index.tsx +1 -2
- package/src/views/useDialog.tsx +8 -1
- package/src/views/useDrawer.tsx +8 -1
- package/src/views/usePage.tsx +51 -5
- package/src/views/usePopover.tsx +4 -1
- package/src/views/viewEvents.ts +55 -0
package/src/flowContext.ts
CHANGED
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { ISchema } from '@formily/json-schema';
|
|
11
11
|
import { observable } from '@formily/reactive';
|
|
12
|
-
import { APIClient } from '@nocobase/sdk';
|
|
12
|
+
import { APIClient, RequestOptions } from '@nocobase/sdk';
|
|
13
13
|
import type { Router } from '@remix-run/router';
|
|
14
14
|
import { MessageInstance } from 'antd/es/message/interface';
|
|
15
15
|
import * as antd from 'antd';
|
|
16
|
-
import * as antdIcons from '@ant-design/icons';
|
|
17
16
|
import type { HookAPI } from 'antd/es/modal/useModal';
|
|
18
17
|
import { NotificationInstance } from 'antd/es/notification/interface';
|
|
19
18
|
import _ from 'lodash';
|
|
@@ -38,17 +37,26 @@ import {
|
|
|
38
37
|
extractPropertyPath,
|
|
39
38
|
extractUsedVariablePaths,
|
|
40
39
|
FlowExitException,
|
|
40
|
+
FLOW_ENGINE_NAMESPACE,
|
|
41
|
+
isCtxDatePathPrefix,
|
|
42
|
+
isCssFile,
|
|
43
|
+
prepareRunJsCode,
|
|
44
|
+
resolveCtxDatePath,
|
|
41
45
|
resolveDefaultParams,
|
|
42
46
|
resolveExpressions,
|
|
47
|
+
resolveModuleUrl,
|
|
43
48
|
} from './utils';
|
|
44
49
|
import { FlowExitAllException } from './utils/exceptions';
|
|
45
50
|
import { enqueueVariablesResolve, JSONValue } from './utils/params-resolvers';
|
|
46
51
|
import type { RecordRef } from './utils/serverContextParams';
|
|
47
52
|
import { buildServerContextParams as _buildServerContextParams } from './utils/serverContextParams';
|
|
53
|
+
import { inferRecordRef } from './utils/variablesParams';
|
|
48
54
|
import { FlowView, FlowViewer } from './views/FlowView';
|
|
49
|
-
import { RunJSContextRegistry, getModelClassName } from './runjs-context/registry';
|
|
55
|
+
import { RunJSContextRegistry, getModelClassName, type RunJSVersion } from './runjs-context/registry';
|
|
50
56
|
import { createEphemeralContext } from './utils/createEphemeralContext';
|
|
51
57
|
import dayjs from 'dayjs';
|
|
58
|
+
import { externalReactRender, setupRunJSLibs } from './runjsLibs';
|
|
59
|
+
import { runjsImportAsync, runjsImportModule, runjsRequireAsync } from './utils/runjsModuleLoader';
|
|
52
60
|
|
|
53
61
|
// Helper: detect a RecordRef-like object
|
|
54
62
|
function isRecordRefLike(val: any): boolean {
|
|
@@ -71,13 +79,109 @@ function filterBuilderOutputByPaths(built: any, neededPaths: string[]): any {
|
|
|
71
79
|
return undefined;
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
// Helper: extract top-level segment of a subpath (e.g. 'a.b' -> 'a', 'tags[0].name' -> 'tags')
|
|
83
|
+
function topLevelOf(subPath: string): string | undefined {
|
|
84
|
+
if (!subPath) return undefined;
|
|
85
|
+
const m = String(subPath).match(/^([^.[]+)/);
|
|
86
|
+
return m?.[1];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Helper: infer selects (fields/appends) from usage paths (mirrors server-side inferSelectsFromUsage)
|
|
90
|
+
function inferSelectsFromUsage(paths: string[] = []): { generatedAppends?: string[]; generatedFields?: string[] } {
|
|
91
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
92
|
+
return { generatedAppends: undefined, generatedFields: undefined };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const appendSet = new Set<string>();
|
|
96
|
+
const fieldSet = new Set<string>();
|
|
97
|
+
|
|
98
|
+
const normalizePath = (raw: string): string => {
|
|
99
|
+
if (!raw) return '';
|
|
100
|
+
let s = String(raw);
|
|
101
|
+
// remove numeric indexes like [0]
|
|
102
|
+
s = s.replace(/\[(?:\d+)\]/g, '');
|
|
103
|
+
// normalize string indexes like ["name"] / ['name'] into .name
|
|
104
|
+
s = s.replace(/\[(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')\]/g, (_m, g1, g2) => `.${(g1 || g2) as string}`);
|
|
105
|
+
s = s.replace(/\.\.+/g, '.');
|
|
106
|
+
s = s.replace(/^\./, '').replace(/\.$/, '');
|
|
107
|
+
return s;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for (let path of paths) {
|
|
111
|
+
if (!path) continue;
|
|
112
|
+
// drop leading numeric index like [0].name
|
|
113
|
+
while (/^\[(\d+)\](\.|$)/.test(path)) {
|
|
114
|
+
path = path.replace(/^\[(\d+)\]\.?/, '');
|
|
115
|
+
}
|
|
116
|
+
const norm = normalizePath(path);
|
|
117
|
+
if (!norm) continue;
|
|
118
|
+
const segments = norm.split('.').filter(Boolean);
|
|
119
|
+
if (segments.length === 0) continue;
|
|
120
|
+
|
|
121
|
+
if (segments.length === 1) {
|
|
122
|
+
fieldSet.add(segments[0]);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
127
|
+
appendSet.add(segments.slice(0, i + 1).join('.'));
|
|
128
|
+
}
|
|
129
|
+
fieldSet.add(segments.join('.'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const generatedAppends = appendSet.size ? Array.from(appendSet) : undefined;
|
|
133
|
+
const generatedFields = fieldSet.size ? Array.from(fieldSet) : undefined;
|
|
134
|
+
return { generatedAppends, generatedFields };
|
|
135
|
+
}
|
|
136
|
+
|
|
74
137
|
type Getter<T = any> = (ctx: FlowContext) => T | Promise<T>;
|
|
75
138
|
|
|
139
|
+
export type FlowContextDocRef = string | { url: string; title?: string };
|
|
140
|
+
|
|
141
|
+
export type FlowDeprecationDoc =
|
|
142
|
+
| boolean
|
|
143
|
+
| {
|
|
144
|
+
/**
|
|
145
|
+
* 废弃说明(面向人/大模型)。
|
|
146
|
+
*/
|
|
147
|
+
message?: string;
|
|
148
|
+
/**
|
|
149
|
+
* 推荐替代 API(例如 'ctx.resolveJsonTemplate')。
|
|
150
|
+
*/
|
|
151
|
+
replacedBy?: string | string[];
|
|
152
|
+
/**
|
|
153
|
+
* 开始废弃的版本号(可选)。
|
|
154
|
+
*/
|
|
155
|
+
since?: string;
|
|
156
|
+
/**
|
|
157
|
+
* 预计移除的版本号(可选)。
|
|
158
|
+
*/
|
|
159
|
+
removedIn?: string;
|
|
160
|
+
/**
|
|
161
|
+
* 参考链接(可选)。
|
|
162
|
+
*/
|
|
163
|
+
ref?: FlowContextDocRef;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export type FlowContextDocParam = {
|
|
167
|
+
name: string;
|
|
168
|
+
description?: string;
|
|
169
|
+
type?: string;
|
|
170
|
+
optional?: boolean;
|
|
171
|
+
default?: JSONValue;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export type FlowContextDocReturn = {
|
|
175
|
+
description?: string;
|
|
176
|
+
type?: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
76
179
|
export interface MetaTreeNode {
|
|
77
180
|
name: string;
|
|
78
181
|
title: string;
|
|
79
182
|
type: string;
|
|
80
183
|
interface?: string;
|
|
184
|
+
options?: any;
|
|
81
185
|
uiSchema?: ISchema;
|
|
82
186
|
render?: (props: any) => JSX.Element;
|
|
83
187
|
// display?: 'default' | 'flatten' | 'none'; // 显示模式:默认、平铺子菜单、完全隐藏, 用于简化meta树显示层级
|
|
@@ -95,6 +199,7 @@ export interface PropertyMeta {
|
|
|
95
199
|
type: string;
|
|
96
200
|
title: string;
|
|
97
201
|
interface?: string;
|
|
202
|
+
options?: any;
|
|
98
203
|
uiSchema?: ISchema; // TODO: 这个是不是压根没必要啊?
|
|
99
204
|
render?: (props: any) => JSX.Element; // 自定义渲染函数
|
|
100
205
|
// 用于 VariableInput 的排序:数值越大,显示越靠前;相同值保持稳定顺序
|
|
@@ -102,11 +207,11 @@ export interface PropertyMeta {
|
|
|
102
207
|
// display?: 'default' | 'flatten' | 'none'; // 显示模式:默认、平铺子菜单、完全隐藏, 用于简化meta树显示层级
|
|
103
208
|
properties?: Record<string, PropertyMeta> | (() => Promise<Record<string, PropertyMeta>>);
|
|
104
209
|
// 变量禁用控制:若 disabled 为真(或函数返回真)则禁用
|
|
105
|
-
disabled?: boolean | (() => boolean);
|
|
210
|
+
disabled?: boolean | (() => boolean | Promise<boolean>);
|
|
106
211
|
// 禁用原因(用于 UI 小问号提示),可为函数
|
|
107
|
-
disabledReason?: string | (() => string | undefined);
|
|
212
|
+
disabledReason?: string | (() => string | undefined | Promise<string | undefined>);
|
|
108
213
|
// 显示控制:当 hidden 为 true(或函数返回 true)时,不在变量选择器中展示该节点
|
|
109
|
-
hidden?: boolean | (() => boolean);
|
|
214
|
+
hidden?: boolean | (() => boolean | Promise<boolean>);
|
|
110
215
|
// 变量解析参数构造器(用于 variables:resolve 的 contextParams,按属性名归位)。
|
|
111
216
|
// 支持返回 RecordRef 或任意嵌套对象(将被 buildServerContextParams 扁平化,例如 { record: RecordRef } -> 'view.record')。
|
|
112
217
|
buildVariablesParams?: (
|
|
@@ -138,16 +243,154 @@ export interface PropertyOptions {
|
|
|
138
243
|
cache?: boolean;
|
|
139
244
|
observable?: boolean; // 是否为 observable 属性
|
|
140
245
|
meta?: PropertyMetaOrFactory; // 支持静态、函数和异步函数(工厂函数可带 title/sort)
|
|
246
|
+
/**
|
|
247
|
+
* 面向工具/大模型的静态文档信息(不影响变量选择器 UI)。
|
|
248
|
+
* - `getApiInfos()` 仅使用 RunJS doc + 这里的 `info`(不会读取/展开 `meta`)
|
|
249
|
+
* - 变量结构信息请使用 `getVarInfos()`(来源于 `meta`)
|
|
250
|
+
*/
|
|
251
|
+
info?: FlowContextPropertyInfoOrFactory;
|
|
141
252
|
// 标记该属性是否在服务端解析:
|
|
142
253
|
// - boolean: true 表示整个顶层变量交给服务端;false 表示仅前端解析
|
|
143
254
|
// - function: 根据子路径决定是否交给服务端(子路径示例:'record.roles[0].name'、'id'、'')
|
|
144
255
|
resolveOnServer?: boolean | ((subPath: string) => boolean);
|
|
145
256
|
// 优化:当需要服务端解析但本属性在 buildVariablesParams 返回空时,是否跳过调用服务端。
|
|
146
|
-
// - 典型场景:formValues /
|
|
257
|
+
// - 典型场景:formValues / item 仅在“已选关联值”存在时才需要服务端;否则没有必要请求。
|
|
147
258
|
// - 默认 false:保持兼容,其他变量即使没有 contextParams 也可选择调用服务端。
|
|
148
259
|
serverOnlyWhenContextParams?: boolean;
|
|
149
260
|
}
|
|
150
261
|
|
|
262
|
+
export type FlowContextMethodInfoInput = {
|
|
263
|
+
description?: string;
|
|
264
|
+
detail?: string;
|
|
265
|
+
examples?: string[];
|
|
266
|
+
completion?: RunJSDocCompletionDoc;
|
|
267
|
+
ref?: FlowContextDocRef;
|
|
268
|
+
deprecated?: FlowDeprecationDoc;
|
|
269
|
+
params?: FlowContextDocParam[];
|
|
270
|
+
returns?: FlowContextDocReturn;
|
|
271
|
+
hidden?: boolean | ((ctx: any) => boolean | Promise<boolean>);
|
|
272
|
+
disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
|
|
273
|
+
disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export type FlowContextMethodInfo = {
|
|
277
|
+
description?: string;
|
|
278
|
+
detail?: string;
|
|
279
|
+
examples?: string[];
|
|
280
|
+
completion?: RunJSDocCompletionDoc;
|
|
281
|
+
ref?: FlowContextDocRef;
|
|
282
|
+
deprecated?: FlowDeprecationDoc;
|
|
283
|
+
params?: FlowContextDocParam[];
|
|
284
|
+
returns?: FlowContextDocReturn;
|
|
285
|
+
disabled?: boolean;
|
|
286
|
+
disabledReason?: string;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export type FlowContextPropertyInfoObjectInput = Omit<
|
|
290
|
+
FlowContextPropertyInfo,
|
|
291
|
+
'disabled' | 'disabledReason' | 'properties'
|
|
292
|
+
> & {
|
|
293
|
+
properties?:
|
|
294
|
+
| Record<string, FlowContextPropertyInfoInput>
|
|
295
|
+
| (() => Promise<Record<string, FlowContextPropertyInfoInput>>);
|
|
296
|
+
hidden?: RunJSDocHiddenOrPathsDoc;
|
|
297
|
+
disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
|
|
298
|
+
disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export type FlowContextPropertyInfoInput = string | FlowContextPropertyInfoObjectInput;
|
|
302
|
+
|
|
303
|
+
export type FlowContextPropertyInfoFactory = {
|
|
304
|
+
(): FlowContextPropertyInfoInput | Promise<FlowContextPropertyInfoInput | null> | null;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export type FlowContextPropertyInfoOrFactory = FlowContextPropertyInfoInput | FlowContextPropertyInfoFactory;
|
|
308
|
+
|
|
309
|
+
export type FlowContextPropertyInfo = {
|
|
310
|
+
title?: string;
|
|
311
|
+
type?: string;
|
|
312
|
+
interface?: string;
|
|
313
|
+
description?: string;
|
|
314
|
+
detail?: string;
|
|
315
|
+
examples?: string[];
|
|
316
|
+
completion?: RunJSDocCompletionDoc;
|
|
317
|
+
ref?: FlowContextDocRef;
|
|
318
|
+
deprecated?: FlowDeprecationDoc;
|
|
319
|
+
params?: FlowContextDocParam[];
|
|
320
|
+
returns?: FlowContextDocReturn;
|
|
321
|
+
disabled?: boolean;
|
|
322
|
+
disabledReason?: string;
|
|
323
|
+
properties?: Record<string, FlowContextPropertyInfo>;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export type FlowContextApiInfo = {
|
|
327
|
+
title?: string;
|
|
328
|
+
type?: string;
|
|
329
|
+
interface?: string;
|
|
330
|
+
description?: string;
|
|
331
|
+
examples?: string[];
|
|
332
|
+
completion?: RunJSDocCompletionDoc;
|
|
333
|
+
ref?: FlowContextDocRef;
|
|
334
|
+
deprecated?: FlowDeprecationDoc;
|
|
335
|
+
params?: FlowContextDocParam[];
|
|
336
|
+
returns?: FlowContextDocReturn;
|
|
337
|
+
disabled?: boolean;
|
|
338
|
+
disabledReason?: string;
|
|
339
|
+
properties?: Record<string, FlowContextApiInfo>;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export type FlowContextInfosEnvNode = {
|
|
343
|
+
/**
|
|
344
|
+
* 说明(面向人/大模型)。建议为一句话。
|
|
345
|
+
*/
|
|
346
|
+
description?: string;
|
|
347
|
+
/**
|
|
348
|
+
* 可用于 `await ctx.getVar(getVar)` 的表达式字符串,推荐以 `ctx.` 开头。
|
|
349
|
+
* 例如:'ctx.popup'、'ctx.resource.collectionName'
|
|
350
|
+
*/
|
|
351
|
+
getVar?: string;
|
|
352
|
+
/**
|
|
353
|
+
* 已解析/可序列化的静态值(用于 prompt 直接使用)。
|
|
354
|
+
* 注意:应保持小体积,避免放入 record 等大对象。
|
|
355
|
+
*/
|
|
356
|
+
value?: JSONValue;
|
|
357
|
+
/**
|
|
358
|
+
* 子节点(用于表达 popup.resource.xxx 等层级结构)。
|
|
359
|
+
*/
|
|
360
|
+
properties?: Record<string, FlowContextInfosEnvNode>;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export type FlowContextInfosEnvs = {
|
|
364
|
+
popup?: FlowContextInfosEnvNode;
|
|
365
|
+
block?: FlowContextInfosEnvNode;
|
|
366
|
+
currentViewBlocks?: FlowContextInfosEnvNode;
|
|
367
|
+
flowModel?: FlowContextInfosEnvNode;
|
|
368
|
+
resource?: FlowContextInfosEnvNode;
|
|
369
|
+
record?: FlowContextInfosEnvNode;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export type FlowContextGetApiInfosOptions = {
|
|
373
|
+
/**
|
|
374
|
+
* RunJS 文档版本(默认 v1)。
|
|
375
|
+
*/
|
|
376
|
+
version?: RunJSVersion;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
export type FlowContextGetVarInfosOptions = {
|
|
380
|
+
/**
|
|
381
|
+
* 最大展开层级(默认 3)。
|
|
382
|
+
* - 当不传 path 时,top-level property depth=1。
|
|
383
|
+
* - 当传 path 时,path 对应节点 depth=1。
|
|
384
|
+
*/
|
|
385
|
+
maxDepth?: number;
|
|
386
|
+
/**
|
|
387
|
+
* 剪裁:仅收集指定 path 下的变量结构信息。
|
|
388
|
+
* - string 形式支持:'record'、'record.id'、'ctx.record'、'{{ ctx.record }}'
|
|
389
|
+
* - string[] 表示多个剪裁路径合并
|
|
390
|
+
*/
|
|
391
|
+
path?: string | string[];
|
|
392
|
+
};
|
|
393
|
+
|
|
151
394
|
type RouteOptions = {
|
|
152
395
|
name?: string; // 路由唯一标识
|
|
153
396
|
path?: string; // 路由模板
|
|
@@ -158,6 +401,7 @@ type RouteOptions = {
|
|
|
158
401
|
export class FlowContext {
|
|
159
402
|
_props: Record<string, PropertyOptions> = {};
|
|
160
403
|
_methods: Record<string, (...args: any[]) => any> = {};
|
|
404
|
+
_methodInfos: Record<string, FlowContextMethodInfoInput> = {};
|
|
161
405
|
protected _cache: Record<string, any> = {};
|
|
162
406
|
protected _observableCache: Record<string, any> = observable.shallow({});
|
|
163
407
|
protected _delegates: FlowContext[] = [];
|
|
@@ -242,8 +486,15 @@ export class FlowContext {
|
|
|
242
486
|
});
|
|
243
487
|
}
|
|
244
488
|
|
|
245
|
-
defineMethod(name: string, fn: (...args: any[]) => any,
|
|
489
|
+
defineMethod(name: string, fn: (...args: any[]) => any, info?: string | FlowContextMethodInfoInput) {
|
|
246
490
|
this._methods[name] = fn;
|
|
491
|
+
if (typeof info === 'string') {
|
|
492
|
+
this._methodInfos[name] = { description: info };
|
|
493
|
+
} else if (info && typeof info === 'object') {
|
|
494
|
+
this._methodInfos[name] = info;
|
|
495
|
+
} else {
|
|
496
|
+
delete this._methodInfos[name];
|
|
497
|
+
}
|
|
247
498
|
Object.defineProperty(this, name, {
|
|
248
499
|
configurable: true,
|
|
249
500
|
enumerable: false,
|
|
@@ -421,6 +672,1821 @@ export class FlowContext {
|
|
|
421
672
|
return sorted.map(([key, metaOrFactory]) => this.#toTreeNode(key, metaOrFactory, [key], []));
|
|
422
673
|
}
|
|
423
674
|
|
|
675
|
+
/**
|
|
676
|
+
* 获取静态 API 文档信息(仅顶层一层)。
|
|
677
|
+
*
|
|
678
|
+
* - 输出仅来自 RunJS doc 与 defineProperty/defineMethod 的 info
|
|
679
|
+
* - 不读取/展开 PropertyMeta(变量结构)
|
|
680
|
+
* - 不自动展开深层 properties
|
|
681
|
+
* - 不返回自动补全字段(例如 completion)
|
|
682
|
+
*/
|
|
683
|
+
async getApiInfos(options: FlowContextGetApiInfosOptions = {}): Promise<Record<string, FlowContextApiInfo>> {
|
|
684
|
+
const version = (options.version as RunJSVersion) || ('v1' as RunJSVersion);
|
|
685
|
+
const evalCtx = this.createProxy();
|
|
686
|
+
|
|
687
|
+
const isPrivateKey = (key: string) => typeof key === 'string' && key.startsWith('_');
|
|
688
|
+
// NOTE: These are variable-like roots documented in RunJS context doc, but should be served by `getVarInfos()`.
|
|
689
|
+
// `getApiInfos()` intentionally excludes them to keep static docs and variable meta separated.
|
|
690
|
+
const isVarRootKey = (key: string) => key === 'record' || key === 'formValues' || key === 'popup';
|
|
691
|
+
|
|
692
|
+
const isPromiseLike = (v: any): v is Promise<any> =>
|
|
693
|
+
!!v && (typeof v === 'object' || typeof v === 'function') && typeof (v as any).then === 'function';
|
|
694
|
+
|
|
695
|
+
const getRunJSDoc = (): any => {
|
|
696
|
+
const modelClass = getModelClassName(this);
|
|
697
|
+
const Ctor = RunJSContextRegistry.resolve(version, modelClass) || RunJSContextRegistry.resolve(version, '*');
|
|
698
|
+
if (!Ctor) return {};
|
|
699
|
+
const locale = (this as any)?.api?.auth?.locale || (this as any)?.i18n?.language || (this as any)?.locale;
|
|
700
|
+
try {
|
|
701
|
+
if ((Ctor as any)?.getDoc?.length) {
|
|
702
|
+
return (Ctor as any).getDoc(locale) || {};
|
|
703
|
+
}
|
|
704
|
+
return (Ctor as any)?.getDoc?.() || {};
|
|
705
|
+
} catch (_) {
|
|
706
|
+
return {};
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const doc = getRunJSDoc();
|
|
711
|
+
const docMethods = __isPlainObject(doc?.methods) ? (doc.methods as Record<string, any>) : {};
|
|
712
|
+
const docProps = __isPlainObject(doc?.properties) ? (doc.properties as Record<string, any>) : {};
|
|
713
|
+
|
|
714
|
+
const toDocObject = (node: any): any | undefined => {
|
|
715
|
+
if (typeof node === 'string') return { description: node };
|
|
716
|
+
if (__isPlainObject(node)) return node;
|
|
717
|
+
return undefined;
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const mapDocKeyToApiKey = (key: string, docNode: any): string => {
|
|
721
|
+
// Some libs are exposed as both `ctx.React` and `ctx.libs.React`. Prefer documenting them under `libs.*`.
|
|
722
|
+
const desc =
|
|
723
|
+
typeof docNode === 'string'
|
|
724
|
+
? docNode
|
|
725
|
+
: __isPlainObject(docNode) && typeof (docNode as any).description === 'string'
|
|
726
|
+
? String((docNode as any).description)
|
|
727
|
+
: undefined;
|
|
728
|
+
if (desc && desc.includes(`ctx.libs.${key}`)) return `libs.${key}`;
|
|
729
|
+
return key;
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const pickMethodInfo = (obj: any): Partial<FlowContextApiInfo> => {
|
|
733
|
+
const src = toDocObject(obj);
|
|
734
|
+
if (!src) return {};
|
|
735
|
+
const out: any = {};
|
|
736
|
+
for (const k of ['description', 'examples', 'ref', 'params', 'returns']) {
|
|
737
|
+
const v = (src as any)[k];
|
|
738
|
+
if (typeof v !== 'undefined') out[k] = v;
|
|
739
|
+
}
|
|
740
|
+
if (Array.isArray(out.examples)) {
|
|
741
|
+
out.examples = out.examples.filter((x: any) => typeof x === 'string' && x.trim());
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const pickPropertyInfo = (obj: any): Partial<FlowContextApiInfo> => {
|
|
747
|
+
const src = toDocObject(obj);
|
|
748
|
+
if (!src) return {};
|
|
749
|
+
const out: any = {};
|
|
750
|
+
for (const k of ['title', 'type', 'interface', 'description', 'examples', 'ref', 'params', 'returns']) {
|
|
751
|
+
const v = (src as any)[k];
|
|
752
|
+
if (typeof v !== 'undefined') out[k] = v;
|
|
753
|
+
}
|
|
754
|
+
if (Array.isArray(out.examples)) {
|
|
755
|
+
out.examples = out.examples.filter((x: any) => typeof x === 'string' && x.trim());
|
|
756
|
+
}
|
|
757
|
+
return out;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const getMethodInfoFromChain = (name: string): FlowContextMethodInfoInput | undefined => {
|
|
761
|
+
const visited = new WeakSet<any>();
|
|
762
|
+
const walk = (ctx: FlowContext): FlowContextMethodInfoInput | undefined => {
|
|
763
|
+
if (!ctx || typeof ctx !== 'object') return undefined;
|
|
764
|
+
if (visited.has(ctx as any)) return undefined;
|
|
765
|
+
visited.add(ctx as any);
|
|
766
|
+
if (Object.prototype.hasOwnProperty.call((ctx as any)._methodInfos || {}, name)) {
|
|
767
|
+
return (ctx as any)._methodInfos?.[name] as FlowContextMethodInfoInput;
|
|
768
|
+
}
|
|
769
|
+
const delegates = (ctx as any)._delegates;
|
|
770
|
+
if (Array.isArray(delegates)) {
|
|
771
|
+
for (const d of delegates) {
|
|
772
|
+
const found = walk(d);
|
|
773
|
+
if (found) return found;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return undefined;
|
|
777
|
+
};
|
|
778
|
+
return walk(this);
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const resolvePropertyInfo = async (key: string): Promise<FlowContextPropertyInfoInput | undefined> => {
|
|
782
|
+
const opt = this.getPropertyOptions(key);
|
|
783
|
+
if (!opt?.info) return undefined;
|
|
784
|
+
try {
|
|
785
|
+
const v = typeof opt.info === 'function' ? (opt.info as any).call(evalCtx, evalCtx) : opt.info;
|
|
786
|
+
const resolved = isPromiseLike(v) ? await v : v;
|
|
787
|
+
return (resolved ?? undefined) as any;
|
|
788
|
+
} catch (_) {
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const propKeys = new Set<string>();
|
|
794
|
+
const methodKeys = new Set<string>();
|
|
795
|
+
for (const k of Object.keys(docProps)) propKeys.add(k);
|
|
796
|
+
for (const k of Object.keys(docMethods)) methodKeys.add(k);
|
|
797
|
+
|
|
798
|
+
const collectInfoKeysDeep = (ctx: FlowContext, visited: WeakSet<any>) => {
|
|
799
|
+
if (!ctx || typeof ctx !== 'object') return;
|
|
800
|
+
if (visited.has(ctx as any)) return;
|
|
801
|
+
visited.add(ctx as any);
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
const props = (ctx as any)._props;
|
|
805
|
+
if (props && typeof props === 'object') {
|
|
806
|
+
for (const [k, v] of Object.entries(props)) {
|
|
807
|
+
if ((v as any)?.info) propKeys.add(k);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} catch (_) {
|
|
811
|
+
// ignore
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
const mi = (ctx as any)._methodInfos;
|
|
816
|
+
if (mi && typeof mi === 'object') {
|
|
817
|
+
for (const k of Object.keys(mi)) methodKeys.add(k);
|
|
818
|
+
}
|
|
819
|
+
} catch (_) {
|
|
820
|
+
// ignore
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
try {
|
|
824
|
+
const delegates = (ctx as any)._delegates;
|
|
825
|
+
if (Array.isArray(delegates)) {
|
|
826
|
+
for (const d of delegates) collectInfoKeysDeep(d, visited);
|
|
827
|
+
}
|
|
828
|
+
} catch (_) {
|
|
829
|
+
// ignore
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
collectInfoKeysDeep(this, new WeakSet<any>());
|
|
833
|
+
|
|
834
|
+
const out: Record<string, FlowContextApiInfo> = {};
|
|
835
|
+
|
|
836
|
+
for (const key of propKeys) {
|
|
837
|
+
if (isPrivateKey(key)) continue;
|
|
838
|
+
if (isVarRootKey(key)) continue;
|
|
839
|
+
const docNode = docProps[key];
|
|
840
|
+
const infoNode = await resolvePropertyInfo(key);
|
|
841
|
+
if (typeof docNode === 'undefined' && typeof infoNode === 'undefined') continue;
|
|
842
|
+
|
|
843
|
+
const docObj = toDocObject(docNode);
|
|
844
|
+
const infoObj = toDocObject(infoNode);
|
|
845
|
+
let node: FlowContextApiInfo = {};
|
|
846
|
+
node = { ...node, ...pickPropertyInfo(docObj) };
|
|
847
|
+
node = { ...node, ...pickPropertyInfo(infoObj) };
|
|
848
|
+
delete (node as any).properties;
|
|
849
|
+
delete (node as any).completion;
|
|
850
|
+
if (!Object.keys(node).length) continue;
|
|
851
|
+
const outKey = mapDocKeyToApiKey(key, docNode);
|
|
852
|
+
// Avoid exposing ctx.React/ctx.ReactDOM/ctx.antd in api docs when mapping to ctx.libs.*.
|
|
853
|
+
out[outKey] = out[outKey] ? { ...(out[outKey] || {}), ...(node || {}) } : node;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
for (const key of methodKeys) {
|
|
857
|
+
if (isPrivateKey(key)) continue;
|
|
858
|
+
const docNode = docMethods[key];
|
|
859
|
+
const info = getMethodInfoFromChain(key);
|
|
860
|
+
if (typeof docNode === 'undefined' && typeof info === 'undefined') continue;
|
|
861
|
+
|
|
862
|
+
const docObj = toDocObject(docNode);
|
|
863
|
+
let node: FlowContextApiInfo = {};
|
|
864
|
+
node = { ...node, ...pickMethodInfo(docObj) };
|
|
865
|
+
node = { ...node, ...pickMethodInfo(info) };
|
|
866
|
+
delete (node as any).properties;
|
|
867
|
+
delete (node as any).completion;
|
|
868
|
+
if (!Object.keys(node).length) continue;
|
|
869
|
+
node.type = 'function';
|
|
870
|
+
|
|
871
|
+
if (!out[key]) out[key] = node;
|
|
872
|
+
else out[key] = { ...(out[key] || {}), ...(node || {}) };
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Flatten libs children (one-layer output, but allow `libs.xxx` keys).
|
|
876
|
+
// Prefer richer doc from root aliases (e.g. `React` mapped to `libs.React`) when available.
|
|
877
|
+
const libsDocObj = toDocObject(docProps.libs);
|
|
878
|
+
const libsChildren = __isPlainObject((libsDocObj as any)?.properties)
|
|
879
|
+
? ((libsDocObj as any).properties as any as Record<string, any>)
|
|
880
|
+
: undefined;
|
|
881
|
+
if (libsChildren) {
|
|
882
|
+
for (const [k, v] of Object.entries(libsChildren)) {
|
|
883
|
+
if (isPrivateKey(k)) continue;
|
|
884
|
+
const outKey = `libs.${k}`;
|
|
885
|
+
if (out[outKey]) continue;
|
|
886
|
+
const childObj = toDocObject(v);
|
|
887
|
+
let node: FlowContextApiInfo = {};
|
|
888
|
+
node = { ...node, ...pickPropertyInfo(childObj) };
|
|
889
|
+
delete (node as any).properties;
|
|
890
|
+
delete (node as any).completion;
|
|
891
|
+
if (!node.description || !String(node.description).trim()) continue;
|
|
892
|
+
out[outKey] = node;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return out;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* 获取运行时环境快照信息(小体积、可序列化)。
|
|
901
|
+
*/
|
|
902
|
+
async getEnvInfos(): Promise<FlowContextInfosEnvs> {
|
|
903
|
+
const evalCtx = this.createProxy();
|
|
904
|
+
|
|
905
|
+
const isPromiseLike = (v: any): v is Promise<any> =>
|
|
906
|
+
!!v && (typeof v === 'object' || typeof v === 'function') && typeof (v as any).then === 'function';
|
|
907
|
+
|
|
908
|
+
const envs: FlowContextInfosEnvs = {};
|
|
909
|
+
|
|
910
|
+
type ResourceSnapshotKey = 'dataSourceKey' | 'collectionName' | 'associationName' | 'filterByTk' | 'sourceId';
|
|
911
|
+
type ResourceSnapshot = Partial<Record<ResourceSnapshotKey, JSONValue>>;
|
|
912
|
+
type ResourceLike = ResourceSnapshot & {
|
|
913
|
+
getDataSourceKey?: () => JSONValue;
|
|
914
|
+
getFilterByTk?: () => JSONValue;
|
|
915
|
+
getSourceId?: () => JSONValue;
|
|
916
|
+
getResourceName?: () => string;
|
|
917
|
+
getMeta?: (key: string) => unknown;
|
|
918
|
+
};
|
|
919
|
+
type ModelCtorLike = { name?: string; meta?: { label?: string } };
|
|
920
|
+
type ModelLike = { uid?: string; title?: string; resource?: unknown; constructor?: ModelCtorLike };
|
|
921
|
+
type PopupLike = { uid?: string; resource?: unknown; record?: unknown; sourceRecord?: unknown; parent?: unknown };
|
|
922
|
+
|
|
923
|
+
const getMaybe = <T = any>(fn: () => T): T | undefined => {
|
|
924
|
+
try {
|
|
925
|
+
return fn();
|
|
926
|
+
} catch (_) {
|
|
927
|
+
return undefined;
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const hasSnapshotValue = <T>(v: T): v is Exclude<T, undefined | null> => {
|
|
932
|
+
if (typeof v === 'undefined' || v === null) return false;
|
|
933
|
+
if (typeof v === 'string') return v.trim().length > 0;
|
|
934
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
935
|
+
return true;
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const getResourceSnapshot = (res: unknown): ResourceSnapshot => {
|
|
939
|
+
const out: ResourceSnapshot = {};
|
|
940
|
+
if (!res) return out;
|
|
941
|
+
const r = res as ResourceLike;
|
|
942
|
+
|
|
943
|
+
// Direct fields (popup/view inputArgs style)
|
|
944
|
+
for (const k of [
|
|
945
|
+
'dataSourceKey',
|
|
946
|
+
'collectionName',
|
|
947
|
+
'associationName',
|
|
948
|
+
'filterByTk',
|
|
949
|
+
'sourceId',
|
|
950
|
+
] as ResourceSnapshotKey[]) {
|
|
951
|
+
const v = r?.[k];
|
|
952
|
+
if (hasSnapshotValue(v)) out[k] = v;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// FlowResource-like methods (BaseRecordResource/SQLResource)
|
|
956
|
+
if (!('dataSourceKey' in out)) {
|
|
957
|
+
const v = r.getDataSourceKey?.();
|
|
958
|
+
if (hasSnapshotValue(v)) out.dataSourceKey = v;
|
|
959
|
+
}
|
|
960
|
+
if (!('filterByTk' in out)) {
|
|
961
|
+
const v = r.getFilterByTk?.();
|
|
962
|
+
if (hasSnapshotValue(v)) out.filterByTk = v;
|
|
963
|
+
}
|
|
964
|
+
if (!('filterByTk' in out)) {
|
|
965
|
+
const v = r.getMeta?.('currentFilterByTk') as JSONValue | undefined;
|
|
966
|
+
if (hasSnapshotValue(v)) out.filterByTk = v;
|
|
967
|
+
}
|
|
968
|
+
if (!('sourceId' in out)) {
|
|
969
|
+
const v = r.getSourceId?.();
|
|
970
|
+
if (hasSnapshotValue(v)) out.sourceId = v;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Infer collection/association from resourceName when not provided
|
|
974
|
+
if (!('collectionName' in out) || !('associationName' in out)) {
|
|
975
|
+
const rn = r.getResourceName?.();
|
|
976
|
+
const resourceName = typeof rn === 'string' ? rn.trim() : '';
|
|
977
|
+
if (resourceName) {
|
|
978
|
+
const parts = resourceName
|
|
979
|
+
.split('.')
|
|
980
|
+
.map((x) => x.trim())
|
|
981
|
+
.filter(Boolean);
|
|
982
|
+
if (parts.length === 1) {
|
|
983
|
+
if (!('collectionName' in out)) out.collectionName = parts[0];
|
|
984
|
+
} else if (parts.length >= 2) {
|
|
985
|
+
if (!('collectionName' in out)) out.collectionName = parts[0];
|
|
986
|
+
if (!('associationName' in out)) out.associationName = parts.slice(1).join('.');
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return out;
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// Resolve popup (may be Promise)
|
|
995
|
+
const popup = await (async () => {
|
|
996
|
+
try {
|
|
997
|
+
const raw = (evalCtx as any).popup;
|
|
998
|
+
return isPromiseLike(raw) ? await raw : raw;
|
|
999
|
+
} catch (_) {
|
|
1000
|
+
return undefined;
|
|
1001
|
+
}
|
|
1002
|
+
})();
|
|
1003
|
+
|
|
1004
|
+
const popupLike = popup as PopupLike | undefined;
|
|
1005
|
+
const model = getMaybe(() => (evalCtx as any).model) as ModelLike | undefined;
|
|
1006
|
+
const blockModel = getMaybe(() => (evalCtx as any).blockModel) as ModelLike | undefined;
|
|
1007
|
+
const inputArgs = getMaybe(() => (evalCtx as any).view?.inputArgs) as
|
|
1008
|
+
| (ResourceLike & { viewUid?: string })
|
|
1009
|
+
| undefined;
|
|
1010
|
+
const ctxResource = getMaybe(() => (evalCtx as any).resource) as ResourceLike | undefined;
|
|
1011
|
+
|
|
1012
|
+
const popupResource = popupLike?.resource;
|
|
1013
|
+
const popupResourceSnap = getResourceSnapshot(popupResource);
|
|
1014
|
+
|
|
1015
|
+
const blockOwner = blockModel;
|
|
1016
|
+
const blockOwnerExpr = blockModel ? 'ctx.blockModel' : undefined;
|
|
1017
|
+
const blockResourceBaseExpr = blockOwnerExpr ? `${blockOwnerExpr}.resource` : undefined;
|
|
1018
|
+
const blockResource = blockOwner?.resource;
|
|
1019
|
+
const blockResourceSnap = getResourceSnapshot(blockResource);
|
|
1020
|
+
const inputArgsSnap = getResourceSnapshot(inputArgs);
|
|
1021
|
+
const ctxResourceSnap = getResourceSnapshot(ctxResource);
|
|
1022
|
+
|
|
1023
|
+
// Resource snapshot (for prompt)
|
|
1024
|
+
const pickWithGetVar = <T>(
|
|
1025
|
+
pairs: Array<{
|
|
1026
|
+
value: T | undefined;
|
|
1027
|
+
getVar: string;
|
|
1028
|
+
}>,
|
|
1029
|
+
): { value: T; getVar: string } | undefined => {
|
|
1030
|
+
for (const p of pairs) {
|
|
1031
|
+
if (hasSnapshotValue(p.value)) return { value: p.value, getVar: p.getVar };
|
|
1032
|
+
}
|
|
1033
|
+
return undefined;
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
const hasAnyResourceValuesIn = (snap: ResourceSnapshot): boolean =>
|
|
1037
|
+
hasSnapshotValue(snap.collectionName) ||
|
|
1038
|
+
hasSnapshotValue(snap.dataSourceKey) ||
|
|
1039
|
+
hasSnapshotValue(snap.associationName);
|
|
1040
|
+
|
|
1041
|
+
const resourceBaseExpr: string | undefined = hasAnyResourceValuesIn(popupResourceSnap)
|
|
1042
|
+
? 'ctx.popup.resource'
|
|
1043
|
+
: hasAnyResourceValuesIn(blockResourceSnap)
|
|
1044
|
+
? blockResourceBaseExpr
|
|
1045
|
+
: hasAnyResourceValuesIn(inputArgsSnap)
|
|
1046
|
+
? 'ctx.view.inputArgs'
|
|
1047
|
+
: hasAnyResourceValuesIn(ctxResourceSnap)
|
|
1048
|
+
? 'ctx.resource'
|
|
1049
|
+
: undefined;
|
|
1050
|
+
|
|
1051
|
+
const collectionNamePick = pickWithGetVar([
|
|
1052
|
+
{ value: popupResourceSnap?.collectionName, getVar: 'ctx.popup.resource.collectionName' },
|
|
1053
|
+
{ value: blockResourceSnap?.collectionName, getVar: `${blockResourceBaseExpr}.collectionName` },
|
|
1054
|
+
{ value: inputArgsSnap?.collectionName, getVar: 'ctx.view.inputArgs.collectionName' },
|
|
1055
|
+
{ value: ctxResourceSnap?.collectionName, getVar: 'ctx.resource.collectionName' },
|
|
1056
|
+
]);
|
|
1057
|
+
const dataSourceKeyPick = pickWithGetVar([
|
|
1058
|
+
{ value: popupResourceSnap?.dataSourceKey, getVar: 'ctx.popup.resource.dataSourceKey' },
|
|
1059
|
+
{ value: blockResourceSnap?.dataSourceKey, getVar: `${blockResourceBaseExpr}.dataSourceKey` },
|
|
1060
|
+
{ value: inputArgsSnap?.dataSourceKey, getVar: 'ctx.view.inputArgs.dataSourceKey' },
|
|
1061
|
+
{ value: ctxResourceSnap?.dataSourceKey, getVar: 'ctx.resource.dataSourceKey' },
|
|
1062
|
+
]);
|
|
1063
|
+
const associationNamePick = pickWithGetVar([
|
|
1064
|
+
{ value: popupResourceSnap?.associationName, getVar: 'ctx.popup.resource.associationName' },
|
|
1065
|
+
{ value: blockResourceSnap?.associationName, getVar: `${blockResourceBaseExpr}.associationName` },
|
|
1066
|
+
{ value: inputArgsSnap?.associationName, getVar: 'ctx.view.inputArgs.associationName' },
|
|
1067
|
+
{ value: ctxResourceSnap?.associationName, getVar: 'ctx.resource.associationName' },
|
|
1068
|
+
]);
|
|
1069
|
+
const filterByTkPick = pickWithGetVar([
|
|
1070
|
+
{ value: popupResourceSnap?.filterByTk, getVar: 'ctx.popup.resource.filterByTk' },
|
|
1071
|
+
{ value: blockResourceSnap?.filterByTk, getVar: `${blockResourceBaseExpr}.filterByTk` },
|
|
1072
|
+
{ value: inputArgsSnap?.filterByTk, getVar: 'ctx.view.inputArgs.filterByTk' },
|
|
1073
|
+
{ value: ctxResourceSnap?.filterByTk, getVar: 'ctx.resource.filterByTk' },
|
|
1074
|
+
]);
|
|
1075
|
+
const sourceIdPick = pickWithGetVar([
|
|
1076
|
+
{ value: popupResourceSnap?.sourceId, getVar: 'ctx.popup.resource.sourceId' },
|
|
1077
|
+
{ value: blockResourceSnap?.sourceId, getVar: `${blockResourceBaseExpr}.sourceId` },
|
|
1078
|
+
{ value: inputArgsSnap?.sourceId, getVar: 'ctx.view.inputArgs.sourceId' },
|
|
1079
|
+
{ value: ctxResourceSnap?.sourceId, getVar: 'ctx.resource.sourceId' },
|
|
1080
|
+
]);
|
|
1081
|
+
|
|
1082
|
+
const resourceProps: Record<string, FlowContextInfosEnvNode> = {};
|
|
1083
|
+
let hasResourceValues = false;
|
|
1084
|
+
const collectionNameValue = collectionNamePick?.value;
|
|
1085
|
+
if (hasSnapshotValue(collectionNameValue)) {
|
|
1086
|
+
resourceProps.collectionName = {
|
|
1087
|
+
description: 'Collection name',
|
|
1088
|
+
getVar: collectionNamePick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.collectionName` : undefined),
|
|
1089
|
+
value: collectionNameValue,
|
|
1090
|
+
};
|
|
1091
|
+
hasResourceValues = true;
|
|
1092
|
+
}
|
|
1093
|
+
const dataSourceKeyValue = dataSourceKeyPick?.value;
|
|
1094
|
+
if (hasSnapshotValue(dataSourceKeyValue)) {
|
|
1095
|
+
resourceProps.dataSourceKey = {
|
|
1096
|
+
description: 'Data source key',
|
|
1097
|
+
getVar: dataSourceKeyPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.dataSourceKey` : undefined),
|
|
1098
|
+
value: dataSourceKeyValue,
|
|
1099
|
+
};
|
|
1100
|
+
hasResourceValues = true;
|
|
1101
|
+
}
|
|
1102
|
+
const associationNameValue = associationNamePick?.value;
|
|
1103
|
+
if (hasSnapshotValue(associationNameValue)) {
|
|
1104
|
+
resourceProps.associationName = {
|
|
1105
|
+
description: 'Association name',
|
|
1106
|
+
getVar: associationNamePick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.associationName` : undefined),
|
|
1107
|
+
value: associationNameValue,
|
|
1108
|
+
};
|
|
1109
|
+
hasResourceValues = true;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Only include envs.resource when snapshot contains at least one resource value.
|
|
1113
|
+
// Optional fields like filterByTk/sourceId are included (without value) only when envs.resource exists.
|
|
1114
|
+
if (hasResourceValues) {
|
|
1115
|
+
if (hasSnapshotValue(filterByTkPick?.value)) {
|
|
1116
|
+
resourceProps.filterByTk = {
|
|
1117
|
+
description: 'Record filterByTk',
|
|
1118
|
+
getVar: filterByTkPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.filterByTk` : undefined),
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
if (hasSnapshotValue(sourceIdPick?.value)) {
|
|
1122
|
+
resourceProps.sourceId = {
|
|
1123
|
+
description: 'Source record ID (sourceId)',
|
|
1124
|
+
getVar: sourceIdPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.sourceId` : undefined),
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
envs.resource = {
|
|
1129
|
+
description: 'Resource information',
|
|
1130
|
+
getVar: resourceBaseExpr,
|
|
1131
|
+
properties: resourceProps,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Record (only when filterByTk is available)
|
|
1136
|
+
if (hasSnapshotValue(filterByTkPick?.value)) {
|
|
1137
|
+
envs.record = {
|
|
1138
|
+
description: 'Current record',
|
|
1139
|
+
getVar: 'ctx.record',
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const pickLabel = (obj: ModelLike | null | undefined): string | undefined => {
|
|
1144
|
+
try {
|
|
1145
|
+
const t = obj?.title;
|
|
1146
|
+
if (typeof t === 'string' && t.trim()) return t;
|
|
1147
|
+
} catch (_) {
|
|
1148
|
+
// ignore
|
|
1149
|
+
}
|
|
1150
|
+
try {
|
|
1151
|
+
const label = obj?.constructor?.meta?.label;
|
|
1152
|
+
if (typeof label === 'string' && label.trim()) return label;
|
|
1153
|
+
} catch (_) {
|
|
1154
|
+
// ignore
|
|
1155
|
+
}
|
|
1156
|
+
return undefined;
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
// FlowModel (when ctx.model exists)
|
|
1160
|
+
if (model) {
|
|
1161
|
+
const modelLabel = pickLabel(model);
|
|
1162
|
+
const modelUid = model.uid;
|
|
1163
|
+
const modelClassName = model.constructor?.name;
|
|
1164
|
+
const modelResourceSnap = getResourceSnapshot(model.resource);
|
|
1165
|
+
const modelResourceProps: Record<string, FlowContextInfosEnvNode> = {};
|
|
1166
|
+
let hasModelResourceValues = false;
|
|
1167
|
+
const modelCollectionName = modelResourceSnap.collectionName;
|
|
1168
|
+
if (hasSnapshotValue(modelCollectionName)) {
|
|
1169
|
+
modelResourceProps.collectionName = {
|
|
1170
|
+
description: 'Collection name',
|
|
1171
|
+
getVar: 'ctx.model.resource.collectionName',
|
|
1172
|
+
value: modelCollectionName,
|
|
1173
|
+
};
|
|
1174
|
+
hasModelResourceValues = true;
|
|
1175
|
+
}
|
|
1176
|
+
const modelDataSourceKey = modelResourceSnap.dataSourceKey;
|
|
1177
|
+
if (hasSnapshotValue(modelDataSourceKey)) {
|
|
1178
|
+
modelResourceProps.dataSourceKey = {
|
|
1179
|
+
description: 'Data source key',
|
|
1180
|
+
getVar: 'ctx.model.resource.dataSourceKey',
|
|
1181
|
+
value: modelDataSourceKey,
|
|
1182
|
+
};
|
|
1183
|
+
hasModelResourceValues = true;
|
|
1184
|
+
}
|
|
1185
|
+
const modelAssociationName = modelResourceSnap.associationName;
|
|
1186
|
+
if (hasSnapshotValue(modelAssociationName)) {
|
|
1187
|
+
modelResourceProps.associationName = {
|
|
1188
|
+
description: 'Association name',
|
|
1189
|
+
getVar: 'ctx.model.resource.associationName',
|
|
1190
|
+
value: modelAssociationName,
|
|
1191
|
+
};
|
|
1192
|
+
hasModelResourceValues = true;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
envs.flowModel = {
|
|
1196
|
+
description: 'Current FlowModel information',
|
|
1197
|
+
getVar: 'ctx.model',
|
|
1198
|
+
properties: {
|
|
1199
|
+
...(hasSnapshotValue(modelLabel) ? { label: { description: 'Flow model label', value: modelLabel } } : {}),
|
|
1200
|
+
...(hasSnapshotValue(modelClassName)
|
|
1201
|
+
? {
|
|
1202
|
+
modelClass: {
|
|
1203
|
+
description: 'Flow model class name',
|
|
1204
|
+
value: modelClassName,
|
|
1205
|
+
},
|
|
1206
|
+
}
|
|
1207
|
+
: {}),
|
|
1208
|
+
...(hasSnapshotValue(modelUid)
|
|
1209
|
+
? { uid: { description: 'Flow model uid', getVar: 'ctx.model.uid', value: modelUid } }
|
|
1210
|
+
: {}),
|
|
1211
|
+
...(hasModelResourceValues
|
|
1212
|
+
? {
|
|
1213
|
+
resource: {
|
|
1214
|
+
description: 'Flow model resource',
|
|
1215
|
+
getVar: 'ctx.model.resource',
|
|
1216
|
+
properties: modelResourceProps,
|
|
1217
|
+
},
|
|
1218
|
+
}
|
|
1219
|
+
: {}),
|
|
1220
|
+
},
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// popup info (when ctx.popup exists)
|
|
1225
|
+
if (popupLike?.uid) {
|
|
1226
|
+
envs.popup = {
|
|
1227
|
+
description: 'Current popup information',
|
|
1228
|
+
getVar: 'ctx.popup',
|
|
1229
|
+
properties: {
|
|
1230
|
+
uid: { description: 'Popup uid', getVar: 'ctx.popup.uid', value: popupLike.uid },
|
|
1231
|
+
...(popupLike?.record ? { record: { description: 'Popup record', getVar: 'ctx.popup.record' } } : {}),
|
|
1232
|
+
...(popupLike?.sourceRecord
|
|
1233
|
+
? { sourceRecord: { description: 'Popup source record', getVar: 'ctx.popup.sourceRecord' } }
|
|
1234
|
+
: {}),
|
|
1235
|
+
},
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// block (when ctx.blockModel exists)
|
|
1240
|
+
if (blockOwner) {
|
|
1241
|
+
const blockLabel = pickLabel(blockOwner);
|
|
1242
|
+
const blockUid = blockOwner.uid;
|
|
1243
|
+
const blockModelClass = blockOwner.constructor?.name;
|
|
1244
|
+
const blockResourceProps: Record<string, FlowContextInfosEnvNode> = {};
|
|
1245
|
+
let hasBlockResourceValues = false;
|
|
1246
|
+
const blockCollectionName = blockResourceSnap.collectionName;
|
|
1247
|
+
if (hasSnapshotValue(blockCollectionName)) {
|
|
1248
|
+
blockResourceProps.collectionName = {
|
|
1249
|
+
description: 'Collection name',
|
|
1250
|
+
getVar: `${blockResourceBaseExpr}.collectionName`,
|
|
1251
|
+
value: blockCollectionName,
|
|
1252
|
+
};
|
|
1253
|
+
hasBlockResourceValues = true;
|
|
1254
|
+
}
|
|
1255
|
+
const blockDataSourceKey = blockResourceSnap.dataSourceKey;
|
|
1256
|
+
if (hasSnapshotValue(blockDataSourceKey)) {
|
|
1257
|
+
blockResourceProps.dataSourceKey = {
|
|
1258
|
+
description: 'Data source key',
|
|
1259
|
+
getVar: `${blockResourceBaseExpr}.dataSourceKey`,
|
|
1260
|
+
value: blockDataSourceKey,
|
|
1261
|
+
};
|
|
1262
|
+
hasBlockResourceValues = true;
|
|
1263
|
+
}
|
|
1264
|
+
const blockAssociationName = blockResourceSnap.associationName;
|
|
1265
|
+
if (hasSnapshotValue(blockAssociationName)) {
|
|
1266
|
+
blockResourceProps.associationName = {
|
|
1267
|
+
description: 'Association name',
|
|
1268
|
+
getVar: `${blockResourceBaseExpr}.associationName`,
|
|
1269
|
+
value: blockAssociationName,
|
|
1270
|
+
};
|
|
1271
|
+
hasBlockResourceValues = true;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
envs.block = {
|
|
1275
|
+
description: 'Current block information',
|
|
1276
|
+
getVar: blockOwnerExpr,
|
|
1277
|
+
properties: {
|
|
1278
|
+
...(hasSnapshotValue(blockLabel) ? { label: { description: 'Block label', value: blockLabel } } : {}),
|
|
1279
|
+
...(hasSnapshotValue(blockModelClass)
|
|
1280
|
+
? { modelClass: { description: 'Block model class name', value: blockModelClass } }
|
|
1281
|
+
: {}),
|
|
1282
|
+
...(hasSnapshotValue(blockUid)
|
|
1283
|
+
? { uid: { description: 'Block uid', getVar: `${blockOwnerExpr}.uid`, value: blockUid } }
|
|
1284
|
+
: {}),
|
|
1285
|
+
...(hasBlockResourceValues
|
|
1286
|
+
? {
|
|
1287
|
+
resource: {
|
|
1288
|
+
description: 'Block resource',
|
|
1289
|
+
getVar: blockResourceBaseExpr,
|
|
1290
|
+
properties: blockResourceProps,
|
|
1291
|
+
},
|
|
1292
|
+
}
|
|
1293
|
+
: {}),
|
|
1294
|
+
},
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Current view blocks snapshot (page view or current popup view)
|
|
1299
|
+
const viewUid = (() => {
|
|
1300
|
+
const popupUid = popupLike?.uid;
|
|
1301
|
+
if (hasSnapshotValue(popupUid)) return String(popupUid).trim();
|
|
1302
|
+
const v = (inputArgs as any)?.viewUid;
|
|
1303
|
+
if (hasSnapshotValue(v)) return String(v).trim();
|
|
1304
|
+
return undefined;
|
|
1305
|
+
})();
|
|
1306
|
+
|
|
1307
|
+
const engine = getMaybe(() => (evalCtx as any).engine) as FlowEngine | undefined;
|
|
1308
|
+
const viewModel = viewUid ? engine?.getModel(viewUid, true) : undefined;
|
|
1309
|
+
|
|
1310
|
+
type ViewTreeNode = {
|
|
1311
|
+
uid: string;
|
|
1312
|
+
subModels?: Record<string, unknown>;
|
|
1313
|
+
context?: { blockModel?: unknown };
|
|
1314
|
+
resource?: unknown;
|
|
1315
|
+
constructor?: { name?: string };
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
const isBlockModelInstance = (m: ViewTreeNode): boolean => m.context?.blockModel === m;
|
|
1319
|
+
|
|
1320
|
+
if (viewModel) {
|
|
1321
|
+
const queue: ViewTreeNode[] = [viewModel as unknown as ViewTreeNode];
|
|
1322
|
+
const blocks: Array<Record<string, JSONValue>> = [];
|
|
1323
|
+
|
|
1324
|
+
for (let i = 0; i < queue.length; i++) {
|
|
1325
|
+
const m = queue[i];
|
|
1326
|
+
|
|
1327
|
+
if (isBlockModelInstance(m)) {
|
|
1328
|
+
const modelClass = m.constructor?.name || m.uid;
|
|
1329
|
+
const label = pickLabel(m as any) || modelClass || m.uid;
|
|
1330
|
+
|
|
1331
|
+
const resSnap = getResourceSnapshot(m.resource);
|
|
1332
|
+
const resource: Record<string, JSONValue> = {};
|
|
1333
|
+
if (hasSnapshotValue(resSnap.dataSourceKey)) resource.dataSourceKey = resSnap.dataSourceKey;
|
|
1334
|
+
if (hasSnapshotValue(resSnap.collectionName)) resource.collectionName = resSnap.collectionName;
|
|
1335
|
+
if (hasSnapshotValue(resSnap.associationName)) resource.associationName = resSnap.associationName;
|
|
1336
|
+
|
|
1337
|
+
const block: Record<string, JSONValue> = {
|
|
1338
|
+
uid: m.uid,
|
|
1339
|
+
label,
|
|
1340
|
+
modelClass,
|
|
1341
|
+
...(Object.keys(resource).length > 0 ? { resource } : {}),
|
|
1342
|
+
};
|
|
1343
|
+
blocks.push(block);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const subModels = m.subModels;
|
|
1347
|
+
if (subModels && typeof subModels === 'object') {
|
|
1348
|
+
for (const v of Object.values(subModels)) {
|
|
1349
|
+
if (!v) continue;
|
|
1350
|
+
if (Array.isArray(v)) queue.push(...(v as ViewTreeNode[]));
|
|
1351
|
+
else queue.push(v as ViewTreeNode);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
if (blocks.length) {
|
|
1357
|
+
envs.currentViewBlocks = {
|
|
1358
|
+
description: 'Current view blocks',
|
|
1359
|
+
value: blocks,
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
return envs;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* 获取变量结构信息(来源于 PropertyMeta)。
|
|
1369
|
+
*
|
|
1370
|
+
* - 返回静态 plain object(不包含函数)
|
|
1371
|
+
* - 支持 maxDepth(默认 3)与 path 剪裁
|
|
1372
|
+
*/
|
|
1373
|
+
async getVarInfos(options: FlowContextGetVarInfosOptions = {}): Promise<Record<string, FlowContextApiInfo>> {
|
|
1374
|
+
const maxDepthRaw = options.maxDepth ?? 3;
|
|
1375
|
+
const maxDepth = Number.isFinite(maxDepthRaw) ? Math.max(1, Math.floor(maxDepthRaw)) : 3;
|
|
1376
|
+
const version = 'v1' as RunJSVersion;
|
|
1377
|
+
const evalCtx = this.createProxy();
|
|
1378
|
+
|
|
1379
|
+
const isPrivateKey = (key: string) => typeof key === 'string' && key.startsWith('_');
|
|
1380
|
+
|
|
1381
|
+
const isPromiseLike = (v: any): v is Promise<any> =>
|
|
1382
|
+
!!v && (typeof v === 'object' || typeof v === 'function') && typeof (v as any).then === 'function';
|
|
1383
|
+
|
|
1384
|
+
// Per-call cache for resolved PropertyMetaFactory nodes to avoid repeated async calls.
|
|
1385
|
+
const metaFactoryCache = new WeakMap<Function, Promise<PropertyMeta | null>>();
|
|
1386
|
+
const resolveMetaOrFactory = async (meta?: PropertyMetaOrFactory): Promise<PropertyMeta | undefined> => {
|
|
1387
|
+
if (!meta) return undefined;
|
|
1388
|
+
if (typeof meta !== 'function') return meta;
|
|
1389
|
+
let pending = metaFactoryCache.get(meta);
|
|
1390
|
+
if (!pending) {
|
|
1391
|
+
pending = (async () => {
|
|
1392
|
+
const v = (meta as any).call(evalCtx, evalCtx);
|
|
1393
|
+
const resolved = isPromiseLike(v) ? await v : v;
|
|
1394
|
+
return resolved || null;
|
|
1395
|
+
})();
|
|
1396
|
+
metaFactoryCache.set(meta, pending);
|
|
1397
|
+
}
|
|
1398
|
+
const resolved = await pending;
|
|
1399
|
+
return resolved || undefined;
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
const buildEnvs = async (): Promise<FlowContextInfosEnvs> => {
|
|
1403
|
+
const envs: FlowContextInfosEnvs = {};
|
|
1404
|
+
|
|
1405
|
+
type ResourceSnapshotKey = 'dataSourceKey' | 'collectionName' | 'associationName' | 'filterByTk' | 'sourceId';
|
|
1406
|
+
type ResourceSnapshot = Partial<Record<ResourceSnapshotKey, JSONValue>>;
|
|
1407
|
+
type ResourceLike = ResourceSnapshot & {
|
|
1408
|
+
getDataSourceKey?: () => JSONValue;
|
|
1409
|
+
getFilterByTk?: () => JSONValue;
|
|
1410
|
+
getSourceId?: () => JSONValue;
|
|
1411
|
+
getResourceName?: () => string;
|
|
1412
|
+
getMeta?: (key: string) => unknown;
|
|
1413
|
+
};
|
|
1414
|
+
type ModelCtorLike = { name?: string; meta?: { label?: string } };
|
|
1415
|
+
type ModelLike = { uid?: string; title?: string; resource?: unknown; constructor?: ModelCtorLike };
|
|
1416
|
+
type PopupLike = { uid?: string; resource?: unknown; record?: unknown; sourceRecord?: unknown; parent?: unknown };
|
|
1417
|
+
|
|
1418
|
+
const getMaybe = <T = any>(fn: () => T): T | undefined => {
|
|
1419
|
+
try {
|
|
1420
|
+
return fn();
|
|
1421
|
+
} catch (_) {
|
|
1422
|
+
return undefined;
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
const hasSnapshotValue = <T>(v: T): v is Exclude<T, undefined | null> => {
|
|
1427
|
+
if (typeof v === 'undefined' || v === null) return false;
|
|
1428
|
+
if (typeof v === 'string') return v.trim().length > 0;
|
|
1429
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
1430
|
+
return true;
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
const getResourceSnapshot = (res: unknown): ResourceSnapshot => {
|
|
1434
|
+
const out: ResourceSnapshot = {};
|
|
1435
|
+
if (!res) return out;
|
|
1436
|
+
const r = res as ResourceLike;
|
|
1437
|
+
|
|
1438
|
+
// Direct fields (popup/view inputArgs style)
|
|
1439
|
+
for (const k of [
|
|
1440
|
+
'dataSourceKey',
|
|
1441
|
+
'collectionName',
|
|
1442
|
+
'associationName',
|
|
1443
|
+
'filterByTk',
|
|
1444
|
+
'sourceId',
|
|
1445
|
+
] as ResourceSnapshotKey[]) {
|
|
1446
|
+
const v = r?.[k];
|
|
1447
|
+
if (hasSnapshotValue(v)) out[k] = v;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// FlowResource-like methods (BaseRecordResource/SQLResource)
|
|
1451
|
+
if (!('dataSourceKey' in out)) {
|
|
1452
|
+
const v = r.getDataSourceKey?.();
|
|
1453
|
+
if (hasSnapshotValue(v)) out.dataSourceKey = v;
|
|
1454
|
+
}
|
|
1455
|
+
if (!('filterByTk' in out)) {
|
|
1456
|
+
const v = r.getFilterByTk?.();
|
|
1457
|
+
if (hasSnapshotValue(v)) out.filterByTk = v;
|
|
1458
|
+
}
|
|
1459
|
+
if (!('filterByTk' in out)) {
|
|
1460
|
+
const v = r.getMeta?.('currentFilterByTk') as JSONValue | undefined;
|
|
1461
|
+
if (hasSnapshotValue(v)) out.filterByTk = v;
|
|
1462
|
+
}
|
|
1463
|
+
if (!('sourceId' in out)) {
|
|
1464
|
+
const v = r.getSourceId?.();
|
|
1465
|
+
if (hasSnapshotValue(v)) out.sourceId = v;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Infer collection/association from resourceName when not provided
|
|
1469
|
+
if (!('collectionName' in out) || !('associationName' in out)) {
|
|
1470
|
+
const rn = r.getResourceName?.();
|
|
1471
|
+
const resourceName = typeof rn === 'string' ? rn.trim() : '';
|
|
1472
|
+
if (resourceName) {
|
|
1473
|
+
const parts = resourceName
|
|
1474
|
+
.split('.')
|
|
1475
|
+
.map((x) => x.trim())
|
|
1476
|
+
.filter(Boolean);
|
|
1477
|
+
if (parts.length === 1) {
|
|
1478
|
+
if (!('collectionName' in out)) out.collectionName = parts[0];
|
|
1479
|
+
} else if (parts.length >= 2) {
|
|
1480
|
+
if (!('collectionName' in out)) out.collectionName = parts[0];
|
|
1481
|
+
if (!('associationName' in out)) out.associationName = parts.slice(1).join('.');
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
return out;
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
// Resolve popup (may be Promise)
|
|
1490
|
+
const popup = await (async () => {
|
|
1491
|
+
try {
|
|
1492
|
+
const raw = (evalCtx as any).popup;
|
|
1493
|
+
return isPromiseLike(raw) ? await raw : raw;
|
|
1494
|
+
} catch (_) {
|
|
1495
|
+
return undefined;
|
|
1496
|
+
}
|
|
1497
|
+
})();
|
|
1498
|
+
|
|
1499
|
+
const popupLike = popup as PopupLike | undefined;
|
|
1500
|
+
const model = getMaybe(() => (evalCtx as any).model) as ModelLike | undefined;
|
|
1501
|
+
const blockModel = getMaybe(() => (evalCtx as any).blockModel) as ModelLike | undefined;
|
|
1502
|
+
const inputArgs = getMaybe(() => (evalCtx as any).view?.inputArgs) as
|
|
1503
|
+
| (ResourceLike & { viewUid?: string })
|
|
1504
|
+
| undefined;
|
|
1505
|
+
const ctxResource = getMaybe(() => (evalCtx as any).resource) as ResourceLike | undefined;
|
|
1506
|
+
|
|
1507
|
+
const popupResource = popupLike?.resource;
|
|
1508
|
+
const popupResourceSnap = getResourceSnapshot(popupResource);
|
|
1509
|
+
|
|
1510
|
+
const blockOwner = blockModel;
|
|
1511
|
+
const blockOwnerExpr = blockModel ? 'ctx.blockModel' : undefined;
|
|
1512
|
+
const blockResourceBaseExpr = blockOwnerExpr ? `${blockOwnerExpr}.resource` : undefined;
|
|
1513
|
+
const blockResource = blockOwner?.resource;
|
|
1514
|
+
const blockResourceSnap = getResourceSnapshot(blockResource);
|
|
1515
|
+
const inputArgsSnap = getResourceSnapshot(inputArgs);
|
|
1516
|
+
const ctxResourceSnap = getResourceSnapshot(ctxResource);
|
|
1517
|
+
|
|
1518
|
+
// Resource snapshot (for prompt)
|
|
1519
|
+
const pickWithGetVar = <T>(
|
|
1520
|
+
pairs: Array<{
|
|
1521
|
+
value: T | undefined;
|
|
1522
|
+
getVar: string;
|
|
1523
|
+
}>,
|
|
1524
|
+
): { value: T; getVar: string } | undefined => {
|
|
1525
|
+
for (const p of pairs) {
|
|
1526
|
+
if (hasSnapshotValue(p.value)) return { value: p.value, getVar: p.getVar };
|
|
1527
|
+
}
|
|
1528
|
+
return undefined;
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
const hasAnyResourceValuesIn = (snap: ResourceSnapshot): boolean =>
|
|
1532
|
+
hasSnapshotValue(snap.collectionName) ||
|
|
1533
|
+
hasSnapshotValue(snap.dataSourceKey) ||
|
|
1534
|
+
hasSnapshotValue(snap.associationName);
|
|
1535
|
+
|
|
1536
|
+
const resourceBaseExpr: string | undefined = hasAnyResourceValuesIn(popupResourceSnap)
|
|
1537
|
+
? 'ctx.popup.resource'
|
|
1538
|
+
: hasAnyResourceValuesIn(blockResourceSnap)
|
|
1539
|
+
? blockResourceBaseExpr
|
|
1540
|
+
: hasAnyResourceValuesIn(inputArgsSnap)
|
|
1541
|
+
? 'ctx.view.inputArgs'
|
|
1542
|
+
: hasAnyResourceValuesIn(ctxResourceSnap)
|
|
1543
|
+
? 'ctx.resource'
|
|
1544
|
+
: undefined;
|
|
1545
|
+
|
|
1546
|
+
const collectionNamePick = pickWithGetVar([
|
|
1547
|
+
{ value: popupResourceSnap?.collectionName, getVar: 'ctx.popup.resource.collectionName' },
|
|
1548
|
+
{ value: blockResourceSnap?.collectionName, getVar: `${blockResourceBaseExpr}.collectionName` },
|
|
1549
|
+
{ value: inputArgsSnap?.collectionName, getVar: 'ctx.view.inputArgs.collectionName' },
|
|
1550
|
+
{ value: ctxResourceSnap?.collectionName, getVar: 'ctx.resource.collectionName' },
|
|
1551
|
+
]);
|
|
1552
|
+
const dataSourceKeyPick = pickWithGetVar([
|
|
1553
|
+
{ value: popupResourceSnap?.dataSourceKey, getVar: 'ctx.popup.resource.dataSourceKey' },
|
|
1554
|
+
{ value: blockResourceSnap?.dataSourceKey, getVar: `${blockResourceBaseExpr}.dataSourceKey` },
|
|
1555
|
+
{ value: inputArgsSnap?.dataSourceKey, getVar: 'ctx.view.inputArgs.dataSourceKey' },
|
|
1556
|
+
{ value: ctxResourceSnap?.dataSourceKey, getVar: 'ctx.resource.dataSourceKey' },
|
|
1557
|
+
]);
|
|
1558
|
+
const associationNamePick = pickWithGetVar([
|
|
1559
|
+
{ value: popupResourceSnap?.associationName, getVar: 'ctx.popup.resource.associationName' },
|
|
1560
|
+
{ value: blockResourceSnap?.associationName, getVar: `${blockResourceBaseExpr}.associationName` },
|
|
1561
|
+
{ value: inputArgsSnap?.associationName, getVar: 'ctx.view.inputArgs.associationName' },
|
|
1562
|
+
{ value: ctxResourceSnap?.associationName, getVar: 'ctx.resource.associationName' },
|
|
1563
|
+
]);
|
|
1564
|
+
const filterByTkPick = pickWithGetVar([
|
|
1565
|
+
{ value: popupResourceSnap?.filterByTk, getVar: 'ctx.popup.resource.filterByTk' },
|
|
1566
|
+
{ value: blockResourceSnap?.filterByTk, getVar: `${blockResourceBaseExpr}.filterByTk` },
|
|
1567
|
+
{ value: inputArgsSnap?.filterByTk, getVar: 'ctx.view.inputArgs.filterByTk' },
|
|
1568
|
+
{ value: ctxResourceSnap?.filterByTk, getVar: 'ctx.resource.filterByTk' },
|
|
1569
|
+
]);
|
|
1570
|
+
const sourceIdPick = pickWithGetVar([
|
|
1571
|
+
{ value: popupResourceSnap?.sourceId, getVar: 'ctx.popup.resource.sourceId' },
|
|
1572
|
+
{ value: blockResourceSnap?.sourceId, getVar: `${blockResourceBaseExpr}.sourceId` },
|
|
1573
|
+
{ value: inputArgsSnap?.sourceId, getVar: 'ctx.view.inputArgs.sourceId' },
|
|
1574
|
+
{ value: ctxResourceSnap?.sourceId, getVar: 'ctx.resource.sourceId' },
|
|
1575
|
+
]);
|
|
1576
|
+
|
|
1577
|
+
const resourceProps: Record<string, FlowContextInfosEnvNode> = {};
|
|
1578
|
+
let hasResourceValues = false;
|
|
1579
|
+
const collectionNameValue = collectionNamePick?.value;
|
|
1580
|
+
if (hasSnapshotValue(collectionNameValue)) {
|
|
1581
|
+
resourceProps.collectionName = {
|
|
1582
|
+
description: 'Collection name',
|
|
1583
|
+
getVar: collectionNamePick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.collectionName` : undefined),
|
|
1584
|
+
value: collectionNameValue,
|
|
1585
|
+
};
|
|
1586
|
+
hasResourceValues = true;
|
|
1587
|
+
}
|
|
1588
|
+
const dataSourceKeyValue = dataSourceKeyPick?.value;
|
|
1589
|
+
if (hasSnapshotValue(dataSourceKeyValue)) {
|
|
1590
|
+
resourceProps.dataSourceKey = {
|
|
1591
|
+
description: 'Data source key',
|
|
1592
|
+
getVar: dataSourceKeyPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.dataSourceKey` : undefined),
|
|
1593
|
+
value: dataSourceKeyValue,
|
|
1594
|
+
};
|
|
1595
|
+
hasResourceValues = true;
|
|
1596
|
+
}
|
|
1597
|
+
const associationNameValue = associationNamePick?.value;
|
|
1598
|
+
if (hasSnapshotValue(associationNameValue)) {
|
|
1599
|
+
resourceProps.associationName = {
|
|
1600
|
+
description: 'Association name',
|
|
1601
|
+
getVar: associationNamePick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.associationName` : undefined),
|
|
1602
|
+
value: associationNameValue,
|
|
1603
|
+
};
|
|
1604
|
+
hasResourceValues = true;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Only include envs.resource when snapshot contains at least one resource value.
|
|
1608
|
+
// Optional fields like filterByTk/sourceId are included (without value) only when envs.resource exists.
|
|
1609
|
+
if (hasResourceValues) {
|
|
1610
|
+
if (hasSnapshotValue(filterByTkPick?.value)) {
|
|
1611
|
+
resourceProps.filterByTk = {
|
|
1612
|
+
description: 'Record filterByTk',
|
|
1613
|
+
getVar: filterByTkPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.filterByTk` : undefined),
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
if (hasSnapshotValue(sourceIdPick?.value)) {
|
|
1617
|
+
resourceProps.sourceId = {
|
|
1618
|
+
description: 'Source record ID (sourceId)',
|
|
1619
|
+
getVar: sourceIdPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.sourceId` : undefined),
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
envs.resource = {
|
|
1624
|
+
description: 'Resource information',
|
|
1625
|
+
getVar: resourceBaseExpr,
|
|
1626
|
+
properties: resourceProps,
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Record (only when filterByTk is available)
|
|
1631
|
+
if (hasSnapshotValue(filterByTkPick?.value)) {
|
|
1632
|
+
envs.record = {
|
|
1633
|
+
description: 'Current record',
|
|
1634
|
+
getVar: 'ctx.record',
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
const pickLabel = (obj: ModelLike | null | undefined): string | undefined => {
|
|
1639
|
+
try {
|
|
1640
|
+
const t = obj?.title;
|
|
1641
|
+
if (typeof t === 'string' && t.trim()) return t;
|
|
1642
|
+
} catch (_) {
|
|
1643
|
+
// ignore
|
|
1644
|
+
}
|
|
1645
|
+
try {
|
|
1646
|
+
const label = obj?.constructor?.meta?.label;
|
|
1647
|
+
if (typeof label === 'string' && label.trim()) return label;
|
|
1648
|
+
} catch (_) {
|
|
1649
|
+
// ignore
|
|
1650
|
+
}
|
|
1651
|
+
return undefined;
|
|
1652
|
+
};
|
|
1653
|
+
|
|
1654
|
+
// FlowModel (when ctx.model exists)
|
|
1655
|
+
if (model) {
|
|
1656
|
+
const modelLabel = pickLabel(model);
|
|
1657
|
+
const modelUid = model.uid;
|
|
1658
|
+
const modelClassName = model.constructor?.name;
|
|
1659
|
+
const modelResourceSnap = getResourceSnapshot(model.resource);
|
|
1660
|
+
const modelResourceProps: Record<string, FlowContextInfosEnvNode> = {};
|
|
1661
|
+
let hasModelResourceValues = false;
|
|
1662
|
+
const modelCollectionName = modelResourceSnap.collectionName;
|
|
1663
|
+
if (hasSnapshotValue(modelCollectionName)) {
|
|
1664
|
+
modelResourceProps.collectionName = {
|
|
1665
|
+
description: 'Collection name',
|
|
1666
|
+
getVar: 'ctx.model.resource.collectionName',
|
|
1667
|
+
value: modelCollectionName,
|
|
1668
|
+
};
|
|
1669
|
+
hasModelResourceValues = true;
|
|
1670
|
+
}
|
|
1671
|
+
const modelDataSourceKey = modelResourceSnap.dataSourceKey;
|
|
1672
|
+
if (hasSnapshotValue(modelDataSourceKey)) {
|
|
1673
|
+
modelResourceProps.dataSourceKey = {
|
|
1674
|
+
description: 'Data source key',
|
|
1675
|
+
getVar: 'ctx.model.resource.dataSourceKey',
|
|
1676
|
+
value: modelDataSourceKey,
|
|
1677
|
+
};
|
|
1678
|
+
hasModelResourceValues = true;
|
|
1679
|
+
}
|
|
1680
|
+
const modelAssociationName = modelResourceSnap.associationName;
|
|
1681
|
+
if (hasSnapshotValue(modelAssociationName)) {
|
|
1682
|
+
modelResourceProps.associationName = {
|
|
1683
|
+
description: 'Association name',
|
|
1684
|
+
getVar: 'ctx.model.resource.associationName',
|
|
1685
|
+
value: modelAssociationName,
|
|
1686
|
+
};
|
|
1687
|
+
hasModelResourceValues = true;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
envs.flowModel = {
|
|
1691
|
+
description: 'Current FlowModel information',
|
|
1692
|
+
getVar: 'ctx.model',
|
|
1693
|
+
properties: {
|
|
1694
|
+
...(hasSnapshotValue(modelLabel) ? { label: { description: 'Flow model label', value: modelLabel } } : {}),
|
|
1695
|
+
...(hasSnapshotValue(modelClassName)
|
|
1696
|
+
? {
|
|
1697
|
+
modelClass: {
|
|
1698
|
+
description: 'Flow model class name',
|
|
1699
|
+
value: modelClassName,
|
|
1700
|
+
},
|
|
1701
|
+
}
|
|
1702
|
+
: {}),
|
|
1703
|
+
...(hasSnapshotValue(modelUid)
|
|
1704
|
+
? { uid: { description: 'Flow model uid', getVar: 'ctx.model.uid', value: modelUid } }
|
|
1705
|
+
: {}),
|
|
1706
|
+
...(hasModelResourceValues
|
|
1707
|
+
? {
|
|
1708
|
+
resource: {
|
|
1709
|
+
description: 'Resource information',
|
|
1710
|
+
getVar: 'ctx.model.resource',
|
|
1711
|
+
properties: {
|
|
1712
|
+
...modelResourceProps,
|
|
1713
|
+
...(hasSnapshotValue(modelResourceSnap?.filterByTk)
|
|
1714
|
+
? {
|
|
1715
|
+
filterByTk: {
|
|
1716
|
+
description: 'Record filterByTk',
|
|
1717
|
+
getVar: 'ctx.model.resource.filterByTk',
|
|
1718
|
+
},
|
|
1719
|
+
}
|
|
1720
|
+
: {}),
|
|
1721
|
+
...(hasSnapshotValue(modelResourceSnap?.sourceId)
|
|
1722
|
+
? {
|
|
1723
|
+
sourceId: {
|
|
1724
|
+
description: 'Source record ID (sourceId)',
|
|
1725
|
+
getVar: 'ctx.model.resource.sourceId',
|
|
1726
|
+
},
|
|
1727
|
+
}
|
|
1728
|
+
: {}),
|
|
1729
|
+
},
|
|
1730
|
+
},
|
|
1731
|
+
}
|
|
1732
|
+
: {}),
|
|
1733
|
+
},
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// Block (when ctx.blockModel exists)
|
|
1738
|
+
if (blockOwner && blockOwnerExpr) {
|
|
1739
|
+
const blockLabel = pickLabel(blockOwner);
|
|
1740
|
+
const blockUid = blockOwner.uid;
|
|
1741
|
+
const blockModelClass = blockOwner.constructor?.name;
|
|
1742
|
+
|
|
1743
|
+
const blockResourceProps: Record<string, FlowContextInfosEnvNode> = {};
|
|
1744
|
+
let hasBlockResourceValues = false;
|
|
1745
|
+
const blockCollectionName = blockResourceSnap.collectionName;
|
|
1746
|
+
if (hasSnapshotValue(blockCollectionName)) {
|
|
1747
|
+
blockResourceProps.collectionName = {
|
|
1748
|
+
description: 'Collection name',
|
|
1749
|
+
getVar: `${blockResourceBaseExpr}.collectionName`,
|
|
1750
|
+
value: blockCollectionName,
|
|
1751
|
+
};
|
|
1752
|
+
hasBlockResourceValues = true;
|
|
1753
|
+
}
|
|
1754
|
+
const blockDataSourceKey = blockResourceSnap.dataSourceKey;
|
|
1755
|
+
if (hasSnapshotValue(blockDataSourceKey)) {
|
|
1756
|
+
blockResourceProps.dataSourceKey = {
|
|
1757
|
+
description: 'Data source key',
|
|
1758
|
+
getVar: `${blockResourceBaseExpr}.dataSourceKey`,
|
|
1759
|
+
value: blockDataSourceKey,
|
|
1760
|
+
};
|
|
1761
|
+
hasBlockResourceValues = true;
|
|
1762
|
+
}
|
|
1763
|
+
const blockAssociationName = blockResourceSnap.associationName;
|
|
1764
|
+
if (hasSnapshotValue(blockAssociationName)) {
|
|
1765
|
+
blockResourceProps.associationName = {
|
|
1766
|
+
description: 'Association name',
|
|
1767
|
+
getVar: `${blockResourceBaseExpr}.associationName`,
|
|
1768
|
+
value: blockAssociationName,
|
|
1769
|
+
};
|
|
1770
|
+
hasBlockResourceValues = true;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
envs.block = {
|
|
1774
|
+
description: 'Current block information',
|
|
1775
|
+
getVar: 'ctx.blockModel',
|
|
1776
|
+
properties: {
|
|
1777
|
+
...(hasSnapshotValue(blockLabel) ? { label: { description: 'Block label', value: blockLabel } } : {}),
|
|
1778
|
+
...(hasSnapshotValue(blockModelClass)
|
|
1779
|
+
? {
|
|
1780
|
+
modelClass: {
|
|
1781
|
+
description: 'Block model class name',
|
|
1782
|
+
value: blockModelClass,
|
|
1783
|
+
},
|
|
1784
|
+
}
|
|
1785
|
+
: {}),
|
|
1786
|
+
...(hasSnapshotValue(blockUid)
|
|
1787
|
+
? {
|
|
1788
|
+
uid: {
|
|
1789
|
+
description: 'Block uid',
|
|
1790
|
+
getVar: 'ctx.blockModel.uid',
|
|
1791
|
+
value: blockUid,
|
|
1792
|
+
},
|
|
1793
|
+
}
|
|
1794
|
+
: {}),
|
|
1795
|
+
...(hasBlockResourceValues
|
|
1796
|
+
? {
|
|
1797
|
+
resource: {
|
|
1798
|
+
description: 'Resource information',
|
|
1799
|
+
getVar: 'ctx.blockModel.resource',
|
|
1800
|
+
properties: {
|
|
1801
|
+
...blockResourceProps,
|
|
1802
|
+
...(hasSnapshotValue(blockResourceSnap?.filterByTk)
|
|
1803
|
+
? {
|
|
1804
|
+
filterByTk: {
|
|
1805
|
+
description: 'Record filterByTk',
|
|
1806
|
+
getVar: 'ctx.blockModel.resource.filterByTk',
|
|
1807
|
+
},
|
|
1808
|
+
}
|
|
1809
|
+
: {}),
|
|
1810
|
+
...(hasSnapshotValue(blockResourceSnap?.sourceId)
|
|
1811
|
+
? {
|
|
1812
|
+
sourceId: {
|
|
1813
|
+
description: 'Source record ID (sourceId)',
|
|
1814
|
+
getVar: 'ctx.blockModel.resource.sourceId',
|
|
1815
|
+
},
|
|
1816
|
+
}
|
|
1817
|
+
: {}),
|
|
1818
|
+
},
|
|
1819
|
+
},
|
|
1820
|
+
}
|
|
1821
|
+
: {}),
|
|
1822
|
+
},
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// Popup (only when popup exists)
|
|
1827
|
+
if (popupLike?.uid) {
|
|
1828
|
+
const popupUid = popupLike.uid;
|
|
1829
|
+
const popupResourceProps: Record<string, FlowContextInfosEnvNode> = {};
|
|
1830
|
+
let hasPopupResourceValues = false;
|
|
1831
|
+
const popupCollectionName = popupResourceSnap.collectionName;
|
|
1832
|
+
if (hasSnapshotValue(popupCollectionName)) {
|
|
1833
|
+
popupResourceProps.collectionName = {
|
|
1834
|
+
description: 'Collection name',
|
|
1835
|
+
getVar: 'ctx.popup.resource.collectionName',
|
|
1836
|
+
value: popupCollectionName,
|
|
1837
|
+
};
|
|
1838
|
+
hasPopupResourceValues = true;
|
|
1839
|
+
}
|
|
1840
|
+
const popupDataSourceKey = popupResourceSnap.dataSourceKey;
|
|
1841
|
+
if (hasSnapshotValue(popupDataSourceKey)) {
|
|
1842
|
+
popupResourceProps.dataSourceKey = {
|
|
1843
|
+
description: 'Data source key',
|
|
1844
|
+
getVar: 'ctx.popup.resource.dataSourceKey',
|
|
1845
|
+
value: popupDataSourceKey,
|
|
1846
|
+
};
|
|
1847
|
+
hasPopupResourceValues = true;
|
|
1848
|
+
}
|
|
1849
|
+
const popupAssociationName = popupResourceSnap.associationName;
|
|
1850
|
+
if (hasSnapshotValue(popupAssociationName)) {
|
|
1851
|
+
popupResourceProps.associationName = {
|
|
1852
|
+
description: 'Association name',
|
|
1853
|
+
getVar: 'ctx.popup.resource.associationName',
|
|
1854
|
+
value: popupAssociationName,
|
|
1855
|
+
};
|
|
1856
|
+
hasPopupResourceValues = true;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
envs.popup = {
|
|
1860
|
+
description: 'Current popup information',
|
|
1861
|
+
getVar: 'ctx.popup',
|
|
1862
|
+
properties: {
|
|
1863
|
+
uid: { description: 'Popup uid', getVar: 'ctx.popup.uid', value: popupUid },
|
|
1864
|
+
record: {
|
|
1865
|
+
description: 'Current popup record (object).',
|
|
1866
|
+
getVar: 'ctx.popup.record',
|
|
1867
|
+
},
|
|
1868
|
+
sourceRecord: {
|
|
1869
|
+
description: 'Current popup sourceRecord (object).',
|
|
1870
|
+
getVar: 'ctx.popup.sourceRecord',
|
|
1871
|
+
},
|
|
1872
|
+
parent: {
|
|
1873
|
+
description: 'Parent popup info (object).',
|
|
1874
|
+
getVar: 'ctx.popup.parent',
|
|
1875
|
+
},
|
|
1876
|
+
...(hasPopupResourceValues
|
|
1877
|
+
? {
|
|
1878
|
+
resource: {
|
|
1879
|
+
description: 'Resource information',
|
|
1880
|
+
getVar: 'ctx.popup.resource',
|
|
1881
|
+
properties: {
|
|
1882
|
+
...popupResourceProps,
|
|
1883
|
+
...(hasSnapshotValue(popupResourceSnap?.filterByTk)
|
|
1884
|
+
? {
|
|
1885
|
+
filterByTk: {
|
|
1886
|
+
description: 'Record filterByTk',
|
|
1887
|
+
getVar: 'ctx.popup.resource.filterByTk',
|
|
1888
|
+
},
|
|
1889
|
+
}
|
|
1890
|
+
: {}),
|
|
1891
|
+
...(hasSnapshotValue(popupResourceSnap?.sourceId)
|
|
1892
|
+
? {
|
|
1893
|
+
sourceId: {
|
|
1894
|
+
description: 'Source record ID (sourceId)',
|
|
1895
|
+
getVar: 'ctx.popup.resource.sourceId',
|
|
1896
|
+
},
|
|
1897
|
+
}
|
|
1898
|
+
: {}),
|
|
1899
|
+
},
|
|
1900
|
+
},
|
|
1901
|
+
}
|
|
1902
|
+
: {}),
|
|
1903
|
+
},
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// Current view blocks snapshot (page or current popup)
|
|
1908
|
+
const viewUid = (() => {
|
|
1909
|
+
const popupUid = popupLike?.uid;
|
|
1910
|
+
if (hasSnapshotValue(popupUid)) return popupUid.trim();
|
|
1911
|
+
const v = inputArgs?.viewUid;
|
|
1912
|
+
if (hasSnapshotValue(v)) return v.trim();
|
|
1913
|
+
return undefined;
|
|
1914
|
+
})();
|
|
1915
|
+
|
|
1916
|
+
const engine = getMaybe(() => (evalCtx as any).engine) as FlowEngine | undefined;
|
|
1917
|
+
const viewModel = viewUid ? engine?.getModel(viewUid, true) : undefined;
|
|
1918
|
+
|
|
1919
|
+
type ViewTreeNode = {
|
|
1920
|
+
uid: string;
|
|
1921
|
+
subModels?: Record<string, unknown>;
|
|
1922
|
+
context?: { blockModel?: unknown };
|
|
1923
|
+
resource?: unknown;
|
|
1924
|
+
constructor?: { name?: string };
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
const isBlockModelInstance = (m: ViewTreeNode): boolean => m.context?.blockModel === m;
|
|
1928
|
+
|
|
1929
|
+
if (viewModel) {
|
|
1930
|
+
const queue: ViewTreeNode[] = [viewModel as unknown as ViewTreeNode];
|
|
1931
|
+
const blocks: Array<Record<string, JSONValue>> = [];
|
|
1932
|
+
|
|
1933
|
+
for (let i = 0; i < queue.length; i++) {
|
|
1934
|
+
const m = queue[i];
|
|
1935
|
+
|
|
1936
|
+
if (isBlockModelInstance(m)) {
|
|
1937
|
+
const modelClass = m.constructor?.name || m.uid;
|
|
1938
|
+
const label = pickLabel(m) || modelClass || m.uid;
|
|
1939
|
+
|
|
1940
|
+
const resSnap = getResourceSnapshot(m.resource);
|
|
1941
|
+
const resource: Record<string, JSONValue> = {};
|
|
1942
|
+
if (hasSnapshotValue(resSnap.dataSourceKey)) resource.dataSourceKey = resSnap.dataSourceKey;
|
|
1943
|
+
if (hasSnapshotValue(resSnap.collectionName)) resource.collectionName = resSnap.collectionName;
|
|
1944
|
+
if (hasSnapshotValue(resSnap.associationName)) resource.associationName = resSnap.associationName;
|
|
1945
|
+
|
|
1946
|
+
const block: Record<string, JSONValue> = {
|
|
1947
|
+
uid: m.uid,
|
|
1948
|
+
label,
|
|
1949
|
+
modelClass,
|
|
1950
|
+
...(Object.keys(resource).length > 0 ? { resource } : {}),
|
|
1951
|
+
};
|
|
1952
|
+
blocks.push(block);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
const subModels = m.subModels;
|
|
1956
|
+
if (subModels && typeof subModels === 'object') {
|
|
1957
|
+
for (const v of Object.values(subModels)) {
|
|
1958
|
+
if (!v) continue;
|
|
1959
|
+
if (Array.isArray(v)) queue.push(...(v as ViewTreeNode[]));
|
|
1960
|
+
else queue.push(v as ViewTreeNode);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
envs.currentViewBlocks = {
|
|
1966
|
+
description: 'Current view blocks',
|
|
1967
|
+
value: blocks,
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
return envs;
|
|
1972
|
+
};
|
|
1973
|
+
|
|
1974
|
+
const normalizePath = (raw: string): string | undefined => {
|
|
1975
|
+
if (typeof raw !== 'string') return undefined;
|
|
1976
|
+
const s = raw.trim();
|
|
1977
|
+
if (!s) return undefined;
|
|
1978
|
+
const extracted = extractPropertyPath(s);
|
|
1979
|
+
if (Array.isArray(extracted) && extracted.length > 0) {
|
|
1980
|
+
return extracted.join('.');
|
|
1981
|
+
}
|
|
1982
|
+
if (s === 'ctx') return '';
|
|
1983
|
+
if (s.startsWith('ctx.')) return s.slice(4).trim();
|
|
1984
|
+
return s;
|
|
1985
|
+
};
|
|
1986
|
+
|
|
1987
|
+
const paths = (() => {
|
|
1988
|
+
const p = options.path;
|
|
1989
|
+
const list = typeof p === 'string' ? [p] : Array.isArray(p) ? p : [];
|
|
1990
|
+
return list
|
|
1991
|
+
.map((x) => normalizePath(String(x)))
|
|
1992
|
+
.filter((x): x is string => typeof x === 'string' && x.length > 0);
|
|
1993
|
+
})();
|
|
1994
|
+
|
|
1995
|
+
const hasRootPath = (() => {
|
|
1996
|
+
const p = options.path;
|
|
1997
|
+
if (typeof p === 'string') return normalizePath(p) === '';
|
|
1998
|
+
if (Array.isArray(p)) return p.some((x) => normalizePath(String(x)) === '');
|
|
1999
|
+
return false;
|
|
2000
|
+
})();
|
|
2001
|
+
|
|
2002
|
+
const collectKeysDeep = (ctx: FlowContext, out: Set<string>, key: '_props' | '_methods', visited: WeakSet<any>) => {
|
|
2003
|
+
if (!ctx || typeof ctx !== 'object') return;
|
|
2004
|
+
if (visited.has(ctx as any)) return;
|
|
2005
|
+
visited.add(ctx as any);
|
|
2006
|
+
try {
|
|
2007
|
+
const bag = (ctx as any)[key];
|
|
2008
|
+
if (bag && typeof bag === 'object') {
|
|
2009
|
+
for (const k of Object.keys(bag)) out.add(k);
|
|
2010
|
+
}
|
|
2011
|
+
} catch (_) {
|
|
2012
|
+
// ignore
|
|
2013
|
+
}
|
|
2014
|
+
try {
|
|
2015
|
+
const delegates = (ctx as any)._delegates;
|
|
2016
|
+
if (Array.isArray(delegates)) {
|
|
2017
|
+
for (const d of delegates) collectKeysDeep(d, out, key, visited);
|
|
2018
|
+
}
|
|
2019
|
+
} catch (_) {
|
|
2020
|
+
// ignore
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
|
|
2024
|
+
const getRunJSDoc = (): any => {
|
|
2025
|
+
const modelClass = getModelClassName(this);
|
|
2026
|
+
const Ctor = RunJSContextRegistry.resolve(version, modelClass) || RunJSContextRegistry.resolve(version, '*');
|
|
2027
|
+
if (!Ctor) return {};
|
|
2028
|
+
const locale = (this as any)?.api?.auth?.locale || (this as any)?.i18n?.language || (this as any)?.locale;
|
|
2029
|
+
try {
|
|
2030
|
+
if ((Ctor as any)?.getDoc?.length) {
|
|
2031
|
+
return (Ctor as any).getDoc(locale) || {};
|
|
2032
|
+
}
|
|
2033
|
+
return (Ctor as any)?.getDoc?.() || {};
|
|
2034
|
+
} catch (_) {
|
|
2035
|
+
return {};
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
|
|
2039
|
+
const doc = getRunJSDoc();
|
|
2040
|
+
const docMethods = __isPlainObject(doc?.methods) ? (doc.methods as Record<string, any>) : {};
|
|
2041
|
+
const docProps = __isPlainObject(doc?.properties) ? (doc.properties as Record<string, any>) : {};
|
|
2042
|
+
|
|
2043
|
+
const toDocObject = (node: any): any | undefined => {
|
|
2044
|
+
if (typeof node === 'string') return { description: node };
|
|
2045
|
+
if (__isPlainObject(node)) return node;
|
|
2046
|
+
return undefined;
|
|
2047
|
+
};
|
|
2048
|
+
|
|
2049
|
+
const evalBool = async (raw: any, call: (fn: Function) => any): Promise<boolean | undefined> => {
|
|
2050
|
+
if (typeof raw === 'undefined') return undefined;
|
|
2051
|
+
if (typeof raw === 'boolean') return raw;
|
|
2052
|
+
if (typeof raw === 'function') {
|
|
2053
|
+
try {
|
|
2054
|
+
const v = call(raw);
|
|
2055
|
+
return isPromiseLike(v) ? !!(await v) : !!v;
|
|
2056
|
+
} catch (_) {
|
|
2057
|
+
return false;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
return !!raw;
|
|
2061
|
+
};
|
|
2062
|
+
|
|
2063
|
+
const evalString = async (raw: any, call: (fn: Function) => any): Promise<string | undefined> => {
|
|
2064
|
+
if (typeof raw === 'undefined' || raw === null) return undefined;
|
|
2065
|
+
if (typeof raw === 'string') return raw;
|
|
2066
|
+
if (typeof raw === 'function') {
|
|
2067
|
+
try {
|
|
2068
|
+
const v = call(raw);
|
|
2069
|
+
const resolved = isPromiseLike(v) ? await v : v;
|
|
2070
|
+
if (typeof resolved === 'string') return resolved;
|
|
2071
|
+
return typeof resolved === 'undefined' || resolved === null ? undefined : String(resolved);
|
|
2072
|
+
} catch (_) {
|
|
2073
|
+
return undefined;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
return String(raw);
|
|
2077
|
+
};
|
|
2078
|
+
|
|
2079
|
+
const evalRunJSHidden = async (
|
|
2080
|
+
raw: any,
|
|
2081
|
+
): Promise<{
|
|
2082
|
+
hideSelf: boolean;
|
|
2083
|
+
hideSubpaths: string[];
|
|
2084
|
+
}> => {
|
|
2085
|
+
let hideSelf = false;
|
|
2086
|
+
let list: any = [];
|
|
2087
|
+
try {
|
|
2088
|
+
if (typeof raw === 'boolean') hideSelf = raw;
|
|
2089
|
+
else if (Array.isArray(raw)) list = raw;
|
|
2090
|
+
else if (typeof raw === 'function') {
|
|
2091
|
+
const v = raw(evalCtx);
|
|
2092
|
+
const resolved = isPromiseLike(v) ? await v : v;
|
|
2093
|
+
if (typeof resolved === 'boolean') hideSelf = resolved;
|
|
2094
|
+
else if (Array.isArray(resolved)) list = resolved;
|
|
2095
|
+
}
|
|
2096
|
+
} catch (_) {
|
|
2097
|
+
hideSelf = false;
|
|
2098
|
+
list = [];
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
const hideSubpaths: string[] = [];
|
|
2102
|
+
if (Array.isArray(list)) {
|
|
2103
|
+
for (const p of list) {
|
|
2104
|
+
if (typeof p !== 'string') continue;
|
|
2105
|
+
const s = p.trim();
|
|
2106
|
+
if (!s) continue;
|
|
2107
|
+
// Only relative paths are supported. Ignore "ctx.xxx" absolute style to avoid ambiguity.
|
|
2108
|
+
if (s === 'ctx' || s.startsWith('ctx.')) continue;
|
|
2109
|
+
if (/\s/.test(s)) continue;
|
|
2110
|
+
hideSubpaths.push(s);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return { hideSelf: !!hideSelf, hideSubpaths };
|
|
2114
|
+
};
|
|
2115
|
+
|
|
2116
|
+
const isHiddenByPrefixes = (path: string, hiddenPrefixes: Set<string>) => {
|
|
2117
|
+
if (!path) return false;
|
|
2118
|
+
const parts = path.split('.').filter(Boolean);
|
|
2119
|
+
while (parts.length) {
|
|
2120
|
+
if (hiddenPrefixes.has(parts.join('.'))) return true;
|
|
2121
|
+
parts.pop();
|
|
2122
|
+
}
|
|
2123
|
+
return false;
|
|
2124
|
+
};
|
|
2125
|
+
|
|
2126
|
+
const pickMethodInfo = (obj: any): Partial<FlowContextApiInfo> => {
|
|
2127
|
+
const src = toDocObject(obj);
|
|
2128
|
+
if (!src) return {};
|
|
2129
|
+
const out: any = {};
|
|
2130
|
+
for (const k of ['description', 'examples', 'completion', 'ref', 'params', 'returns']) {
|
|
2131
|
+
const v = (src as any)[k];
|
|
2132
|
+
if (typeof v !== 'undefined') out[k] = v;
|
|
2133
|
+
}
|
|
2134
|
+
if (Array.isArray(out.examples)) {
|
|
2135
|
+
out.examples = out.examples.filter((x: any) => typeof x === 'string' && x.trim());
|
|
2136
|
+
}
|
|
2137
|
+
return out;
|
|
2138
|
+
};
|
|
2139
|
+
|
|
2140
|
+
const pickPropertyInfo = (obj: any): Partial<FlowContextApiInfo> => {
|
|
2141
|
+
const src = toDocObject(obj);
|
|
2142
|
+
if (!src) return {};
|
|
2143
|
+
const out: any = {};
|
|
2144
|
+
for (const k of [
|
|
2145
|
+
'title',
|
|
2146
|
+
'type',
|
|
2147
|
+
'interface',
|
|
2148
|
+
'description',
|
|
2149
|
+
'examples',
|
|
2150
|
+
'completion',
|
|
2151
|
+
'ref',
|
|
2152
|
+
'params',
|
|
2153
|
+
'returns',
|
|
2154
|
+
]) {
|
|
2155
|
+
const v = (src as any)[k];
|
|
2156
|
+
if (typeof v !== 'undefined') out[k] = v;
|
|
2157
|
+
}
|
|
2158
|
+
if (Array.isArray(out.examples)) {
|
|
2159
|
+
out.examples = out.examples.filter((x: any) => typeof x === 'string' && x.trim());
|
|
2160
|
+
}
|
|
2161
|
+
return out;
|
|
2162
|
+
};
|
|
2163
|
+
|
|
2164
|
+
const getMethodInfoFromChain = (name: string): FlowContextMethodInfoInput | undefined => {
|
|
2165
|
+
const visited = new WeakSet<any>();
|
|
2166
|
+
const walk = (ctx: FlowContext): FlowContextMethodInfoInput | undefined => {
|
|
2167
|
+
if (!ctx || typeof ctx !== 'object') return undefined;
|
|
2168
|
+
if (visited.has(ctx as any)) return undefined;
|
|
2169
|
+
visited.add(ctx as any);
|
|
2170
|
+
if (Object.prototype.hasOwnProperty.call((ctx as any)._methodInfos || {}, name)) {
|
|
2171
|
+
return (ctx as any)._methodInfos?.[name] as FlowContextMethodInfoInput;
|
|
2172
|
+
}
|
|
2173
|
+
const delegates = (ctx as any)._delegates;
|
|
2174
|
+
if (Array.isArray(delegates)) {
|
|
2175
|
+
for (const d of delegates) {
|
|
2176
|
+
const found = walk(d);
|
|
2177
|
+
if (found) return found;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
return undefined;
|
|
2181
|
+
};
|
|
2182
|
+
return walk(this);
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
const hasMethodInChain = (name: string): boolean => {
|
|
2186
|
+
const visited = new WeakSet<any>();
|
|
2187
|
+
const walk = (ctx: FlowContext): boolean => {
|
|
2188
|
+
if (!ctx || typeof ctx !== 'object') return false;
|
|
2189
|
+
if (visited.has(ctx as any)) return false;
|
|
2190
|
+
visited.add(ctx as any);
|
|
2191
|
+
if (Object.prototype.hasOwnProperty.call((ctx as any)._methods || {}, name)) return true;
|
|
2192
|
+
const delegates = (ctx as any)._delegates;
|
|
2193
|
+
if (Array.isArray(delegates)) {
|
|
2194
|
+
for (const d of delegates) {
|
|
2195
|
+
if (walk(d)) return true;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
return false;
|
|
2199
|
+
};
|
|
2200
|
+
return walk(this);
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
const buildMethodInfo = async (name: string): Promise<FlowContextApiInfo | undefined> => {
|
|
2204
|
+
if (isPrivateKey(name)) return undefined;
|
|
2205
|
+
const docNode = docMethods[name];
|
|
2206
|
+
const info = getMethodInfoFromChain(name);
|
|
2207
|
+
const exists = typeof docNode !== 'undefined' || typeof info !== 'undefined' || hasMethodInChain(name);
|
|
2208
|
+
if (!exists) return undefined;
|
|
2209
|
+
|
|
2210
|
+
const docObj = toDocObject(docNode);
|
|
2211
|
+
const docHidden = await evalBool((docObj as any)?.hidden, (fn) => fn(evalCtx));
|
|
2212
|
+
const infoHidden = await evalBool(info?.hidden, (fn) => fn(evalCtx));
|
|
2213
|
+
if (!!docHidden || !!infoHidden) return undefined;
|
|
2214
|
+
|
|
2215
|
+
const docDisabled = await evalBool((docObj as any)?.disabled, (fn) => fn(evalCtx));
|
|
2216
|
+
const docDisabledReason = await evalString((docObj as any)?.disabledReason, (fn) => fn(evalCtx));
|
|
2217
|
+
const infoDisabled = await evalBool(info?.disabled, (fn) => fn(evalCtx));
|
|
2218
|
+
const infoDisabledReason = await evalString(info?.disabledReason, (fn) => fn(evalCtx));
|
|
2219
|
+
const disabled = typeof infoDisabled !== 'undefined' ? infoDisabled : docDisabled;
|
|
2220
|
+
const disabledReason = typeof infoDisabledReason !== 'undefined' ? infoDisabledReason : docDisabledReason;
|
|
2221
|
+
|
|
2222
|
+
let out: FlowContextApiInfo = {};
|
|
2223
|
+
out = { ...out, ...pickMethodInfo(docObj) };
|
|
2224
|
+
out = { ...out, ...pickMethodInfo(info) };
|
|
2225
|
+
if (typeof disabled !== 'undefined') out.disabled = !!disabled;
|
|
2226
|
+
if (typeof disabledReason !== 'undefined') out.disabledReason = disabledReason;
|
|
2227
|
+
if (!Object.keys(out).length) return undefined;
|
|
2228
|
+
// Mark as callable for tooling (e.g. code-editor completion).
|
|
2229
|
+
out.type = 'function';
|
|
2230
|
+
return out;
|
|
2231
|
+
};
|
|
2232
|
+
|
|
2233
|
+
const buildPropertyInfoFromNodes = async (args: {
|
|
2234
|
+
docNode?: any;
|
|
2235
|
+
metaNode?: PropertyMetaOrFactory;
|
|
2236
|
+
infoNode?: FlowContextPropertyInfoInput;
|
|
2237
|
+
depth: number;
|
|
2238
|
+
pathFromRoot: string[];
|
|
2239
|
+
hiddenPrefixes: Set<string>;
|
|
2240
|
+
}): Promise<FlowContextApiInfo | undefined> => {
|
|
2241
|
+
const { docNode, metaNode, infoNode, depth, pathFromRoot, hiddenPrefixes } = args;
|
|
2242
|
+
const relPath = pathFromRoot.join('.');
|
|
2243
|
+
if (isHiddenByPrefixes(relPath, hiddenPrefixes)) return undefined;
|
|
2244
|
+
|
|
2245
|
+
const docObj = toDocObject(docNode);
|
|
2246
|
+
const infoObj = toDocObject(infoNode);
|
|
2247
|
+
|
|
2248
|
+
const docHiddenDecision = await evalRunJSHidden((docObj as any)?.hidden);
|
|
2249
|
+
if (docHiddenDecision.hideSelf) return undefined;
|
|
2250
|
+
const infoHiddenDecision = await evalRunJSHidden((infoObj as any)?.hidden);
|
|
2251
|
+
if (infoHiddenDecision.hideSelf) return undefined;
|
|
2252
|
+
|
|
2253
|
+
const resolvedMetaNode = await resolveMetaOrFactory(metaNode);
|
|
2254
|
+
const metaHidden = await evalBool(resolvedMetaNode?.hidden, (fn) => (fn as any).call(resolvedMetaNode, evalCtx));
|
|
2255
|
+
if (metaHidden) return undefined;
|
|
2256
|
+
|
|
2257
|
+
const childHiddenPrefixes = new Set(hiddenPrefixes);
|
|
2258
|
+
for (const sub of [...docHiddenDecision.hideSubpaths, ...infoHiddenDecision.hideSubpaths]) {
|
|
2259
|
+
const normalized = sub.trim();
|
|
2260
|
+
if (!normalized) continue;
|
|
2261
|
+
const stripped = normalized === 'ctx' ? '' : normalized.startsWith('ctx.') ? normalized.slice(4) : normalized;
|
|
2262
|
+
if (!stripped) continue;
|
|
2263
|
+
const abs = relPath ? `${relPath}.${stripped}` : stripped;
|
|
2264
|
+
childHiddenPrefixes.add(abs);
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
const docDisabled = await evalBool((docObj as any)?.disabled, (fn) => fn(evalCtx));
|
|
2268
|
+
const docDisabledReason = await evalString((docObj as any)?.disabledReason, (fn) => fn(evalCtx));
|
|
2269
|
+
const metaDisabled = await evalBool(resolvedMetaNode?.disabled, (fn) =>
|
|
2270
|
+
(fn as any).call(resolvedMetaNode, evalCtx),
|
|
2271
|
+
);
|
|
2272
|
+
const metaDisabledReason = await evalString(resolvedMetaNode?.disabledReason, (fn) =>
|
|
2273
|
+
(fn as any).call(resolvedMetaNode, evalCtx),
|
|
2274
|
+
);
|
|
2275
|
+
const infoDisabled = await evalBool((infoObj as any)?.disabled, (fn) => fn(evalCtx));
|
|
2276
|
+
const infoDisabledReason = await evalString((infoObj as any)?.disabledReason, (fn) => fn(evalCtx));
|
|
2277
|
+
const disabled =
|
|
2278
|
+
typeof infoDisabled !== 'undefined'
|
|
2279
|
+
? infoDisabled
|
|
2280
|
+
: typeof metaDisabled !== 'undefined'
|
|
2281
|
+
? metaDisabled
|
|
2282
|
+
: docDisabled;
|
|
2283
|
+
const disabledReason =
|
|
2284
|
+
typeof infoDisabledReason !== 'undefined'
|
|
2285
|
+
? infoDisabledReason
|
|
2286
|
+
: typeof metaDisabledReason !== 'undefined'
|
|
2287
|
+
? metaDisabledReason
|
|
2288
|
+
: docDisabledReason;
|
|
2289
|
+
|
|
2290
|
+
let out: FlowContextApiInfo = {};
|
|
2291
|
+
out = { ...out, ...pickPropertyInfo(docObj) };
|
|
2292
|
+
out = { ...out, ...pickPropertyInfo(resolvedMetaNode) };
|
|
2293
|
+
out = { ...out, ...pickPropertyInfo(infoObj) };
|
|
2294
|
+
if (typeof disabled !== 'undefined') out.disabled = !!disabled;
|
|
2295
|
+
if (typeof disabledReason !== 'undefined') out.disabledReason = disabledReason;
|
|
2296
|
+
|
|
2297
|
+
if (depth >= maxDepth) return Object.keys(out).length ? out : undefined;
|
|
2298
|
+
|
|
2299
|
+
const docChildren = __isPlainObject((docObj as any)?.properties)
|
|
2300
|
+
? ((docObj as any).properties as any as Record<string, any>)
|
|
2301
|
+
: undefined;
|
|
2302
|
+
|
|
2303
|
+
let metaChildren: Record<string, PropertyMetaOrFactory> | undefined;
|
|
2304
|
+
if (resolvedMetaNode?.properties) {
|
|
2305
|
+
try {
|
|
2306
|
+
const props = resolvedMetaNode.properties;
|
|
2307
|
+
if (typeof props === 'function') {
|
|
2308
|
+
const resolved = await (props as any).call(resolvedMetaNode, evalCtx);
|
|
2309
|
+
resolvedMetaNode.properties = resolved;
|
|
2310
|
+
metaChildren = resolved as Record<string, PropertyMetaOrFactory>;
|
|
2311
|
+
} else if (__isPlainObject(props)) {
|
|
2312
|
+
metaChildren = props as Record<string, PropertyMetaOrFactory>;
|
|
2313
|
+
}
|
|
2314
|
+
} catch (_) {
|
|
2315
|
+
metaChildren = undefined;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
let infoChildren: Record<string, FlowContextPropertyInfoInput> | undefined;
|
|
2320
|
+
if (__isPlainObject(infoObj) && (infoObj as any)?.properties) {
|
|
2321
|
+
try {
|
|
2322
|
+
const props = (infoObj as any).properties;
|
|
2323
|
+
if (typeof props === 'function') {
|
|
2324
|
+
const resolved = await (props as any).call(infoObj, evalCtx);
|
|
2325
|
+
(infoObj as any).properties = resolved;
|
|
2326
|
+
infoChildren = resolved as Record<string, FlowContextPropertyInfoInput>;
|
|
2327
|
+
} else if (__isPlainObject(props)) {
|
|
2328
|
+
infoChildren = props as Record<string, FlowContextPropertyInfoInput>;
|
|
2329
|
+
}
|
|
2330
|
+
} catch (_) {
|
|
2331
|
+
infoChildren = undefined;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
const keys = new Set<string>();
|
|
2336
|
+
if (docChildren) for (const k of Object.keys(docChildren)) keys.add(k);
|
|
2337
|
+
if (metaChildren) for (const k of Object.keys(metaChildren)) keys.add(k);
|
|
2338
|
+
if (infoChildren) for (const k of Object.keys(infoChildren)) keys.add(k);
|
|
2339
|
+
if (!keys.size) return Object.keys(out).length ? out : undefined;
|
|
2340
|
+
|
|
2341
|
+
const childrenOut: Record<string, FlowContextApiInfo> = {};
|
|
2342
|
+
for (const k of keys) {
|
|
2343
|
+
if (isPrivateKey(k)) continue;
|
|
2344
|
+
const child = await buildPropertyInfoFromNodes({
|
|
2345
|
+
docNode: docChildren?.[k],
|
|
2346
|
+
metaNode: metaChildren?.[k],
|
|
2347
|
+
infoNode: infoChildren?.[k],
|
|
2348
|
+
depth: depth + 1,
|
|
2349
|
+
pathFromRoot: [...pathFromRoot, k],
|
|
2350
|
+
hiddenPrefixes: childHiddenPrefixes,
|
|
2351
|
+
});
|
|
2352
|
+
if (child) childrenOut[k] = child;
|
|
2353
|
+
}
|
|
2354
|
+
if (Object.keys(childrenOut).length) out.properties = childrenOut;
|
|
2355
|
+
if (!Object.keys(out).length) return undefined;
|
|
2356
|
+
return out;
|
|
2357
|
+
};
|
|
2358
|
+
|
|
2359
|
+
const resolvePropertyMetaAtPath = async (segments: string[]): Promise<PropertyMetaOrFactory | undefined> => {
|
|
2360
|
+
if (!segments.length) return undefined;
|
|
2361
|
+
const [first, ...rest] = segments;
|
|
2362
|
+
const opt = this.getPropertyOptions(first);
|
|
2363
|
+
if (!opt?.meta) return undefined;
|
|
2364
|
+
try {
|
|
2365
|
+
// Fast path: when querying the root key only, return the meta (may be a factory) and let
|
|
2366
|
+
// buildPropertyInfoFromNodes decide whether to resolve it based on maxDepth.
|
|
2367
|
+
if (!rest.length) return opt.meta as PropertyMetaOrFactory;
|
|
2368
|
+
|
|
2369
|
+
let current = await resolveMetaOrFactory(opt.meta as PropertyMetaOrFactory);
|
|
2370
|
+
if (!current) return undefined;
|
|
2371
|
+
|
|
2372
|
+
for (let i = 0; i < rest.length; i++) {
|
|
2373
|
+
const key = rest[i];
|
|
2374
|
+
|
|
2375
|
+
let props: any = (current as any)?.properties;
|
|
2376
|
+
if (!props) return undefined;
|
|
2377
|
+
if (typeof props === 'function') {
|
|
2378
|
+
const resolved = await props.call(current, evalCtx);
|
|
2379
|
+
(current as any).properties = resolved;
|
|
2380
|
+
props = resolved;
|
|
2381
|
+
}
|
|
2382
|
+
if (!props || typeof props !== 'object') return undefined;
|
|
2383
|
+
|
|
2384
|
+
const next = (props as any)?.[key] as PropertyMetaOrFactory | undefined;
|
|
2385
|
+
if (!next) return undefined;
|
|
2386
|
+
|
|
2387
|
+
// Return the node at the requested path (may still be a factory).
|
|
2388
|
+
if (i === rest.length - 1) return next;
|
|
2389
|
+
|
|
2390
|
+
const resolvedNext = await resolveMetaOrFactory(next);
|
|
2391
|
+
if (!resolvedNext) return undefined;
|
|
2392
|
+
current = resolvedNext;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
return undefined;
|
|
2396
|
+
} catch (_) {
|
|
2397
|
+
return undefined;
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
const resolvePropertyInfoAtPath = async (segments: string[]): Promise<FlowContextPropertyInfoInput | undefined> => {
|
|
2402
|
+
if (!segments.length) return undefined;
|
|
2403
|
+
const [first, ...rest] = segments;
|
|
2404
|
+
const opt = this.getPropertyOptions(first);
|
|
2405
|
+
if (!opt?.info) return undefined;
|
|
2406
|
+
|
|
2407
|
+
try {
|
|
2408
|
+
let cur: any = typeof opt.info === 'function' ? await (opt.info as any).call(evalCtx, evalCtx) : opt.info;
|
|
2409
|
+
if (!rest.length) return cur as FlowContextPropertyInfoInput;
|
|
2410
|
+
|
|
2411
|
+
for (const key of rest) {
|
|
2412
|
+
const obj = toDocObject(cur);
|
|
2413
|
+
if (!__isPlainObject(obj)) return undefined;
|
|
2414
|
+
let props: any = (obj as any)?.properties;
|
|
2415
|
+
if (!props) return undefined;
|
|
2416
|
+
if (typeof props === 'function') {
|
|
2417
|
+
const resolved = await props.call(obj, evalCtx);
|
|
2418
|
+
(obj as any).properties = resolved;
|
|
2419
|
+
props = resolved;
|
|
2420
|
+
}
|
|
2421
|
+
if (!__isPlainObject(props)) return undefined;
|
|
2422
|
+
cur = (props as any)[key];
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
return cur as FlowContextPropertyInfoInput;
|
|
2426
|
+
} catch (_) {
|
|
2427
|
+
return undefined;
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
|
|
2431
|
+
const resolveDocNodeAtPath = (segments: string[]): any => {
|
|
2432
|
+
if (!segments.length) return undefined;
|
|
2433
|
+
let cur: any = docProps[segments[0]];
|
|
2434
|
+
for (let i = 1; i < segments.length; i++) {
|
|
2435
|
+
const obj = toDocObject(cur);
|
|
2436
|
+
if (!__isPlainObject(obj)) return undefined;
|
|
2437
|
+
const props = (obj as any).properties;
|
|
2438
|
+
if (!__isPlainObject(props)) return undefined;
|
|
2439
|
+
cur = (props as any)[segments[i]];
|
|
2440
|
+
}
|
|
2441
|
+
return cur;
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
// path 剪裁:每个 path 独立返回一个根节点(key 为 path 字符串)
|
|
2445
|
+
if (!hasRootPath && paths.length) {
|
|
2446
|
+
const out: Record<string, FlowContextApiInfo> = {};
|
|
2447
|
+
|
|
2448
|
+
for (const p of paths) {
|
|
2449
|
+
const segments = p
|
|
2450
|
+
.split('.')
|
|
2451
|
+
.map((x) => x.trim())
|
|
2452
|
+
.filter(Boolean);
|
|
2453
|
+
if (segments.some((s) => isPrivateKey(s))) continue;
|
|
2454
|
+
if (!segments.length) continue;
|
|
2455
|
+
|
|
2456
|
+
const metaNode = await resolvePropertyMetaAtPath(segments);
|
|
2457
|
+
const pi = await buildPropertyInfoFromNodes({
|
|
2458
|
+
docNode: undefined,
|
|
2459
|
+
metaNode,
|
|
2460
|
+
infoNode: undefined,
|
|
2461
|
+
depth: 1,
|
|
2462
|
+
pathFromRoot: [],
|
|
2463
|
+
hiddenPrefixes: new Set(),
|
|
2464
|
+
});
|
|
2465
|
+
if (pi) out[p] = pi;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
return out;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// 全量输出:仅基于 property meta(含委托链)
|
|
2472
|
+
const metaMap = this._getPropertiesMeta();
|
|
2473
|
+
const out: Record<string, FlowContextApiInfo> = {};
|
|
2474
|
+
for (const [key, metaNode] of Object.entries(metaMap)) {
|
|
2475
|
+
if (isPrivateKey(key)) continue;
|
|
2476
|
+
const pi = await buildPropertyInfoFromNodes({
|
|
2477
|
+
docNode: undefined,
|
|
2478
|
+
metaNode,
|
|
2479
|
+
infoNode: undefined,
|
|
2480
|
+
depth: 1,
|
|
2481
|
+
pathFromRoot: [key],
|
|
2482
|
+
hiddenPrefixes: new Set(),
|
|
2483
|
+
});
|
|
2484
|
+
if (pi) out[key] = pi;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
return out;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
424
2490
|
#createChildNodes(
|
|
425
2491
|
properties: Record<string, PropertyMeta> | (() => Promise<Record<string, PropertyMeta>>),
|
|
426
2492
|
parentPaths: string[] = [],
|
|
@@ -666,9 +2732,16 @@ export class FlowContext {
|
|
|
666
2732
|
const computeStateFromMeta = (m: PropertyMeta): { disabled: boolean; reason?: string; hidden: boolean } => {
|
|
667
2733
|
if (!m) return { disabled: false, hidden: false };
|
|
668
2734
|
const disabledVal = typeof m.disabled === 'function' ? m.disabled() : m.disabled;
|
|
669
|
-
const
|
|
2735
|
+
const reasonVal = typeof m.disabledReason === 'function' ? m.disabledReason() : m.disabledReason;
|
|
670
2736
|
const hiddenVal = typeof m.hidden === 'function' ? m.hidden() : m.hidden;
|
|
671
|
-
|
|
2737
|
+
const disabledIsPromise = disabledVal && typeof (disabledVal as any).then === 'function';
|
|
2738
|
+
const reasonIsPromise = reasonVal && typeof (reasonVal as any).then === 'function';
|
|
2739
|
+
const hiddenIsPromise = hiddenVal && typeof (hiddenVal as any).then === 'function';
|
|
2740
|
+
// getPropertyMetaTree 为同步 API:遇到 Promise 时 fail-open(不隐藏/不禁用)
|
|
2741
|
+
const disabled = disabledIsPromise ? false : !!disabledVal;
|
|
2742
|
+
const reason = reasonIsPromise ? undefined : (reasonVal as any);
|
|
2743
|
+
const hidden = hiddenIsPromise ? false : !!hiddenVal;
|
|
2744
|
+
return { disabled, reason, hidden };
|
|
672
2745
|
};
|
|
673
2746
|
|
|
674
2747
|
if (typeof metaOrFactory === 'function') {
|
|
@@ -679,6 +2752,7 @@ export class FlowContext {
|
|
|
679
2752
|
title: metaOrFactory.title || initialTitle, // 初始使用 name 作为 title
|
|
680
2753
|
type: 'object', // 初始类型
|
|
681
2754
|
interface: undefined,
|
|
2755
|
+
options: undefined,
|
|
682
2756
|
uiSchema: undefined,
|
|
683
2757
|
paths,
|
|
684
2758
|
parentTitles: parentTitles.length > 0 ? parentTitles : undefined,
|
|
@@ -710,6 +2784,7 @@ export class FlowContext {
|
|
|
710
2784
|
node.title = finalTitle;
|
|
711
2785
|
node.type = meta?.type;
|
|
712
2786
|
node.interface = meta?.interface;
|
|
2787
|
+
node.options = meta?.options;
|
|
713
2788
|
node.uiSchema = meta?.uiSchema;
|
|
714
2789
|
// parentTitles 保持不变,因为它不包含自身 title
|
|
715
2790
|
|
|
@@ -742,6 +2817,7 @@ export class FlowContext {
|
|
|
742
2817
|
title: nodeTitle,
|
|
743
2818
|
type: metaOrFactory.type,
|
|
744
2819
|
interface: metaOrFactory.interface,
|
|
2820
|
+
options: metaOrFactory.options,
|
|
745
2821
|
uiSchema: metaOrFactory.uiSchema,
|
|
746
2822
|
paths,
|
|
747
2823
|
parentTitles: parentTitles.length > 0 ? parentTitles : undefined,
|
|
@@ -908,13 +2984,15 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
908
2984
|
declare dataSourceManager: DataSourceManager;
|
|
909
2985
|
declare requireAsync: (url: string) => Promise<any>;
|
|
910
2986
|
declare importAsync: (url: string) => Promise<any>;
|
|
911
|
-
declare createJSRunner: (options?: JSRunnerOptions) => JSRunner
|
|
2987
|
+
declare createJSRunner: (options?: JSRunnerOptions) => Promise<JSRunner>;
|
|
912
2988
|
declare pageInfo: { version?: 'v1' | 'v2' };
|
|
913
2989
|
/**
|
|
914
2990
|
* @deprecated use `resolveJsonTemplate` instead
|
|
915
2991
|
*/
|
|
916
2992
|
declare renderJson: (template: JSONValue) => Promise<any>;
|
|
917
2993
|
declare resolveJsonTemplate: (template: JSONValue) => Promise<any>;
|
|
2994
|
+
declare getVar: (path: string) => Promise<any>;
|
|
2995
|
+
declare request: (options: RequestOptions) => Promise<any>;
|
|
918
2996
|
declare runjs: (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<any>;
|
|
919
2997
|
declare getAction: <TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowContext>(
|
|
920
2998
|
name: string,
|
|
@@ -939,6 +3017,31 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
939
3017
|
declare location: Location;
|
|
940
3018
|
declare sql: FlowSQLRepository;
|
|
941
3019
|
declare logger: pino.Logger;
|
|
3020
|
+
|
|
3021
|
+
constructor() {
|
|
3022
|
+
super();
|
|
3023
|
+
this.defineMethod('getModel', (modelName: string, searchInPreviousEngines?: boolean) => {
|
|
3024
|
+
return this.engine.getModel(modelName, searchInPreviousEngines);
|
|
3025
|
+
});
|
|
3026
|
+
this.defineMethod('request', (options: RequestOptions) => {
|
|
3027
|
+
return this.api.request(options);
|
|
3028
|
+
});
|
|
3029
|
+
this.defineMethod(
|
|
3030
|
+
'runjs',
|
|
3031
|
+
async function (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) {
|
|
3032
|
+
const { preprocessTemplates, ...runnerOptions } = options || {};
|
|
3033
|
+
const mergedGlobals = { ...(runnerOptions?.globals || {}), ...(variables || {}) };
|
|
3034
|
+
const runner = await this.createJSRunner({
|
|
3035
|
+
...(runnerOptions || {}),
|
|
3036
|
+
globals: mergedGlobals,
|
|
3037
|
+
});
|
|
3038
|
+
// Enable by default; use `preprocessTemplates: false` to explicitly disable.
|
|
3039
|
+
const shouldPreprocessTemplates = preprocessTemplates !== false;
|
|
3040
|
+
const jsCode = await prepareRunJsCode(String(code ?? ''), { preprocessTemplates: shouldPreprocessTemplates });
|
|
3041
|
+
return runner.run(jsCode);
|
|
3042
|
+
},
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
942
3045
|
}
|
|
943
3046
|
|
|
944
3047
|
class BaseFlowModelContext extends BaseFlowEngineContext {
|
|
@@ -956,7 +3059,16 @@ class BaseFlowModelContext extends BaseFlowEngineContext {
|
|
|
956
3059
|
EventDefinition<TModel, TCtx>
|
|
957
3060
|
>;
|
|
958
3061
|
declare runAction: (actionName: string, params?: Record<string, any>) => Promise<any> | any;
|
|
3062
|
+
/**
|
|
3063
|
+
* @deprecated use `makeResource` instead
|
|
3064
|
+
*/
|
|
959
3065
|
declare createResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
|
|
3066
|
+
/**
|
|
3067
|
+
* Create a new resource instance without adding it to the context.
|
|
3068
|
+
* @param resourceType - The resource type.
|
|
3069
|
+
* @returns The resource instance.
|
|
3070
|
+
*/
|
|
3071
|
+
declare makeResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
|
|
960
3072
|
}
|
|
961
3073
|
|
|
962
3074
|
export class FlowEngineContext extends BaseFlowEngineContext {
|
|
@@ -976,25 +3088,30 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
976
3088
|
dataSourceManager.addDataSource(mainDataSource);
|
|
977
3089
|
this.defineProperty('engine', {
|
|
978
3090
|
value: this.engine,
|
|
3091
|
+
info: {
|
|
3092
|
+
description: 'FlowEngine instance.',
|
|
3093
|
+
detail: 'FlowEngine',
|
|
3094
|
+
},
|
|
979
3095
|
});
|
|
980
3096
|
this.defineProperty('sql', {
|
|
981
|
-
get: () => new FlowSQLRepository(
|
|
3097
|
+
get: (ctx) => new FlowSQLRepository(ctx),
|
|
3098
|
+
cache: false,
|
|
3099
|
+
info: {
|
|
3100
|
+
description: 'SQL helper (FlowSQLRepository).',
|
|
3101
|
+
detail: 'FlowSQLRepository',
|
|
3102
|
+
},
|
|
982
3103
|
});
|
|
983
3104
|
this.defineProperty('dataSourceManager', {
|
|
984
3105
|
value: dataSourceManager,
|
|
3106
|
+
info: {
|
|
3107
|
+
description: 'DataSourceManager instance.',
|
|
3108
|
+
detail: 'DataSourceManager',
|
|
3109
|
+
},
|
|
985
3110
|
});
|
|
986
3111
|
const i18n = new FlowI18n(this);
|
|
987
3112
|
this.defineMethod('t', (keyOrTemplate: string, options?: any) => {
|
|
988
3113
|
return i18n.translate(keyOrTemplate, options);
|
|
989
3114
|
});
|
|
990
|
-
this.defineMethod('runjs', async (code, variables, options?: JSRunnerOptions) => {
|
|
991
|
-
const mergedGlobals = { ...(options?.globals || {}), ...(variables || {}) };
|
|
992
|
-
const runner = (await (this as any).createJSRunner({
|
|
993
|
-
...(options || {}),
|
|
994
|
-
globals: mergedGlobals,
|
|
995
|
-
})) as JSRunner;
|
|
996
|
-
return runner.run(code);
|
|
997
|
-
});
|
|
998
3115
|
this.defineMethod('renderJson', function (template: any) {
|
|
999
3116
|
return this.resolveJsonTemplate(template);
|
|
1000
3117
|
});
|
|
@@ -1030,6 +3147,22 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1030
3147
|
const needServer = Object.keys(serverVarPaths).length > 0;
|
|
1031
3148
|
let serverResolved = template;
|
|
1032
3149
|
if (needServer) {
|
|
3150
|
+
const inferRecordRefWithMeta = (ctx: any): RecordRef | undefined => {
|
|
3151
|
+
const ref = inferRecordRef(ctx as any);
|
|
3152
|
+
if (ref) return ref as RecordRef;
|
|
3153
|
+
try {
|
|
3154
|
+
const tk = ctx?.resource?.getMeta?.('currentFilterByTk');
|
|
3155
|
+
if (typeof tk === 'undefined' || tk === null) return undefined;
|
|
3156
|
+
const collection =
|
|
3157
|
+
ctx?.collection?.name || ctx?.resource?.getResourceName?.()?.split?.('.')?.slice?.(-1)?.[0];
|
|
3158
|
+
if (!collection) return undefined;
|
|
3159
|
+
const dataSourceKey = ctx?.collection?.dataSourceKey || ctx?.resource?.getDataSourceKey?.();
|
|
3160
|
+
return { collection, dataSourceKey, filterByTk: tk } as RecordRef;
|
|
3161
|
+
} catch (_) {
|
|
3162
|
+
return undefined;
|
|
3163
|
+
}
|
|
3164
|
+
};
|
|
3165
|
+
|
|
1033
3166
|
const collectFromMeta = async (): Promise<Record<string, any>> => {
|
|
1034
3167
|
const out: Record<string, any> = {};
|
|
1035
3168
|
try {
|
|
@@ -1069,7 +3202,62 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1069
3202
|
};
|
|
1070
3203
|
|
|
1071
3204
|
const inputFromMeta = await collectFromMeta();
|
|
1072
|
-
const autoInput = { ...inputFromMeta }
|
|
3205
|
+
const autoInput = { ...inputFromMeta } as Record<string, any>;
|
|
3206
|
+
|
|
3207
|
+
// Special-case: formValues
|
|
3208
|
+
// If server needs to resolve some formValues paths but meta params only cover association anchors
|
|
3209
|
+
// (e.g. formValues.customer) and some top-level paths are missing (e.g. formValues.status),
|
|
3210
|
+
// inject a top-level record anchor (formValues -> { collection, filterByTk, fields/appends }) so server can fetch DB values.
|
|
3211
|
+
// This anchor MUST be selective (fields/appends derived from serverVarPaths['formValues']) to avoid server overriding
|
|
3212
|
+
// client-only values for configured form fields in the same template.
|
|
3213
|
+
try {
|
|
3214
|
+
const varName = 'formValues';
|
|
3215
|
+
const neededPaths = serverVarPaths[varName] || [];
|
|
3216
|
+
if (neededPaths.length) {
|
|
3217
|
+
const requiredTop = new Set<string>();
|
|
3218
|
+
for (const p of neededPaths) {
|
|
3219
|
+
const top = topLevelOf(p);
|
|
3220
|
+
if (top) requiredTop.add(top);
|
|
3221
|
+
}
|
|
3222
|
+
const metaOut = inputFromMeta?.[varName];
|
|
3223
|
+
const builtTop = new Set<string>();
|
|
3224
|
+
if (metaOut && typeof metaOut === 'object' && !Array.isArray(metaOut) && !isRecordRefLike(metaOut)) {
|
|
3225
|
+
Object.keys(metaOut).forEach((k) => builtTop.add(k));
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
const missing = [...requiredTop].filter((k) => !builtTop.has(k));
|
|
3229
|
+
if (missing.length) {
|
|
3230
|
+
const ref = inferRecordRefWithMeta(this);
|
|
3231
|
+
if (ref) {
|
|
3232
|
+
const { generatedFields, generatedAppends } = inferSelectsFromUsage(neededPaths);
|
|
3233
|
+
const recordRef: RecordRef = {
|
|
3234
|
+
...ref,
|
|
3235
|
+
fields: generatedFields,
|
|
3236
|
+
appends: generatedAppends,
|
|
3237
|
+
};
|
|
3238
|
+
|
|
3239
|
+
// Preserve existing association anchors by lifting them to dotted keys before overwriting formValues
|
|
3240
|
+
const existing = autoInput[varName];
|
|
3241
|
+
if (
|
|
3242
|
+
existing &&
|
|
3243
|
+
typeof existing === 'object' &&
|
|
3244
|
+
!Array.isArray(existing) &&
|
|
3245
|
+
!isRecordRefLike(existing)
|
|
3246
|
+
) {
|
|
3247
|
+
for (const [k, v] of Object.entries(existing)) {
|
|
3248
|
+
autoInput[`${varName}.${k}`] = v;
|
|
3249
|
+
}
|
|
3250
|
+
delete autoInput[varName];
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
autoInput[varName] = recordRef;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
} catch (_) {
|
|
3258
|
+
// ignore
|
|
3259
|
+
}
|
|
3260
|
+
|
|
1073
3261
|
const autoContextParams = Object.keys(autoInput).length
|
|
1074
3262
|
? _buildServerContextParams(this, autoInput)
|
|
1075
3263
|
: undefined;
|
|
@@ -1100,6 +3288,23 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1100
3288
|
|
|
1101
3289
|
return resolveExpressions(serverResolved, this);
|
|
1102
3290
|
});
|
|
3291
|
+
|
|
3292
|
+
// Helper: resolve a single ctx expression value via resolveJsonTemplate behavior.
|
|
3293
|
+
// Example: await ctx.getVar('ctx.record.id')
|
|
3294
|
+
this.defineMethod(
|
|
3295
|
+
'getVar',
|
|
3296
|
+
async function (this: BaseFlowEngineContext, varPath: string) {
|
|
3297
|
+
const raw = typeof varPath === 'string' ? varPath : String(varPath ?? '');
|
|
3298
|
+
const s = raw.trim();
|
|
3299
|
+
if (!s) return undefined;
|
|
3300
|
+
// Preferred input: 'ctx.xxx.yyy' (expression), consistent with envs.getVar outputs.
|
|
3301
|
+
if (s !== 'ctx' && !s.startsWith('ctx.')) {
|
|
3302
|
+
throw new Error(`ctx.getVar(path) expects an expression starting with "ctx.", got: "${s}"`);
|
|
3303
|
+
}
|
|
3304
|
+
return this.resolveJsonTemplate(`{{ ${s} }}` as any);
|
|
3305
|
+
},
|
|
3306
|
+
'Resolve a ctx expression value by path (expression starts with "ctx.").',
|
|
3307
|
+
);
|
|
1103
3308
|
this.defineProperty('requirejs', {
|
|
1104
3309
|
get: () => this.app?.requirejs?.requirejs,
|
|
1105
3310
|
});
|
|
@@ -1181,71 +3386,79 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1181
3386
|
user: this.user,
|
|
1182
3387
|
}),
|
|
1183
3388
|
});
|
|
1184
|
-
this.
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
3389
|
+
this.defineProperty('date', {
|
|
3390
|
+
get: () => {
|
|
3391
|
+
const createBranch = (prefix: string[]) => {
|
|
3392
|
+
return new Proxy(
|
|
3393
|
+
{},
|
|
3394
|
+
{
|
|
3395
|
+
get: (_target, prop) => {
|
|
3396
|
+
if (typeof prop !== 'string') return undefined;
|
|
3397
|
+
const nextPath = [...prefix, prop];
|
|
3398
|
+
if (!isCtxDatePathPrefix(nextPath)) {
|
|
3399
|
+
return undefined;
|
|
3400
|
+
}
|
|
3401
|
+
const resolved = resolveCtxDatePath(nextPath);
|
|
3402
|
+
if (typeof resolved !== 'undefined') {
|
|
3403
|
+
return resolved;
|
|
3404
|
+
}
|
|
3405
|
+
return createBranch(nextPath);
|
|
3406
|
+
},
|
|
3407
|
+
},
|
|
3408
|
+
);
|
|
3409
|
+
};
|
|
1192
3410
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
link.onload = () => resolve(null);
|
|
1197
|
-
link.onerror = () => reject(new Error(`Failed to load CSS: ${url}`));
|
|
1198
|
-
document.head.appendChild(link);
|
|
1199
|
-
});
|
|
3411
|
+
return createBranch(['date']);
|
|
3412
|
+
},
|
|
3413
|
+
cache: false,
|
|
1200
3414
|
});
|
|
3415
|
+
this.defineMethod(
|
|
3416
|
+
'loadCSS',
|
|
3417
|
+
async (href: string) => {
|
|
3418
|
+
const url = resolveModuleUrl(href);
|
|
3419
|
+
return new Promise((resolve, reject) => {
|
|
3420
|
+
// Check if CSS is already loaded
|
|
3421
|
+
const existingLink = document.querySelector(`link[href="${url}"]`);
|
|
3422
|
+
if (existingLink) {
|
|
3423
|
+
resolve(null);
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
const link = document.createElement('link');
|
|
3428
|
+
link.rel = 'stylesheet';
|
|
3429
|
+
link.href = url;
|
|
3430
|
+
link.onload = () => resolve(null);
|
|
3431
|
+
link.onerror = () => reject(new Error(`Failed to load CSS: ${url}`));
|
|
3432
|
+
document.head.appendChild(link);
|
|
3433
|
+
});
|
|
3434
|
+
},
|
|
3435
|
+
{
|
|
3436
|
+
description: 'Load a CSS file by URL (browser only).',
|
|
3437
|
+
params: [{ name: 'href', type: 'string', description: 'CSS URL.' }],
|
|
3438
|
+
returns: { type: 'Promise<void>' },
|
|
3439
|
+
completion: { insertText: "await ctx.loadCSS('https://example.com/style.css')" },
|
|
3440
|
+
examples: ["await ctx.loadCSS('https://example.com/style.css');"],
|
|
3441
|
+
},
|
|
3442
|
+
);
|
|
1201
3443
|
this.defineMethod('requireAsync', async (url: string) => {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
[url],
|
|
1209
|
-
(...args: any[]) => {
|
|
1210
|
-
resolve(args[0]);
|
|
1211
|
-
},
|
|
1212
|
-
reject,
|
|
1213
|
-
);
|
|
1214
|
-
});
|
|
3444
|
+
// 判断是否为 CSS 文件(支持 example.css?v=123 等形式)
|
|
3445
|
+
if (isCssFile(url)) {
|
|
3446
|
+
return this.loadCSS(url);
|
|
3447
|
+
}
|
|
3448
|
+
const u = resolveModuleUrl(url, { raw: true });
|
|
3449
|
+
return await runjsRequireAsync(this.requirejs, u);
|
|
1215
3450
|
});
|
|
1216
3451
|
// 动态按 URL 加载 ESM 模块
|
|
1217
3452
|
// - 使用 Vite / Webpack ignore 注释,避免被预打包或重写
|
|
1218
|
-
// -
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if (cache.has(u)) return cache.get(u)!;
|
|
1228
|
-
// 尝试使用原生 dynamic import(加上 vite/webpack 的 ignore 注释)
|
|
1229
|
-
const nativeImport = () => import(/* @vite-ignore */ /* webpackIgnore: true */ u);
|
|
1230
|
-
// 兜底方案:通过 eval 在运行时构造 import,避免被打包器接管
|
|
1231
|
-
const evalImport = () => {
|
|
1232
|
-
const importer = (0, eval)('u => import(u)');
|
|
1233
|
-
return importer(u);
|
|
1234
|
-
};
|
|
1235
|
-
const p = (async () => {
|
|
1236
|
-
try {
|
|
1237
|
-
return await nativeImport();
|
|
1238
|
-
} catch (err: any) {
|
|
1239
|
-
// 常见于打包产物仍然拦截了 dynamic import 或开发态插件未识别 ignore 注释
|
|
1240
|
-
try {
|
|
1241
|
-
return await evalImport();
|
|
1242
|
-
} catch (err2) {
|
|
1243
|
-
throw err2 || err;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
})();
|
|
1247
|
-
cache.set(u, p);
|
|
1248
|
-
return p;
|
|
3453
|
+
// - 通常返回模块命名空间对象(包含 default 与命名导出);
|
|
3454
|
+
// 若模块只有 default 一个导出,则会直接返回 default 值以提升易用性(无需再访问 .default)
|
|
3455
|
+
this.defineMethod('importAsync', async function (this: any, url: string) {
|
|
3456
|
+
// 判断是否为 CSS 文件(支持 example.css?v=123 等形式)
|
|
3457
|
+
if (isCssFile(url)) {
|
|
3458
|
+
return this.loadCSS(url);
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
return await runjsImportModule(this, url, { importer: runjsImportAsync });
|
|
1249
3462
|
});
|
|
1250
3463
|
this.defineMethod('createJSRunner', async function (options?: JSRunnerOptions) {
|
|
1251
3464
|
try {
|
|
@@ -1254,17 +3467,24 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1254
3467
|
} catch (_) {
|
|
1255
3468
|
// ignore if setup is not available
|
|
1256
3469
|
}
|
|
1257
|
-
const version =
|
|
3470
|
+
const version = options?.version || 'v1';
|
|
1258
3471
|
const modelClass = getModelClassName(this);
|
|
1259
|
-
const Ctor =
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
3472
|
+
const Ctor: new (delegate: any) => any = RunJSContextRegistry.resolve(version, modelClass) || FlowRunJSContext;
|
|
3473
|
+
const runCtx = new Ctor(this);
|
|
3474
|
+
runCtx.defineMethod('t', (key: string, options?: any) => {
|
|
3475
|
+
return this.t(key, { ns: 'runjs', ...options });
|
|
3476
|
+
});
|
|
3477
|
+
|
|
3478
|
+
let doc: RunJSDocMeta = {};
|
|
3479
|
+
try {
|
|
3480
|
+
const locale = (this as any)?.api?.auth?.locale || (this as any)?.i18n?.language || (this as any)?.locale;
|
|
3481
|
+
if ((Ctor as any)?.getDoc?.length) doc = (Ctor as any).getDoc(locale) || {};
|
|
3482
|
+
else doc = (Ctor as any)?.getDoc?.() || {};
|
|
3483
|
+
} catch (_) {
|
|
3484
|
+
doc = {};
|
|
3485
|
+
}
|
|
3486
|
+
const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
|
|
3487
|
+
const globals: Record<string, any> = { ctx: deprecatedCtx, ...(options?.globals || {}) };
|
|
1268
3488
|
const { timeoutMs } = options || {};
|
|
1269
3489
|
return new JSRunner({ globals, timeoutMs });
|
|
1270
3490
|
});
|
|
@@ -1282,57 +3502,6 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1282
3502
|
return this.engine.getEvents();
|
|
1283
3503
|
});
|
|
1284
3504
|
|
|
1285
|
-
// // Date variables (for variable selector meta tree)
|
|
1286
|
-
// this.defineProperty('date', {
|
|
1287
|
-
// get: () => {
|
|
1288
|
-
// const vars = getDateVars() as Record<string, any>;
|
|
1289
|
-
// // align with client options: add dayBeforeYesterday
|
|
1290
|
-
// vars.dayBeforeYesterday = toUnit('day', -2);
|
|
1291
|
-
// const now = new Date().toISOString();
|
|
1292
|
-
// const out: Record<string, any> = {};
|
|
1293
|
-
// for (const [k, v] of Object.entries(vars)) {
|
|
1294
|
-
// try {
|
|
1295
|
-
// out[k] = typeof v === 'function' ? v({ now }) : v;
|
|
1296
|
-
// } catch (e) {
|
|
1297
|
-
// // ignore
|
|
1298
|
-
// }
|
|
1299
|
-
// }
|
|
1300
|
-
// return out;
|
|
1301
|
-
// },
|
|
1302
|
-
// meta: () => {
|
|
1303
|
-
// const title = this.t('Date variables');
|
|
1304
|
-
// const mk = (t: string) => ({ type: 'any', title: this.t(t) });
|
|
1305
|
-
// return {
|
|
1306
|
-
// type: 'object',
|
|
1307
|
-
// title,
|
|
1308
|
-
// properties: {
|
|
1309
|
-
// now: mk('Current time'),
|
|
1310
|
-
// dayBeforeYesterday: mk('Day before yesterday'),
|
|
1311
|
-
// yesterday: mk('Yesterday'),
|
|
1312
|
-
// today: mk('Today'),
|
|
1313
|
-
// tomorrow: mk('Tomorrow'),
|
|
1314
|
-
// lastIsoWeek: mk('Last week'),
|
|
1315
|
-
// thisIsoWeek: mk('This week'),
|
|
1316
|
-
// nextIsoWeek: mk('Next week'),
|
|
1317
|
-
// lastMonth: mk('Last month'),
|
|
1318
|
-
// thisMonth: mk('This month'),
|
|
1319
|
-
// nextMonth: mk('Next month'),
|
|
1320
|
-
// lastQuarter: mk('Last quarter'),
|
|
1321
|
-
// thisQuarter: mk('This quarter'),
|
|
1322
|
-
// nextQuarter: mk('Next quarter'),
|
|
1323
|
-
// lastYear: mk('Last year'),
|
|
1324
|
-
// thisYear: mk('This year'),
|
|
1325
|
-
// nextYear: mk('Next year'),
|
|
1326
|
-
// last7Days: mk('Last 7 days'),
|
|
1327
|
-
// next7Days: mk('Next 7 days'),
|
|
1328
|
-
// last30Days: mk('Last 30 days'),
|
|
1329
|
-
// next30Days: mk('Next 30 days'),
|
|
1330
|
-
// last90Days: mk('Last 90 days'),
|
|
1331
|
-
// next90Days: mk('Next 90 days'),
|
|
1332
|
-
// },
|
|
1333
|
-
// } as PropertyMeta;
|
|
1334
|
-
// },
|
|
1335
|
-
// });
|
|
1336
3505
|
this.defineMethod(
|
|
1337
3506
|
'runAction',
|
|
1338
3507
|
async function (this: BaseFlowEngineContext, actionName: string, params?: Record<string, any>) {
|
|
@@ -1375,17 +3544,34 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1375
3544
|
context: this.createProxy(),
|
|
1376
3545
|
});
|
|
1377
3546
|
});
|
|
3547
|
+
this.defineMethod('makeResource', function (this: BaseFlowEngineContext, resourceType) {
|
|
3548
|
+
return this.engine.createResource(resourceType, {
|
|
3549
|
+
context: this.createProxy(),
|
|
3550
|
+
});
|
|
3551
|
+
});
|
|
1378
3552
|
// Provide useResource in base engine context so RunJS can call it directly
|
|
3553
|
+
this.defineMethod(
|
|
3554
|
+
'initResource',
|
|
3555
|
+
function (
|
|
3556
|
+
this: BaseFlowEngineContext,
|
|
3557
|
+
className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
|
|
3558
|
+
) {
|
|
3559
|
+
if (!this.has('resource')) {
|
|
3560
|
+
this.defineProperty('resource', {
|
|
3561
|
+
get: () => this.createResource(className),
|
|
3562
|
+
});
|
|
3563
|
+
}
|
|
3564
|
+
return this.resource;
|
|
3565
|
+
},
|
|
3566
|
+
);
|
|
3567
|
+
// @deprecated use `initResource` instead
|
|
1379
3568
|
this.defineMethod(
|
|
1380
3569
|
'useResource',
|
|
1381
3570
|
function (
|
|
1382
3571
|
this: BaseFlowEngineContext,
|
|
1383
3572
|
className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
|
|
1384
3573
|
) {
|
|
1385
|
-
|
|
1386
|
-
this.defineProperty('resource', {
|
|
1387
|
-
get: () => this.createResource(className),
|
|
1388
|
-
});
|
|
3574
|
+
return this.initResource(className);
|
|
1389
3575
|
},
|
|
1390
3576
|
);
|
|
1391
3577
|
}
|
|
@@ -1401,15 +3587,12 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
1401
3587
|
this.defineMethod('onRefReady', (ref, cb, timeout) => {
|
|
1402
3588
|
this.engine.reactView.onRefReady(ref, cb, timeout);
|
|
1403
3589
|
});
|
|
1404
|
-
this.defineMethod('runjs', async (code, variables, options?: { version?: string }) => {
|
|
1405
|
-
const runner = await this.createJSRunner({
|
|
1406
|
-
globals: variables,
|
|
1407
|
-
version: options?.version,
|
|
1408
|
-
});
|
|
1409
|
-
return runner.run(code);
|
|
1410
|
-
});
|
|
1411
3590
|
this.defineProperty('model', {
|
|
1412
3591
|
value: model,
|
|
3592
|
+
info: {
|
|
3593
|
+
description: 'Current FlowModel instance.',
|
|
3594
|
+
detail: 'FlowModel',
|
|
3595
|
+
},
|
|
1413
3596
|
});
|
|
1414
3597
|
// 提供稳定的 ref 实例,确保渲染端与运行时上下文使用同一对象
|
|
1415
3598
|
const stableRef = createRef<HTMLDivElement>();
|
|
@@ -1418,6 +3601,10 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
1418
3601
|
this.model['_refCreated'] = true;
|
|
1419
3602
|
return stableRef;
|
|
1420
3603
|
},
|
|
3604
|
+
info: {
|
|
3605
|
+
description: 'Stable React ref for the view container.',
|
|
3606
|
+
detail: 'React.RefObject<HTMLDivElement>',
|
|
3607
|
+
},
|
|
1421
3608
|
});
|
|
1422
3609
|
this.defineMethod('openView', async function (uid: string, options) {
|
|
1423
3610
|
const opts = { ...options };
|
|
@@ -1478,8 +3665,17 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
1478
3665
|
engineCtx: this.engine.context,
|
|
1479
3666
|
};
|
|
1480
3667
|
model.context.defineProperty('view', { value: pendingView });
|
|
3668
|
+
// 默认按 click 打开,但兼容 popupSettings 绑定到其他事件(例如 DuplicateActionModel 监听 openDuplicatePopup)。
|
|
3669
|
+
const popupFlow = model.getFlow?.('popupSettings');
|
|
3670
|
+
const on = (popupFlow as any)?.on;
|
|
3671
|
+
let openEventName = 'click';
|
|
3672
|
+
if (typeof on === 'string' && on) {
|
|
3673
|
+
openEventName = on;
|
|
3674
|
+
} else if (on && typeof on === 'object' && typeof (on as any).eventName === 'string' && (on as any).eventName) {
|
|
3675
|
+
openEventName = (on as any).eventName;
|
|
3676
|
+
}
|
|
1481
3677
|
await model.dispatchEvent(
|
|
1482
|
-
|
|
3678
|
+
openEventName,
|
|
1483
3679
|
{
|
|
1484
3680
|
// navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
|
|
1485
3681
|
// ...this.model?.['getInputArgs']?.(), // 避免部分关系字段信息丢失, 仿照 ClickableCollectionField 做法
|
|
@@ -1538,12 +3734,16 @@ export class FlowForkModelContext extends BaseFlowModelContext {
|
|
|
1538
3734
|
throw new Error('Invalid FlowModel instance');
|
|
1539
3735
|
}
|
|
1540
3736
|
super();
|
|
1541
|
-
this.addDelegate(
|
|
3737
|
+
this.addDelegate(this.master.context);
|
|
1542
3738
|
this.defineMethod('onRefReady', (ref, cb, timeout) => {
|
|
1543
3739
|
this.engine.reactView.onRefReady(ref, cb, timeout);
|
|
1544
3740
|
});
|
|
1545
3741
|
this.defineProperty('model', {
|
|
1546
3742
|
get: () => this.fork,
|
|
3743
|
+
info: {
|
|
3744
|
+
description: 'Current ForkFlowModel instance (as model).',
|
|
3745
|
+
detail: 'ForkFlowModel',
|
|
3746
|
+
},
|
|
1547
3747
|
});
|
|
1548
3748
|
// 提供稳定的 ref 实例,确保渲染端与运行时上下文使用同一对象
|
|
1549
3749
|
const stableRef = createRef<HTMLDivElement>();
|
|
@@ -1552,13 +3752,10 @@ export class FlowForkModelContext extends BaseFlowModelContext {
|
|
|
1552
3752
|
this.fork['_refCreated'] = true;
|
|
1553
3753
|
return stableRef;
|
|
1554
3754
|
},
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
version: options?.version,
|
|
1560
|
-
});
|
|
1561
|
-
return runner.run(code);
|
|
3755
|
+
info: {
|
|
3756
|
+
description: 'Stable React ref for the view container.',
|
|
3757
|
+
detail: 'React.RefObject<HTMLDivElement>',
|
|
3758
|
+
},
|
|
1562
3759
|
});
|
|
1563
3760
|
}
|
|
1564
3761
|
}
|
|
@@ -1569,7 +3766,19 @@ export class FlowRuntimeContext<
|
|
|
1569
3766
|
> extends BaseFlowModelContext {
|
|
1570
3767
|
declare steps: Record<string, { params: Record<string, any>; uiSchema?: any; result?: any }>;
|
|
1571
3768
|
stepResults: Record<string, any> = {};
|
|
1572
|
-
|
|
3769
|
+
/**
|
|
3770
|
+
* @deprecated use `initResource` instead
|
|
3771
|
+
*/
|
|
3772
|
+
declare useResource: (
|
|
3773
|
+
className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
|
|
3774
|
+
) => void;
|
|
3775
|
+
/**
|
|
3776
|
+
* Initialize a resource instance without adding it to the context.
|
|
3777
|
+
* @param className - The resource class name.
|
|
3778
|
+
*/
|
|
3779
|
+
declare initResource: (
|
|
3780
|
+
className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
|
|
3781
|
+
) => void;
|
|
1573
3782
|
declare getStepParams: (stepKey: string) => Record<string, any>;
|
|
1574
3783
|
declare setStepParams: (stepKey: string, params?: any) => void;
|
|
1575
3784
|
declare getStepResults: (stepKey: string) => any;
|
|
@@ -1591,15 +3800,15 @@ export class FlowRuntimeContext<
|
|
|
1591
3800
|
return _.get(this.steps, [stepKey, 'result']);
|
|
1592
3801
|
});
|
|
1593
3802
|
this.defineMethod(
|
|
1594
|
-
'
|
|
3803
|
+
'initResource',
|
|
1595
3804
|
(className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource') => {
|
|
1596
3805
|
if (model.context.has('resource')) {
|
|
1597
|
-
console.
|
|
3806
|
+
console.log(`[FlowRuntimeContext] useResource - resource already defined in context: ${className}`);
|
|
1598
3807
|
return;
|
|
1599
3808
|
}
|
|
1600
3809
|
model.context.defineProperty('resource', {
|
|
1601
3810
|
get: () => {
|
|
1602
|
-
return this.
|
|
3811
|
+
return this.makeResource(className);
|
|
1603
3812
|
},
|
|
1604
3813
|
});
|
|
1605
3814
|
if (!model['resource']) {
|
|
@@ -1607,6 +3816,13 @@ export class FlowRuntimeContext<
|
|
|
1607
3816
|
}
|
|
1608
3817
|
},
|
|
1609
3818
|
);
|
|
3819
|
+
// @deprecated use `initResource` instead
|
|
3820
|
+
this.defineMethod(
|
|
3821
|
+
'useResource',
|
|
3822
|
+
(className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource') => {
|
|
3823
|
+
return this.initResource(className);
|
|
3824
|
+
},
|
|
3825
|
+
);
|
|
1610
3826
|
this.defineProperty('resource', {
|
|
1611
3827
|
get: () => model['resource'] || model.context['resource'],
|
|
1612
3828
|
cache: false,
|
|
@@ -1614,13 +3830,6 @@ export class FlowRuntimeContext<
|
|
|
1614
3830
|
this.defineMethod('onRefReady', (ref, cb, timeout) => {
|
|
1615
3831
|
this.engine.reactView.onRefReady(ref, cb, timeout);
|
|
1616
3832
|
});
|
|
1617
|
-
this.defineMethod('runjs', async (code, variables, options?: { version?: string }) => {
|
|
1618
|
-
const runner = await this.createJSRunner({
|
|
1619
|
-
globals: variables,
|
|
1620
|
-
version: options?.version,
|
|
1621
|
-
});
|
|
1622
|
-
return runner.run(code);
|
|
1623
|
-
});
|
|
1624
3833
|
}
|
|
1625
3834
|
|
|
1626
3835
|
protected _getOwnProperty(key: string): any {
|
|
@@ -1654,7 +3863,7 @@ export class FlowRuntimeContext<
|
|
|
1654
3863
|
}
|
|
1655
3864
|
|
|
1656
3865
|
exit() {
|
|
1657
|
-
throw new
|
|
3866
|
+
throw new FlowExitAllException(this.flowKey, this.model.uid);
|
|
1658
3867
|
}
|
|
1659
3868
|
|
|
1660
3869
|
exitAll() {
|
|
@@ -1673,6 +3882,16 @@ export type RunJSDocCompletionDoc = {
|
|
|
1673
3882
|
insertText?: string;
|
|
1674
3883
|
};
|
|
1675
3884
|
|
|
3885
|
+
export type RunJSDocHiddenDoc = boolean | ((ctx: any) => boolean | Promise<boolean>);
|
|
3886
|
+
|
|
3887
|
+
// `hidden` is the single visibility entrypoint for RunJSDoc property docs:
|
|
3888
|
+
// - boolean: hide the whole node and its subtree
|
|
3889
|
+
// - string[]: hide specific subpaths under the node (relative dot-paths)
|
|
3890
|
+
export type RunJSDocHiddenOrPathsDoc =
|
|
3891
|
+
| boolean
|
|
3892
|
+
| string[]
|
|
3893
|
+
| ((ctx: any) => boolean | string[] | Promise<boolean | string[]>);
|
|
3894
|
+
|
|
1676
3895
|
export type RunJSDocPropertyDoc =
|
|
1677
3896
|
| string
|
|
1678
3897
|
| {
|
|
@@ -1681,7 +3900,14 @@ export type RunJSDocPropertyDoc =
|
|
|
1681
3900
|
type?: string;
|
|
1682
3901
|
examples?: string[];
|
|
1683
3902
|
completion?: RunJSDocCompletionDoc;
|
|
3903
|
+
ref?: FlowContextDocRef;
|
|
3904
|
+
deprecated?: FlowDeprecationDoc;
|
|
3905
|
+
params?: FlowContextDocParam[];
|
|
3906
|
+
returns?: FlowContextDocReturn;
|
|
1684
3907
|
properties?: Record<string, RunJSDocPropertyDoc>;
|
|
3908
|
+
hidden?: RunJSDocHiddenOrPathsDoc;
|
|
3909
|
+
disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
|
|
3910
|
+
disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
|
|
1685
3911
|
};
|
|
1686
3912
|
|
|
1687
3913
|
export type RunJSDocMethodDoc =
|
|
@@ -1691,6 +3917,13 @@ export type RunJSDocMethodDoc =
|
|
|
1691
3917
|
detail?: string;
|
|
1692
3918
|
examples?: string[];
|
|
1693
3919
|
completion?: RunJSDocCompletionDoc;
|
|
3920
|
+
ref?: FlowContextDocRef;
|
|
3921
|
+
deprecated?: FlowDeprecationDoc;
|
|
3922
|
+
params?: FlowContextDocParam[];
|
|
3923
|
+
returns?: FlowContextDocReturn;
|
|
3924
|
+
hidden?: RunJSDocHiddenDoc;
|
|
3925
|
+
disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
|
|
3926
|
+
disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
|
|
1694
3927
|
};
|
|
1695
3928
|
|
|
1696
3929
|
export type RunJSDocMeta = {
|
|
@@ -1717,13 +3950,518 @@ function __runjsDeepMerge(base: any, patch: any) {
|
|
|
1717
3950
|
}
|
|
1718
3951
|
return out;
|
|
1719
3952
|
}
|
|
3953
|
+
|
|
3954
|
+
function __isPlainObject(val: any): val is Record<string, any> {
|
|
3955
|
+
return !!val && typeof val === 'object' && !Array.isArray(val);
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
type RunJSDeprecatedTreeNode = {
|
|
3959
|
+
deprecated?: FlowDeprecationDoc;
|
|
3960
|
+
children?: Record<string, RunJSDeprecatedTreeNode>;
|
|
3961
|
+
};
|
|
3962
|
+
|
|
3963
|
+
function __isPromiseLike(v: any): v is Promise<any> {
|
|
3964
|
+
return !!v && (typeof v === 'object' || typeof v === 'function') && typeof (v as any).then === 'function';
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
function __normalizeDeprecationDoc(v: any): FlowDeprecationDoc | undefined {
|
|
3968
|
+
if (v === true) return true;
|
|
3969
|
+
if (!v) return undefined;
|
|
3970
|
+
if (__isPlainObject(v)) return v as any;
|
|
3971
|
+
return undefined;
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
function __addDeprecatedPath(root: RunJSDeprecatedTreeNode, path: string[], deprecated: FlowDeprecationDoc) {
|
|
3975
|
+
if (!Array.isArray(path) || !path.length) return;
|
|
3976
|
+
let cur = root;
|
|
3977
|
+
for (const seg of path) {
|
|
3978
|
+
if (!seg) return;
|
|
3979
|
+
cur.children = cur.children || {};
|
|
3980
|
+
cur.children[seg] = cur.children[seg] || {};
|
|
3981
|
+
cur = cur.children[seg];
|
|
3982
|
+
}
|
|
3983
|
+
cur.deprecated = deprecated;
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
function __mergeDeprecatedTree(base: RunJSDeprecatedTreeNode, patch: RunJSDeprecatedTreeNode) {
|
|
3987
|
+
if (patch.deprecated !== undefined) base.deprecated = patch.deprecated;
|
|
3988
|
+
const pChildren = patch.children || {};
|
|
3989
|
+
const keys = Object.keys(pChildren);
|
|
3990
|
+
if (!keys.length) return;
|
|
3991
|
+
base.children = base.children || {};
|
|
3992
|
+
for (const k of keys) {
|
|
3993
|
+
base.children[k] = base.children[k] || {};
|
|
3994
|
+
__mergeDeprecatedTree(base.children[k], pChildren[k]);
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
function __buildDeprecatedTreeFromRunJSDoc(doc?: RunJSDocMeta): RunJSDeprecatedTreeNode {
|
|
3999
|
+
const root: RunJSDeprecatedTreeNode = {};
|
|
4000
|
+
if (!doc) return root;
|
|
4001
|
+
|
|
4002
|
+
const walkProps = (props: any, parentPath: string[]) => {
|
|
4003
|
+
if (!__isPlainObject(props)) return;
|
|
4004
|
+
for (const [key, raw] of Object.entries(props)) {
|
|
4005
|
+
if (!key) continue;
|
|
4006
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
|
4007
|
+
const node = raw as any;
|
|
4008
|
+
const dep = __normalizeDeprecationDoc(node.deprecated);
|
|
4009
|
+
if (dep) __addDeprecatedPath(root, [...parentPath, key], dep);
|
|
4010
|
+
if (__isPlainObject(node.properties)) {
|
|
4011
|
+
walkProps(node.properties, [...parentPath, key]);
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
};
|
|
4015
|
+
|
|
4016
|
+
const walkMethods = (methods: any) => {
|
|
4017
|
+
if (!__isPlainObject(methods)) return;
|
|
4018
|
+
for (const [key, raw] of Object.entries(methods)) {
|
|
4019
|
+
if (!key) continue;
|
|
4020
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
|
4021
|
+
const node = raw as any;
|
|
4022
|
+
const dep = __normalizeDeprecationDoc(node.deprecated);
|
|
4023
|
+
if (dep) __addDeprecatedPath(root, [key], dep);
|
|
4024
|
+
}
|
|
4025
|
+
};
|
|
4026
|
+
|
|
4027
|
+
walkProps((doc as any).properties, []);
|
|
4028
|
+
walkMethods((doc as any).methods);
|
|
4029
|
+
|
|
4030
|
+
return root;
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
function __buildDeprecatedTreeFromFlowContextInfos(ctx: any): RunJSDeprecatedTreeNode {
|
|
4034
|
+
const root: RunJSDeprecatedTreeNode = {};
|
|
4035
|
+
const visited = new WeakSet<any>();
|
|
4036
|
+
|
|
4037
|
+
const collectInfoProperties = (basePath: string[], props: any) => {
|
|
4038
|
+
if (!__isPlainObject(props)) return;
|
|
4039
|
+
for (const [key, raw] of Object.entries(props)) {
|
|
4040
|
+
if (!key) continue;
|
|
4041
|
+
if (typeof raw === 'string') continue;
|
|
4042
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
|
4043
|
+
const node = raw as any;
|
|
4044
|
+
const dep = __normalizeDeprecationDoc(node.deprecated);
|
|
4045
|
+
if (dep) __addDeprecatedPath(root, [...basePath, key], dep);
|
|
4046
|
+
if (__isPlainObject(node.properties)) {
|
|
4047
|
+
collectInfoProperties([...basePath, key], node.properties);
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
};
|
|
4051
|
+
|
|
4052
|
+
const walk = (c: any) => {
|
|
4053
|
+
if (!c || (typeof c !== 'object' && typeof c !== 'function')) return;
|
|
4054
|
+
if (visited.has(c)) return;
|
|
4055
|
+
visited.add(c);
|
|
4056
|
+
|
|
4057
|
+
const methodInfos = (c as any)._methodInfos;
|
|
4058
|
+
if (__isPlainObject(methodInfos)) {
|
|
4059
|
+
for (const [name, info] of Object.entries(methodInfos)) {
|
|
4060
|
+
if (!name) continue;
|
|
4061
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) continue;
|
|
4062
|
+
const dep = __normalizeDeprecationDoc((info as any).deprecated);
|
|
4063
|
+
if (dep) __addDeprecatedPath(root, [name], dep);
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
const props = (c as any)._props;
|
|
4068
|
+
if (__isPlainObject(props)) {
|
|
4069
|
+
for (const [name, opt] of Object.entries(props)) {
|
|
4070
|
+
if (!name) continue;
|
|
4071
|
+
const info = (opt as any)?.info;
|
|
4072
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) continue;
|
|
4073
|
+
const dep = __normalizeDeprecationDoc((info as any).deprecated);
|
|
4074
|
+
if (dep) __addDeprecatedPath(root, [name], dep);
|
|
4075
|
+
if (__isPlainObject((info as any).properties)) {
|
|
4076
|
+
collectInfoProperties([name], (info as any).properties);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
const delegates = (c as any)._delegates;
|
|
4082
|
+
if (Array.isArray(delegates)) {
|
|
4083
|
+
for (const d of delegates) walk(d);
|
|
4084
|
+
}
|
|
4085
|
+
};
|
|
4086
|
+
|
|
4087
|
+
walk(ctx);
|
|
4088
|
+
return root;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
export function createRunJSDeprecationProxy(
|
|
4092
|
+
ctx: any,
|
|
4093
|
+
options: {
|
|
4094
|
+
doc?: RunJSDocMeta;
|
|
4095
|
+
} = {},
|
|
4096
|
+
) {
|
|
4097
|
+
const fromDoc = __buildDeprecatedTreeFromRunJSDoc(options.doc);
|
|
4098
|
+
const fromInfo = __buildDeprecatedTreeFromFlowContextInfos(ctx);
|
|
4099
|
+
__mergeDeprecatedTree(fromDoc, fromInfo);
|
|
4100
|
+
|
|
4101
|
+
const warned = new Set<string>();
|
|
4102
|
+
const proxyToTarget = new WeakMap<object, object>();
|
|
4103
|
+
const objectProxyCache = new WeakMap<object, Map<string, any>>();
|
|
4104
|
+
const functionProxyCache = new WeakMap<Function, Map<string, any>>();
|
|
4105
|
+
|
|
4106
|
+
const extractRunJSLocation = (
|
|
4107
|
+
stack?: string,
|
|
4108
|
+
): { line?: number; column?: number; rawLine?: number; rawColumn?: number } => {
|
|
4109
|
+
if (!stack || typeof stack !== 'string') return {};
|
|
4110
|
+
const WRAPPER_PREFIX_LINES = 2; // JSRunner.run wraps user code with 2 lines before `${code}`
|
|
4111
|
+
const lines = stack.split('\n');
|
|
4112
|
+
for (const l of lines) {
|
|
4113
|
+
if (!l) continue;
|
|
4114
|
+
const m = l.match(/<anonymous>:(\d+):(\d+)/);
|
|
4115
|
+
if (!m) continue;
|
|
4116
|
+
const rawLine = Number(m[1]);
|
|
4117
|
+
const rawColumn = Number(m[2]);
|
|
4118
|
+
const line =
|
|
4119
|
+
Number.isFinite(rawLine) && rawLine > WRAPPER_PREFIX_LINES ? rawLine - WRAPPER_PREFIX_LINES : rawLine;
|
|
4120
|
+
const column = Number.isFinite(rawColumn) ? rawColumn : undefined;
|
|
4121
|
+
return { line, column, rawLine, rawColumn };
|
|
4122
|
+
}
|
|
4123
|
+
return {};
|
|
4124
|
+
};
|
|
4125
|
+
|
|
4126
|
+
const collectInfoProperties = (basePath: string[], props: any) => {
|
|
4127
|
+
if (!__isPlainObject(props)) return;
|
|
4128
|
+
for (const [key, raw] of Object.entries(props)) {
|
|
4129
|
+
if (!key) continue;
|
|
4130
|
+
if (typeof raw === 'string') continue;
|
|
4131
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
|
4132
|
+
const node = raw as any;
|
|
4133
|
+
const dep = __normalizeDeprecationDoc(node.deprecated);
|
|
4134
|
+
if (dep) __addDeprecatedPath(fromDoc, [...basePath, key], dep);
|
|
4135
|
+
if (__isPlainObject(node.properties)) {
|
|
4136
|
+
collectInfoProperties([...basePath, key], node.properties);
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
};
|
|
4140
|
+
|
|
4141
|
+
const updateTreeFromDefineProperty = (name: string, options: any) => {
|
|
4142
|
+
if (!name) return;
|
|
4143
|
+
const info = options?.info;
|
|
4144
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) return;
|
|
4145
|
+
const dep = __normalizeDeprecationDoc((info as any).deprecated);
|
|
4146
|
+
if (dep) __addDeprecatedPath(fromDoc, [name], dep);
|
|
4147
|
+
if (__isPlainObject((info as any).properties)) {
|
|
4148
|
+
collectInfoProperties([name], (info as any).properties);
|
|
4149
|
+
}
|
|
4150
|
+
};
|
|
4151
|
+
|
|
4152
|
+
const updateTreeFromDefineMethod = (name: string, info: any) => {
|
|
4153
|
+
if (!name) return;
|
|
4154
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) return;
|
|
4155
|
+
const dep = __normalizeDeprecationDoc((info as any).deprecated);
|
|
4156
|
+
if (dep) __addDeprecatedPath(fromDoc, [name], dep);
|
|
4157
|
+
};
|
|
4158
|
+
|
|
4159
|
+
const unwrapProxy = (val: any) => {
|
|
4160
|
+
let cur = val;
|
|
4161
|
+
while (cur && (typeof cur === 'object' || typeof cur === 'function')) {
|
|
4162
|
+
const mapped = proxyToTarget.get(cur as any);
|
|
4163
|
+
if (!mapped) break;
|
|
4164
|
+
cur = mapped;
|
|
4165
|
+
}
|
|
4166
|
+
return cur;
|
|
4167
|
+
};
|
|
4168
|
+
|
|
4169
|
+
const formatReplacedBy = (replacedBy: any): string | undefined => {
|
|
4170
|
+
if (!replacedBy) return undefined;
|
|
4171
|
+
if (typeof replacedBy === 'string') return replacedBy.trim() || undefined;
|
|
4172
|
+
if (Array.isArray(replacedBy)) {
|
|
4173
|
+
const parts = replacedBy.map((x) => (typeof x === 'string' ? x.trim() : '')).filter(Boolean);
|
|
4174
|
+
return parts.length ? parts.join(', ') : undefined;
|
|
4175
|
+
}
|
|
4176
|
+
return undefined;
|
|
4177
|
+
};
|
|
4178
|
+
|
|
4179
|
+
const warnOnce = (apiPath: string, deprecated: FlowDeprecationDoc, stack?: string) => {
|
|
4180
|
+
if (!apiPath) return;
|
|
4181
|
+
if (warned.has(apiPath)) return;
|
|
4182
|
+
warned.add(apiPath);
|
|
4183
|
+
|
|
4184
|
+
const logger = (ctx as any)?.logger;
|
|
4185
|
+
const t =
|
|
4186
|
+
typeof (ctx as any)?.t === 'function'
|
|
4187
|
+
? (key: string, options?: any) =>
|
|
4188
|
+
(ctx as any).t(key, { ns: [FLOW_ENGINE_NAMESPACE, 'client'], nsMode: 'fallback', ...options })
|
|
4189
|
+
: (key: string, options?: any) => {
|
|
4190
|
+
const fallback = options?.defaultValue ?? key;
|
|
4191
|
+
if (typeof fallback !== 'string' || !options) return fallback;
|
|
4192
|
+
// lightweight interpolation for fallback strings (i18next-style: {{var}})
|
|
4193
|
+
return fallback.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_m, k) => {
|
|
4194
|
+
const v = options?.[k];
|
|
4195
|
+
return typeof v === 'string' || typeof v === 'number' ? String(v) : '';
|
|
4196
|
+
});
|
|
4197
|
+
};
|
|
4198
|
+
const meta = typeof deprecated === 'object' && deprecated ? deprecated : {};
|
|
4199
|
+
const replacedBy = formatReplacedBy((meta as any).replacedBy);
|
|
4200
|
+
const since = typeof (meta as any).since === 'string' ? String((meta as any).since) : undefined;
|
|
4201
|
+
const removedIn = typeof (meta as any).removedIn === 'string' ? String((meta as any).removedIn) : undefined;
|
|
4202
|
+
const message = typeof (meta as any).message === 'string' ? String((meta as any).message) : '';
|
|
4203
|
+
|
|
4204
|
+
const loc = extractRunJSLocation(stack);
|
|
4205
|
+
|
|
4206
|
+
const locText = loc.line ? `(line ${loc.line}${loc.column ? `:${loc.column}` : ''})` : '';
|
|
4207
|
+
|
|
4208
|
+
const msg = message.trim();
|
|
4209
|
+
const mainText = msg
|
|
4210
|
+
? t('RunJS deprecated warning with message', {
|
|
4211
|
+
defaultValue: '[RunJS][Deprecated] {{api}} {{message}}{{location}}',
|
|
4212
|
+
api: apiPath,
|
|
4213
|
+
message: msg,
|
|
4214
|
+
location: locText,
|
|
4215
|
+
})
|
|
4216
|
+
: t('RunJS deprecated warning', {
|
|
4217
|
+
defaultValue: '[RunJS][Deprecated] {{api}} is deprecated{{location}}',
|
|
4218
|
+
api: apiPath,
|
|
4219
|
+
location: locText,
|
|
4220
|
+
});
|
|
4221
|
+
|
|
4222
|
+
const separator = t('RunJS deprecated separator', { defaultValue: '; ' });
|
|
4223
|
+
const textParts: string[] = [mainText];
|
|
4224
|
+
if (replacedBy)
|
|
4225
|
+
textParts.push(t('RunJS deprecated replacedBy', { defaultValue: 'Use {{replacedBy}} instead', replacedBy }));
|
|
4226
|
+
if (since) textParts.push(t('RunJS deprecated since', { defaultValue: 'since {{since}}', since }));
|
|
4227
|
+
if (removedIn)
|
|
4228
|
+
textParts.push(t('RunJS deprecated removedIn', { defaultValue: 'will be removed in {{removedIn}}', removedIn }));
|
|
4229
|
+
const text = textParts.filter(Boolean).join(separator);
|
|
4230
|
+
|
|
4231
|
+
try {
|
|
4232
|
+
if (logger && typeof logger.warn === 'function') {
|
|
4233
|
+
logger.warn(text);
|
|
4234
|
+
} else {
|
|
4235
|
+
// fail-open: avoid breaking runjs execution when logger is missing
|
|
4236
|
+
console.warn(text);
|
|
4237
|
+
}
|
|
4238
|
+
} catch (_) {
|
|
4239
|
+
// ignore logger failures
|
|
4240
|
+
}
|
|
4241
|
+
};
|
|
4242
|
+
|
|
4243
|
+
const createFunctionProxy = (fn: Function, node: RunJSDeprecatedTreeNode, path: string) => {
|
|
4244
|
+
const dep = node.deprecated;
|
|
4245
|
+
if (!dep) return fn;
|
|
4246
|
+
|
|
4247
|
+
const cacheByPath = functionProxyCache.get(fn) || new Map<string, any>();
|
|
4248
|
+
functionProxyCache.set(fn, cacheByPath);
|
|
4249
|
+
if (cacheByPath.has(path)) return cacheByPath.get(path);
|
|
4250
|
+
|
|
4251
|
+
const proxied = new Proxy(fn, {
|
|
4252
|
+
apply(target, thisArg, argArray) {
|
|
4253
|
+
const stack = warned.has(path) ? undefined : new Error().stack;
|
|
4254
|
+
warnOnce(path, dep, stack);
|
|
4255
|
+
const realThis = unwrapProxy(thisArg);
|
|
4256
|
+
return Reflect.apply(target, realThis, argArray);
|
|
4257
|
+
},
|
|
4258
|
+
get(target, key, receiver) {
|
|
4259
|
+
return Reflect.get(target, key, receiver);
|
|
4260
|
+
},
|
|
4261
|
+
});
|
|
4262
|
+
|
|
4263
|
+
cacheByPath.set(path, proxied);
|
|
4264
|
+
return proxied as any;
|
|
4265
|
+
};
|
|
4266
|
+
|
|
4267
|
+
const createObjectProxy = (target: any, node: RunJSDeprecatedTreeNode, path: string): any => {
|
|
4268
|
+
if (!target || (typeof target !== 'object' && typeof target !== 'function')) return target;
|
|
4269
|
+
if (__isPromiseLike(target)) return target;
|
|
4270
|
+
const hasChildren = !!node.children && Object.keys(node.children).length > 0;
|
|
4271
|
+
if (!hasChildren && path !== 'ctx') return target;
|
|
4272
|
+
|
|
4273
|
+
const cacheByPath = objectProxyCache.get(target) || new Map<string, any>();
|
|
4274
|
+
objectProxyCache.set(target, cacheByPath);
|
|
4275
|
+
if (cacheByPath.has(path)) return cacheByPath.get(path);
|
|
4276
|
+
|
|
4277
|
+
const proxied = new Proxy(target, {
|
|
4278
|
+
get(t, key, receiver) {
|
|
4279
|
+
if (typeof key === 'symbol') {
|
|
4280
|
+
return Reflect.get(t, key, unwrapProxy(receiver));
|
|
4281
|
+
}
|
|
4282
|
+
const prop = String(key);
|
|
4283
|
+
const value = Reflect.get(t, key, unwrapProxy(receiver));
|
|
4284
|
+
|
|
4285
|
+
// Support dynamic deprecation registration via ctx.defineProperty/defineMethod during RunJS execution.
|
|
4286
|
+
// - This is especially useful when the deprecated API is introduced after JSRunner is created.
|
|
4287
|
+
if (path === 'ctx' && prop === 'defineProperty' && typeof value === 'function') {
|
|
4288
|
+
return (...args: any[]) => {
|
|
4289
|
+
const result = value(...args);
|
|
4290
|
+
try {
|
|
4291
|
+
updateTreeFromDefineProperty(String(args?.[0] ?? ''), args?.[1]);
|
|
4292
|
+
} catch (_) {
|
|
4293
|
+
// ignore
|
|
4294
|
+
}
|
|
4295
|
+
return result;
|
|
4296
|
+
};
|
|
4297
|
+
}
|
|
4298
|
+
if (path === 'ctx' && prop === 'defineMethod' && typeof value === 'function') {
|
|
4299
|
+
return (...args: any[]) => {
|
|
4300
|
+
const result = value(...args);
|
|
4301
|
+
try {
|
|
4302
|
+
updateTreeFromDefineMethod(String(args?.[0] ?? ''), args?.[2]);
|
|
4303
|
+
} catch (_) {
|
|
4304
|
+
// ignore
|
|
4305
|
+
}
|
|
4306
|
+
return result;
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
const child = node.children?.[prop];
|
|
4311
|
+
if (!child) return value;
|
|
4312
|
+
|
|
4313
|
+
const childPath = `${path}.${prop}`;
|
|
4314
|
+
if (typeof value === 'function' && child.deprecated) {
|
|
4315
|
+
return createFunctionProxy(value, child, childPath);
|
|
4316
|
+
}
|
|
4317
|
+
if (child.deprecated) {
|
|
4318
|
+
// For non-callable APIs, "use" happens on access (there is no apply step).
|
|
4319
|
+
const stack = warned.has(childPath) ? undefined : new Error().stack;
|
|
4320
|
+
warnOnce(childPath, child.deprecated, stack);
|
|
4321
|
+
}
|
|
4322
|
+
if (value && (typeof value === 'object' || typeof value === 'function') && child.children) {
|
|
4323
|
+
return createObjectProxy(value, child, childPath);
|
|
4324
|
+
}
|
|
4325
|
+
return value;
|
|
4326
|
+
},
|
|
4327
|
+
has(t, key) {
|
|
4328
|
+
return Reflect.has(t, key);
|
|
4329
|
+
},
|
|
4330
|
+
});
|
|
4331
|
+
|
|
4332
|
+
proxyToTarget.set(proxied as any, target);
|
|
4333
|
+
cacheByPath.set(path, proxied);
|
|
4334
|
+
return proxied;
|
|
4335
|
+
};
|
|
4336
|
+
|
|
4337
|
+
return createObjectProxy(ctx, fromDoc, 'ctx');
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
function __mergeRunJSDocDocRecord(base: any, patch: any, mergeDoc: (b: any, p: any) => any): any {
|
|
4341
|
+
if (!__isPlainObject(patch)) return base;
|
|
4342
|
+
// Important: preserve `null` markers when base is not an object (e.g. child class wants to delete parent keys).
|
|
4343
|
+
// If we eagerly delete them here, the deletion intent is lost for later merges in the inheritance chain.
|
|
4344
|
+
if (!__isPlainObject(base)) return patch;
|
|
4345
|
+
const out: any = { ...base };
|
|
4346
|
+
for (const k of Object.keys(patch)) {
|
|
4347
|
+
const pv = patch[k];
|
|
4348
|
+
if (pv === null) {
|
|
4349
|
+
delete out[k];
|
|
4350
|
+
continue;
|
|
4351
|
+
}
|
|
4352
|
+
const bv = __isPlainObject(base) ? base[k] : undefined;
|
|
4353
|
+
const merged = mergeDoc(bv, pv);
|
|
4354
|
+
if (typeof merged === 'undefined') delete out[k];
|
|
4355
|
+
else out[k] = merged;
|
|
4356
|
+
}
|
|
4357
|
+
return out;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
function __mergeRunJSDocPropertyDoc(base: any, patch: any): any {
|
|
4361
|
+
if (patch === null) return undefined;
|
|
4362
|
+
|
|
4363
|
+
const baseIsObj = __isPlainObject(base);
|
|
4364
|
+
const patchIsObj = __isPlainObject(patch);
|
|
4365
|
+
const baseIsStr = typeof base === 'string';
|
|
4366
|
+
const patchIsStr = typeof patch === 'string';
|
|
4367
|
+
|
|
4368
|
+
// Treat string docs as { description: string } when merging with object docs,
|
|
4369
|
+
// to avoid "whole replacement" that drops base hidden/properties/completion.
|
|
4370
|
+
if (patchIsStr) {
|
|
4371
|
+
if (baseIsObj) {
|
|
4372
|
+
return __mergeRunJSDocPropertyDoc(base, { description: patch });
|
|
4373
|
+
}
|
|
4374
|
+
return patch;
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
if (patchIsObj) {
|
|
4378
|
+
const baseObj: any = baseIsObj ? base : baseIsStr ? { description: base } : undefined;
|
|
4379
|
+
const out: any = { ...(baseObj || {}) };
|
|
4380
|
+
for (const k of Object.keys(patch)) {
|
|
4381
|
+
if (k === 'properties') {
|
|
4382
|
+
const pv = (patch as any).properties;
|
|
4383
|
+
if (pv === null) {
|
|
4384
|
+
delete out.properties;
|
|
4385
|
+
continue;
|
|
4386
|
+
}
|
|
4387
|
+
const mergedProps = __mergeRunJSDocDocRecord(baseObj?.properties, pv, __mergeRunJSDocPropertyDoc);
|
|
4388
|
+
if (typeof mergedProps === 'undefined') delete out.properties;
|
|
4389
|
+
else out.properties = mergedProps;
|
|
4390
|
+
continue;
|
|
4391
|
+
}
|
|
4392
|
+
const mergedVal = __runjsDeepMerge(baseObj?.[k], (patch as any)[k]);
|
|
4393
|
+
if (typeof mergedVal === 'undefined') delete out[k];
|
|
4394
|
+
else out[k] = mergedVal;
|
|
4395
|
+
}
|
|
4396
|
+
return out;
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
return patch ?? base;
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
function __mergeRunJSDocMethodDoc(base: any, patch: any): any {
|
|
4403
|
+
if (patch === null) return undefined;
|
|
4404
|
+
|
|
4405
|
+
const baseIsObj = __isPlainObject(base);
|
|
4406
|
+
const patchIsObj = __isPlainObject(patch);
|
|
4407
|
+
const baseIsStr = typeof base === 'string';
|
|
4408
|
+
const patchIsStr = typeof patch === 'string';
|
|
4409
|
+
|
|
4410
|
+
if (patchIsStr) {
|
|
4411
|
+
if (baseIsObj) {
|
|
4412
|
+
return __mergeRunJSDocMethodDoc(base, { description: patch });
|
|
4413
|
+
}
|
|
4414
|
+
return patch;
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
if (patchIsObj) {
|
|
4418
|
+
const baseObj: any = baseIsObj ? base : baseIsStr ? { description: base } : undefined;
|
|
4419
|
+
const out: any = { ...(baseObj || {}) };
|
|
4420
|
+
for (const k of Object.keys(patch)) {
|
|
4421
|
+
const mergedVal = __runjsDeepMerge(baseObj?.[k], (patch as any)[k]);
|
|
4422
|
+
if (typeof mergedVal === 'undefined') delete out[k];
|
|
4423
|
+
else out[k] = mergedVal;
|
|
4424
|
+
}
|
|
4425
|
+
return out;
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
return patch ?? base;
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
function __mergeRunJSDocMeta(base: any, patch: any): RunJSDocMeta {
|
|
4432
|
+
const baseObj: any = __isPlainObject(base) ? base : {};
|
|
4433
|
+
const patchObj: any = __isPlainObject(patch) ? patch : {};
|
|
4434
|
+
const out: any = { ...baseObj };
|
|
4435
|
+
|
|
4436
|
+
for (const k of Object.keys(patchObj)) {
|
|
4437
|
+
if (k === 'properties') {
|
|
4438
|
+
const mergedProps = __mergeRunJSDocDocRecord(baseObj.properties, patchObj.properties, __mergeRunJSDocPropertyDoc);
|
|
4439
|
+
if (typeof mergedProps === 'undefined') delete out.properties;
|
|
4440
|
+
else out.properties = mergedProps;
|
|
4441
|
+
continue;
|
|
4442
|
+
}
|
|
4443
|
+
if (k === 'methods') {
|
|
4444
|
+
const mergedMethods = __mergeRunJSDocDocRecord(baseObj.methods, patchObj.methods, __mergeRunJSDocMethodDoc);
|
|
4445
|
+
if (typeof mergedMethods === 'undefined') delete out.methods;
|
|
4446
|
+
else out.methods = mergedMethods;
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
4449
|
+
const mergedVal = __runjsDeepMerge(baseObj[k], patchObj[k]);
|
|
4450
|
+
if (typeof mergedVal === 'undefined') delete out[k];
|
|
4451
|
+
else out[k] = mergedVal;
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
return out as RunJSDocMeta;
|
|
4455
|
+
}
|
|
1720
4456
|
export class FlowRunJSContext extends FlowContext {
|
|
1721
4457
|
constructor(delegate: FlowContext) {
|
|
1722
4458
|
super();
|
|
1723
4459
|
this.addDelegate(delegate);
|
|
1724
4460
|
this.defineProperty('React', { value: React });
|
|
1725
4461
|
this.defineProperty('antd', { value: antd });
|
|
1726
|
-
this.defineProperty('dayjs', {
|
|
4462
|
+
this.defineProperty('dayjs', {
|
|
4463
|
+
value: dayjs,
|
|
4464
|
+
});
|
|
1727
4465
|
// 为 JS 运行时代码提供带有 antd/App/ConfigProvider 包裹的 React 根
|
|
1728
4466
|
// 保持与 ReactDOMClient 接口一致,优先覆盖 createRoot,其余方法透传
|
|
1729
4467
|
const ReactDOMShim: any = {
|
|
@@ -1735,19 +4473,10 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1735
4473
|
return this.engine.reactView.createRoot(realContainer as HTMLElement, options);
|
|
1736
4474
|
},
|
|
1737
4475
|
};
|
|
4476
|
+
ReactDOMShim.__nbRunjsInternalShim = true;
|
|
1738
4477
|
this.defineProperty('ReactDOM', { value: ReactDOMShim });
|
|
1739
4478
|
|
|
1740
|
-
|
|
1741
|
-
// - 新增库应优先挂载到 ctx.libs.xxx
|
|
1742
|
-
// - 同时保留顶层别名(如 ctx.React / ctx.antd),以兼容历史代码
|
|
1743
|
-
const libs = Object.freeze({
|
|
1744
|
-
React,
|
|
1745
|
-
ReactDOM: ReactDOMShim,
|
|
1746
|
-
antd,
|
|
1747
|
-
dayjs,
|
|
1748
|
-
antdIcons,
|
|
1749
|
-
});
|
|
1750
|
-
this.defineProperty('libs', { value: libs });
|
|
4479
|
+
setupRunJSLibs(this);
|
|
1751
4480
|
|
|
1752
4481
|
// Convenience: ctx.render(<App />[, container])
|
|
1753
4482
|
// - container defaults to ctx.element if available
|
|
@@ -1767,16 +4496,37 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1767
4496
|
globalRef.__nbRunjsRoots = globalRef.__nbRunjsRoots || new WeakMap<any, any>();
|
|
1768
4497
|
const rootMap: WeakMap<any, any> = globalRef.__nbRunjsRoots;
|
|
1769
4498
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
4499
|
+
const disposeEntry = (entry: any) => {
|
|
4500
|
+
if (!entry) return;
|
|
4501
|
+
if (entry.disposeTheme && typeof entry.disposeTheme === 'function') {
|
|
4502
|
+
try {
|
|
4503
|
+
entry.disposeTheme();
|
|
4504
|
+
} catch (_) {
|
|
4505
|
+
// ignore
|
|
4506
|
+
}
|
|
4507
|
+
entry.disposeTheme = undefined;
|
|
4508
|
+
}
|
|
4509
|
+
const root = entry.root || entry;
|
|
4510
|
+
if (root && typeof root.unmount === 'function') {
|
|
1774
4511
|
try {
|
|
1775
|
-
|
|
1776
|
-
}
|
|
1777
|
-
|
|
4512
|
+
root.unmount();
|
|
4513
|
+
} catch (_) {
|
|
4514
|
+
// ignore
|
|
1778
4515
|
}
|
|
1779
4516
|
}
|
|
4517
|
+
};
|
|
4518
|
+
|
|
4519
|
+
const unmountContainerRoot = () => {
|
|
4520
|
+
const existing = rootMap.get(containerEl);
|
|
4521
|
+
if (existing) {
|
|
4522
|
+
disposeEntry(existing);
|
|
4523
|
+
rootMap.delete(containerEl);
|
|
4524
|
+
}
|
|
4525
|
+
};
|
|
4526
|
+
|
|
4527
|
+
// If vnode is string (HTML), unmount react root and set sanitized HTML
|
|
4528
|
+
if (typeof vnode === 'string') {
|
|
4529
|
+
unmountContainerRoot();
|
|
1780
4530
|
const proxy: any = new ElementProxy(containerEl);
|
|
1781
4531
|
proxy.innerHTML = String(vnode ?? '');
|
|
1782
4532
|
return null;
|
|
@@ -1788,39 +4538,64 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1788
4538
|
(vnode as any).nodeType &&
|
|
1789
4539
|
((vnode as any).nodeType === 1 || (vnode as any).nodeType === 3 || (vnode as any).nodeType === 11)
|
|
1790
4540
|
) {
|
|
1791
|
-
|
|
1792
|
-
if (existingRoot && typeof existingRoot.unmount === 'function') {
|
|
1793
|
-
try {
|
|
1794
|
-
existingRoot.unmount();
|
|
1795
|
-
} finally {
|
|
1796
|
-
rootMap.delete(containerEl);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
4541
|
+
unmountContainerRoot();
|
|
1799
4542
|
while (containerEl.firstChild) containerEl.removeChild(containerEl.firstChild);
|
|
1800
4543
|
containerEl.appendChild(vnode as any);
|
|
1801
4544
|
return null;
|
|
1802
4545
|
}
|
|
1803
4546
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
4547
|
+
// 注意:rootMap 是“全局按容器复用”的(key=containerEl)。
|
|
4548
|
+
// 若不同 RunJS ctx 复用同一个 containerEl,且 ReactDOM 实例引用也相同,
|
|
4549
|
+
// 则会复用到旧 entry,进而复用旧 ctx 创建的 autorun(闭包捕获旧 ctx),造成:
|
|
4550
|
+
// 1) 旧 ctx 的 reaction 继续驱动新渲染(跨 ctx 复用风险)
|
|
4551
|
+
// 2) 新 ctx 的主题变化不再触发 rerender
|
|
4552
|
+
// 3) 旧 ctx 被 entry/autorun 间接持有,无法被 GC(内存泄漏)
|
|
4553
|
+
// 因此这里把 ownerKey(当前 ctx)也纳入复用判断;owner 变化时必须重建 entry。
|
|
4554
|
+
const rendererKey = this.ReactDOM;
|
|
4555
|
+
const ownerKey = this;
|
|
4556
|
+
let entry = rootMap.get(containerEl);
|
|
4557
|
+
if (!entry || entry.rendererKey !== rendererKey || entry.ownerKey !== ownerKey) {
|
|
4558
|
+
if (entry) {
|
|
4559
|
+
disposeEntry(entry);
|
|
4560
|
+
rootMap.delete(containerEl);
|
|
4561
|
+
}
|
|
4562
|
+
const root = this.ReactDOM.createRoot(containerEl);
|
|
4563
|
+
entry = { rendererKey, ownerKey, root, disposeTheme: undefined, lastVnode: undefined };
|
|
4564
|
+
rootMap.set(containerEl, entry);
|
|
1808
4565
|
}
|
|
1809
|
-
|
|
1810
|
-
return
|
|
4566
|
+
|
|
4567
|
+
return externalReactRender({
|
|
4568
|
+
ctx: this,
|
|
4569
|
+
entry,
|
|
4570
|
+
vnode,
|
|
4571
|
+
containerEl,
|
|
4572
|
+
rootMap,
|
|
4573
|
+
unmountContainerRoot,
|
|
4574
|
+
internalReact: React,
|
|
4575
|
+
internalAntd: antd,
|
|
4576
|
+
});
|
|
1811
4577
|
},
|
|
1812
4578
|
);
|
|
1813
4579
|
}
|
|
4580
|
+
|
|
4581
|
+
exit() {
|
|
4582
|
+
throw new FlowExitAllException(this.flowKey, this.model?.uid || 'runjs');
|
|
4583
|
+
}
|
|
4584
|
+
|
|
4585
|
+
exitAll() {
|
|
4586
|
+
throw new FlowExitAllException(this.flowKey, this.model?.uid || 'runjs');
|
|
4587
|
+
}
|
|
4588
|
+
|
|
1814
4589
|
static define(meta: RunJSDocMeta, options?: { locale?: string }) {
|
|
1815
4590
|
const locale = options?.locale;
|
|
1816
4591
|
if (locale) {
|
|
1817
4592
|
const map = __runjsClassLocaleMeta.get(this) || new Map<string, RunJSDocMeta>();
|
|
1818
4593
|
const prev = map.get(locale) || {};
|
|
1819
|
-
map.set(locale,
|
|
4594
|
+
map.set(locale, __mergeRunJSDocMeta(prev, meta));
|
|
1820
4595
|
__runjsClassLocaleMeta.set(this, map);
|
|
1821
4596
|
} else {
|
|
1822
4597
|
const prev = __runjsClassDefaultMeta.get(this) || {};
|
|
1823
|
-
__runjsClassDefaultMeta.set(this,
|
|
4598
|
+
__runjsClassDefaultMeta.set(this, __mergeRunJSDocMeta(prev, meta));
|
|
1824
4599
|
}
|
|
1825
4600
|
__runjsDocCache.delete(this);
|
|
1826
4601
|
}
|
|
@@ -1828,7 +4603,7 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1828
4603
|
const self = this as any as Function;
|
|
1829
4604
|
let cacheForClass = __runjsDocCache.get(self);
|
|
1830
4605
|
const cacheKey = String(locale || 'default');
|
|
1831
|
-
if (cacheForClass && cacheForClass.has(cacheKey)) return cacheForClass.get(cacheKey)
|
|
4606
|
+
if (cacheForClass && cacheForClass.has(cacheKey)) return cacheForClass.get(cacheKey) as RunJSDocMeta;
|
|
1832
4607
|
const chain: Function[] = [];
|
|
1833
4608
|
let cur: any = self;
|
|
1834
4609
|
while (cur && cur.prototype) {
|
|
@@ -1837,13 +4612,13 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1837
4612
|
}
|
|
1838
4613
|
let merged: RunJSDocMeta = {};
|
|
1839
4614
|
for (const cls of chain) {
|
|
1840
|
-
merged =
|
|
4615
|
+
merged = __mergeRunJSDocMeta(merged, __runjsClassDefaultMeta.get(cls) || {});
|
|
1841
4616
|
}
|
|
1842
4617
|
if (locale) {
|
|
1843
4618
|
for (const cls of chain) {
|
|
1844
4619
|
const lmap = __runjsClassLocaleMeta.get(cls);
|
|
1845
4620
|
if (lmap && lmap.has(locale)) {
|
|
1846
|
-
merged =
|
|
4621
|
+
merged = __mergeRunJSDocMeta(merged, lmap.get(locale));
|
|
1847
4622
|
}
|
|
1848
4623
|
}
|
|
1849
4624
|
}
|