@nocobase/flow-engine 2.1.0-alpha.1 → 2.1.0-alpha.10
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/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/BlockScopedFlowEngine.js +0 -1
- package/lib/FlowDefinition.d.ts +2 -0
- package/lib/JSRunner.d.ts +15 -0
- package/lib/JSRunner.js +82 -7
- package/lib/ViewScopedFlowEngine.js +8 -1
- package/lib/acl/Acl.js +13 -3
- package/lib/components/FlowContextSelector.js +155 -10
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
- package/lib/components/dnd/gridDragPlanner.js +59 -3
- 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 +21 -3
- package/lib/components/subModel/AddSubModelButton.js +16 -1
- package/lib/components/subModel/utils.js +2 -2
- 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 +84 -51
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +190 -26
- package/lib/flowContext.d.ts +230 -7
- package/lib/flowContext.js +2270 -148
- package/lib/flowEngine.d.ts +160 -1
- package/lib/flowEngine.js +383 -26
- package/lib/flowI18n.js +6 -4
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +51 -17
- package/lib/index.d.ts +7 -1
- package/lib/index.js +21 -0
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -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.d.ts +7 -0
- package/lib/models/flowModel.js +83 -8
- package/lib/provider.js +7 -6
- 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/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +22 -9
- 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 +7 -1
- package/lib/scheduler/ModelOperationScheduler.js +28 -23
- package/lib/types.d.ts +63 -1
- 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 +49 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- 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 +17 -1
- package/lib/utils/schema-utils.js +80 -0
- package/lib/views/FlowView.d.ts +7 -1
- 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/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +28 -6
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +27 -5
- package/lib/views/usePage.d.ts +6 -1
- package/lib/views/usePage.js +53 -9
- 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 +5 -5
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/JSRunner.ts +111 -5
- package/src/ViewScopedFlowEngine.ts +8 -0
- package/src/__tests__/JSRunner.test.ts +91 -1
- 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__/flowEngine.modelLoaders.test.ts +245 -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__/flowSettings.test.ts +94 -15
- package/src/__tests__/provider.test.tsx +0 -5
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +23 -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 +72 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
- package/src/__tests__/runjsSnippets.test.ts +40 -3
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/acl/Acl.tsx +3 -3
- package/src/components/FlowContextSelector.tsx +208 -12
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
- package/src/components/__tests__/gridDragPlanner.test.ts +229 -1
- package/src/components/dnd/gridDragPlanner.ts +68 -2
- 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 +31 -4
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
- package/src/components/subModel/AddSubModelButton.tsx +17 -1
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
- package/src/components/subModel/utils.ts +1 -1
- 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 +88 -110
- package/src/executor/FlowExecutor.ts +230 -28
- package/src/executor/__tests__/flowExecutor.test.ts +123 -0
- package/src/flowContext.ts +2989 -212
- package/src/flowEngine.ts +427 -22
- package/src/flowI18n.ts +7 -5
- package/src/flowSettings.ts +58 -18
- package/src/index.ts +14 -1
- package/src/lazy-helper.tsx +57 -0
- 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 +768 -0
- package/src/models/__tests__/flowModel.clone.test.ts +416 -0
- package/src/models/__tests__/flowModel.test.ts +20 -4
- package/src/models/flowModel.tsx +112 -7
- package/src/provider.tsx +9 -7
- 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/registry.ts +1 -1
- package/src/runjs-context/setup.ts +24 -9
- 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 +41 -24
- package/src/types.ts +86 -1
- package/src/utils/__tests__/dateVariable.test.ts +101 -0
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -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 +157 -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 +38 -3
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- 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 +109 -1
- package/src/views/FlowView.tsx +11 -1
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +44 -16
- 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/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +34 -5
- package/src/views/useDrawer.tsx +33 -4
- package/src/views/usePage.tsx +63 -8
- 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';
|
|
@@ -28,7 +27,7 @@ import { ContextPathProxy } from './ContextPathProxy';
|
|
|
28
27
|
import { DataSource, DataSourceManager } from './data-source';
|
|
29
28
|
import { FlowEngine } from './flowEngine';
|
|
30
29
|
import { FlowI18n } from './flowI18n';
|
|
31
|
-
import { JSRunner, JSRunnerOptions } from './JSRunner';
|
|
30
|
+
import { JSRunner, JSRunnerOptions, shouldPreprocessRunJSTemplates } from './JSRunner';
|
|
32
31
|
import type { FlowModel } from './models/flowModel';
|
|
33
32
|
import type { ForkFlowModel } from './models/forkFlowModel';
|
|
34
33
|
import { FlowResource, FlowSQLRepository } from './resources';
|
|
@@ -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,33 @@ 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
|
+
const shouldPreprocessTemplates = shouldPreprocessRunJSTemplates({
|
|
3039
|
+
version: runnerOptions?.version,
|
|
3040
|
+
preprocessTemplates,
|
|
3041
|
+
});
|
|
3042
|
+
const jsCode = await prepareRunJsCode(String(code ?? ''), { preprocessTemplates: shouldPreprocessTemplates });
|
|
3043
|
+
return runner.run(jsCode);
|
|
3044
|
+
},
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
942
3047
|
}
|
|
943
3048
|
|
|
944
3049
|
class BaseFlowModelContext extends BaseFlowEngineContext {
|
|
@@ -956,7 +3061,16 @@ class BaseFlowModelContext extends BaseFlowEngineContext {
|
|
|
956
3061
|
EventDefinition<TModel, TCtx>
|
|
957
3062
|
>;
|
|
958
3063
|
declare runAction: (actionName: string, params?: Record<string, any>) => Promise<any> | any;
|
|
3064
|
+
/**
|
|
3065
|
+
* @deprecated use `makeResource` instead
|
|
3066
|
+
*/
|
|
959
3067
|
declare createResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
|
|
3068
|
+
/**
|
|
3069
|
+
* Create a new resource instance without adding it to the context.
|
|
3070
|
+
* @param resourceType - The resource type.
|
|
3071
|
+
* @returns The resource instance.
|
|
3072
|
+
*/
|
|
3073
|
+
declare makeResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
|
|
960
3074
|
}
|
|
961
3075
|
|
|
962
3076
|
export class FlowEngineContext extends BaseFlowEngineContext {
|
|
@@ -976,25 +3090,30 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
976
3090
|
dataSourceManager.addDataSource(mainDataSource);
|
|
977
3091
|
this.defineProperty('engine', {
|
|
978
3092
|
value: this.engine,
|
|
3093
|
+
info: {
|
|
3094
|
+
description: 'FlowEngine instance.',
|
|
3095
|
+
detail: 'FlowEngine',
|
|
3096
|
+
},
|
|
979
3097
|
});
|
|
980
3098
|
this.defineProperty('sql', {
|
|
981
|
-
get: () => new FlowSQLRepository(
|
|
3099
|
+
get: (ctx) => new FlowSQLRepository(ctx),
|
|
3100
|
+
cache: false,
|
|
3101
|
+
info: {
|
|
3102
|
+
description: 'SQL helper (FlowSQLRepository).',
|
|
3103
|
+
detail: 'FlowSQLRepository',
|
|
3104
|
+
},
|
|
982
3105
|
});
|
|
983
3106
|
this.defineProperty('dataSourceManager', {
|
|
984
3107
|
value: dataSourceManager,
|
|
3108
|
+
info: {
|
|
3109
|
+
description: 'DataSourceManager instance.',
|
|
3110
|
+
detail: 'DataSourceManager',
|
|
3111
|
+
},
|
|
985
3112
|
});
|
|
986
3113
|
const i18n = new FlowI18n(this);
|
|
987
3114
|
this.defineMethod('t', (keyOrTemplate: string, options?: any) => {
|
|
988
3115
|
return i18n.translate(keyOrTemplate, options);
|
|
989
3116
|
});
|
|
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
3117
|
this.defineMethod('renderJson', function (template: any) {
|
|
999
3118
|
return this.resolveJsonTemplate(template);
|
|
1000
3119
|
});
|
|
@@ -1030,6 +3149,22 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1030
3149
|
const needServer = Object.keys(serverVarPaths).length > 0;
|
|
1031
3150
|
let serverResolved = template;
|
|
1032
3151
|
if (needServer) {
|
|
3152
|
+
const inferRecordRefWithMeta = (ctx: any): RecordRef | undefined => {
|
|
3153
|
+
const ref = inferRecordRef(ctx as any);
|
|
3154
|
+
if (ref) return ref as RecordRef;
|
|
3155
|
+
try {
|
|
3156
|
+
const tk = ctx?.resource?.getMeta?.('currentFilterByTk');
|
|
3157
|
+
if (typeof tk === 'undefined' || tk === null) return undefined;
|
|
3158
|
+
const collection =
|
|
3159
|
+
ctx?.collection?.name || ctx?.resource?.getResourceName?.()?.split?.('.')?.slice?.(-1)?.[0];
|
|
3160
|
+
if (!collection) return undefined;
|
|
3161
|
+
const dataSourceKey = ctx?.collection?.dataSourceKey || ctx?.resource?.getDataSourceKey?.();
|
|
3162
|
+
return { collection, dataSourceKey, filterByTk: tk } as RecordRef;
|
|
3163
|
+
} catch (_) {
|
|
3164
|
+
return undefined;
|
|
3165
|
+
}
|
|
3166
|
+
};
|
|
3167
|
+
|
|
1033
3168
|
const collectFromMeta = async (): Promise<Record<string, any>> => {
|
|
1034
3169
|
const out: Record<string, any> = {};
|
|
1035
3170
|
try {
|
|
@@ -1069,7 +3204,62 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1069
3204
|
};
|
|
1070
3205
|
|
|
1071
3206
|
const inputFromMeta = await collectFromMeta();
|
|
1072
|
-
const autoInput = { ...inputFromMeta }
|
|
3207
|
+
const autoInput = { ...inputFromMeta } as Record<string, any>;
|
|
3208
|
+
|
|
3209
|
+
// Special-case: formValues
|
|
3210
|
+
// If server needs to resolve some formValues paths but meta params only cover association anchors
|
|
3211
|
+
// (e.g. formValues.customer) and some top-level paths are missing (e.g. formValues.status),
|
|
3212
|
+
// inject a top-level record anchor (formValues -> { collection, filterByTk, fields/appends }) so server can fetch DB values.
|
|
3213
|
+
// This anchor MUST be selective (fields/appends derived from serverVarPaths['formValues']) to avoid server overriding
|
|
3214
|
+
// client-only values for configured form fields in the same template.
|
|
3215
|
+
try {
|
|
3216
|
+
const varName = 'formValues';
|
|
3217
|
+
const neededPaths = serverVarPaths[varName] || [];
|
|
3218
|
+
if (neededPaths.length) {
|
|
3219
|
+
const requiredTop = new Set<string>();
|
|
3220
|
+
for (const p of neededPaths) {
|
|
3221
|
+
const top = topLevelOf(p);
|
|
3222
|
+
if (top) requiredTop.add(top);
|
|
3223
|
+
}
|
|
3224
|
+
const metaOut = inputFromMeta?.[varName];
|
|
3225
|
+
const builtTop = new Set<string>();
|
|
3226
|
+
if (metaOut && typeof metaOut === 'object' && !Array.isArray(metaOut) && !isRecordRefLike(metaOut)) {
|
|
3227
|
+
Object.keys(metaOut).forEach((k) => builtTop.add(k));
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
const missing = [...requiredTop].filter((k) => !builtTop.has(k));
|
|
3231
|
+
if (missing.length) {
|
|
3232
|
+
const ref = inferRecordRefWithMeta(this);
|
|
3233
|
+
if (ref) {
|
|
3234
|
+
const { generatedFields, generatedAppends } = inferSelectsFromUsage(neededPaths);
|
|
3235
|
+
const recordRef: RecordRef = {
|
|
3236
|
+
...ref,
|
|
3237
|
+
fields: generatedFields,
|
|
3238
|
+
appends: generatedAppends,
|
|
3239
|
+
};
|
|
3240
|
+
|
|
3241
|
+
// Preserve existing association anchors by lifting them to dotted keys before overwriting formValues
|
|
3242
|
+
const existing = autoInput[varName];
|
|
3243
|
+
if (
|
|
3244
|
+
existing &&
|
|
3245
|
+
typeof existing === 'object' &&
|
|
3246
|
+
!Array.isArray(existing) &&
|
|
3247
|
+
!isRecordRefLike(existing)
|
|
3248
|
+
) {
|
|
3249
|
+
for (const [k, v] of Object.entries(existing)) {
|
|
3250
|
+
autoInput[`${varName}.${k}`] = v;
|
|
3251
|
+
}
|
|
3252
|
+
delete autoInput[varName];
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
autoInput[varName] = recordRef;
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
} catch (_) {
|
|
3260
|
+
// ignore
|
|
3261
|
+
}
|
|
3262
|
+
|
|
1073
3263
|
const autoContextParams = Object.keys(autoInput).length
|
|
1074
3264
|
? _buildServerContextParams(this, autoInput)
|
|
1075
3265
|
: undefined;
|
|
@@ -1100,6 +3290,23 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1100
3290
|
|
|
1101
3291
|
return resolveExpressions(serverResolved, this);
|
|
1102
3292
|
});
|
|
3293
|
+
|
|
3294
|
+
// Helper: resolve a single ctx expression value via resolveJsonTemplate behavior.
|
|
3295
|
+
// Example: await ctx.getVar('ctx.record.id')
|
|
3296
|
+
this.defineMethod(
|
|
3297
|
+
'getVar',
|
|
3298
|
+
async function (this: BaseFlowEngineContext, varPath: string) {
|
|
3299
|
+
const raw = typeof varPath === 'string' ? varPath : String(varPath ?? '');
|
|
3300
|
+
const s = raw.trim();
|
|
3301
|
+
if (!s) return undefined;
|
|
3302
|
+
// Preferred input: 'ctx.xxx.yyy' (expression), consistent with envs.getVar outputs.
|
|
3303
|
+
if (s !== 'ctx' && !s.startsWith('ctx.')) {
|
|
3304
|
+
throw new Error(`ctx.getVar(path) expects an expression starting with "ctx.", got: "${s}"`);
|
|
3305
|
+
}
|
|
3306
|
+
return this.resolveJsonTemplate(`{{ ${s} }}` as any);
|
|
3307
|
+
},
|
|
3308
|
+
'Resolve a ctx expression value by path (expression starts with "ctx.").',
|
|
3309
|
+
);
|
|
1103
3310
|
this.defineProperty('requirejs', {
|
|
1104
3311
|
get: () => this.app?.requirejs?.requirejs,
|
|
1105
3312
|
});
|
|
@@ -1181,71 +3388,79 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1181
3388
|
user: this.user,
|
|
1182
3389
|
}),
|
|
1183
3390
|
});
|
|
1184
|
-
this.
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
3391
|
+
this.defineProperty('date', {
|
|
3392
|
+
get: () => {
|
|
3393
|
+
const createBranch = (prefix: string[]) => {
|
|
3394
|
+
return new Proxy(
|
|
3395
|
+
{},
|
|
3396
|
+
{
|
|
3397
|
+
get: (_target, prop) => {
|
|
3398
|
+
if (typeof prop !== 'string') return undefined;
|
|
3399
|
+
const nextPath = [...prefix, prop];
|
|
3400
|
+
if (!isCtxDatePathPrefix(nextPath)) {
|
|
3401
|
+
return undefined;
|
|
3402
|
+
}
|
|
3403
|
+
const resolved = resolveCtxDatePath(nextPath);
|
|
3404
|
+
if (typeof resolved !== 'undefined') {
|
|
3405
|
+
return resolved;
|
|
3406
|
+
}
|
|
3407
|
+
return createBranch(nextPath);
|
|
3408
|
+
},
|
|
3409
|
+
},
|
|
3410
|
+
);
|
|
3411
|
+
};
|
|
1192
3412
|
|
|
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
|
-
});
|
|
3413
|
+
return createBranch(['date']);
|
|
3414
|
+
},
|
|
3415
|
+
cache: false,
|
|
1200
3416
|
});
|
|
3417
|
+
this.defineMethod(
|
|
3418
|
+
'loadCSS',
|
|
3419
|
+
async (href: string) => {
|
|
3420
|
+
const url = resolveModuleUrl(href);
|
|
3421
|
+
return new Promise((resolve, reject) => {
|
|
3422
|
+
// Check if CSS is already loaded
|
|
3423
|
+
const existingLink = document.querySelector(`link[href="${url}"]`);
|
|
3424
|
+
if (existingLink) {
|
|
3425
|
+
resolve(null);
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
const link = document.createElement('link');
|
|
3430
|
+
link.rel = 'stylesheet';
|
|
3431
|
+
link.href = url;
|
|
3432
|
+
link.onload = () => resolve(null);
|
|
3433
|
+
link.onerror = () => reject(new Error(`Failed to load CSS: ${url}`));
|
|
3434
|
+
document.head.appendChild(link);
|
|
3435
|
+
});
|
|
3436
|
+
},
|
|
3437
|
+
{
|
|
3438
|
+
description: 'Load a CSS file by URL (browser only).',
|
|
3439
|
+
params: [{ name: 'href', type: 'string', description: 'CSS URL.' }],
|
|
3440
|
+
returns: { type: 'Promise<void>' },
|
|
3441
|
+
completion: { insertText: "await ctx.loadCSS('https://example.com/style.css')" },
|
|
3442
|
+
examples: ["await ctx.loadCSS('https://example.com/style.css');"],
|
|
3443
|
+
},
|
|
3444
|
+
);
|
|
1201
3445
|
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
|
-
});
|
|
3446
|
+
// 判断是否为 CSS 文件(支持 example.css?v=123 等形式)
|
|
3447
|
+
if (isCssFile(url)) {
|
|
3448
|
+
return this.loadCSS(url);
|
|
3449
|
+
}
|
|
3450
|
+
const u = resolveModuleUrl(url, { raw: true });
|
|
3451
|
+
return await runjsRequireAsync(this.requirejs, u);
|
|
1215
3452
|
});
|
|
1216
3453
|
// 动态按 URL 加载 ESM 模块
|
|
1217
3454
|
// - 使用 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;
|
|
3455
|
+
// - 通常返回模块命名空间对象(包含 default 与命名导出);
|
|
3456
|
+
// 若模块只有 default 一个导出,则会直接返回 default 值以提升易用性(无需再访问 .default)
|
|
3457
|
+
this.defineMethod('importAsync', async function (this: any, url: string) {
|
|
3458
|
+
// 判断是否为 CSS 文件(支持 example.css?v=123 等形式)
|
|
3459
|
+
if (isCssFile(url)) {
|
|
3460
|
+
return this.loadCSS(url);
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
return await runjsImportModule(this, url, { importer: runjsImportAsync });
|
|
1249
3464
|
});
|
|
1250
3465
|
this.defineMethod('createJSRunner', async function (options?: JSRunnerOptions) {
|
|
1251
3466
|
try {
|
|
@@ -1254,17 +3469,24 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1254
3469
|
} catch (_) {
|
|
1255
3470
|
// ignore if setup is not available
|
|
1256
3471
|
}
|
|
1257
|
-
const version =
|
|
3472
|
+
const version = options?.version || 'v1';
|
|
1258
3473
|
const modelClass = getModelClassName(this);
|
|
1259
|
-
const Ctor =
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
3474
|
+
const Ctor: new (delegate: any) => any = RunJSContextRegistry.resolve(version, modelClass) || FlowRunJSContext;
|
|
3475
|
+
const runCtx = new Ctor(this);
|
|
3476
|
+
runCtx.defineMethod('t', (key: string, options?: any) => {
|
|
3477
|
+
return this.t(key, { ns: 'runjs', ...options });
|
|
3478
|
+
});
|
|
3479
|
+
|
|
3480
|
+
let doc: RunJSDocMeta = {};
|
|
3481
|
+
try {
|
|
3482
|
+
const locale = (this as any)?.api?.auth?.locale || (this as any)?.i18n?.language || (this as any)?.locale;
|
|
3483
|
+
if ((Ctor as any)?.getDoc?.length) doc = (Ctor as any).getDoc(locale) || {};
|
|
3484
|
+
else doc = (Ctor as any)?.getDoc?.() || {};
|
|
3485
|
+
} catch (_) {
|
|
3486
|
+
doc = {};
|
|
3487
|
+
}
|
|
3488
|
+
const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
|
|
3489
|
+
const globals: Record<string, any> = { ctx: deprecatedCtx, ...(options?.globals || {}) };
|
|
1268
3490
|
const { timeoutMs } = options || {};
|
|
1269
3491
|
return new JSRunner({ globals, timeoutMs });
|
|
1270
3492
|
});
|
|
@@ -1282,57 +3504,6 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1282
3504
|
return this.engine.getEvents();
|
|
1283
3505
|
});
|
|
1284
3506
|
|
|
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
3507
|
this.defineMethod(
|
|
1337
3508
|
'runAction',
|
|
1338
3509
|
async function (this: BaseFlowEngineContext, actionName: string, params?: Record<string, any>) {
|
|
@@ -1375,17 +3546,34 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
1375
3546
|
context: this.createProxy(),
|
|
1376
3547
|
});
|
|
1377
3548
|
});
|
|
3549
|
+
this.defineMethod('makeResource', function (this: BaseFlowEngineContext, resourceType) {
|
|
3550
|
+
return this.engine.createResource(resourceType, {
|
|
3551
|
+
context: this.createProxy(),
|
|
3552
|
+
});
|
|
3553
|
+
});
|
|
1378
3554
|
// Provide useResource in base engine context so RunJS can call it directly
|
|
3555
|
+
this.defineMethod(
|
|
3556
|
+
'initResource',
|
|
3557
|
+
function (
|
|
3558
|
+
this: BaseFlowEngineContext,
|
|
3559
|
+
className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
|
|
3560
|
+
) {
|
|
3561
|
+
if (!this.has('resource')) {
|
|
3562
|
+
this.defineProperty('resource', {
|
|
3563
|
+
get: () => this.createResource(className),
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
return this.resource;
|
|
3567
|
+
},
|
|
3568
|
+
);
|
|
3569
|
+
// @deprecated use `initResource` instead
|
|
1379
3570
|
this.defineMethod(
|
|
1380
3571
|
'useResource',
|
|
1381
3572
|
function (
|
|
1382
3573
|
this: BaseFlowEngineContext,
|
|
1383
3574
|
className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
|
|
1384
3575
|
) {
|
|
1385
|
-
|
|
1386
|
-
this.defineProperty('resource', {
|
|
1387
|
-
get: () => this.createResource(className),
|
|
1388
|
-
});
|
|
3576
|
+
return this.initResource(className);
|
|
1389
3577
|
},
|
|
1390
3578
|
);
|
|
1391
3579
|
}
|
|
@@ -1401,15 +3589,12 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
1401
3589
|
this.defineMethod('onRefReady', (ref, cb, timeout) => {
|
|
1402
3590
|
this.engine.reactView.onRefReady(ref, cb, timeout);
|
|
1403
3591
|
});
|
|
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
3592
|
this.defineProperty('model', {
|
|
1412
3593
|
value: model,
|
|
3594
|
+
info: {
|
|
3595
|
+
description: 'Current FlowModel instance.',
|
|
3596
|
+
detail: 'FlowModel',
|
|
3597
|
+
},
|
|
1413
3598
|
});
|
|
1414
3599
|
// 提供稳定的 ref 实例,确保渲染端与运行时上下文使用同一对象
|
|
1415
3600
|
const stableRef = createRef<HTMLDivElement>();
|
|
@@ -1418,6 +3603,10 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
1418
3603
|
this.model['_refCreated'] = true;
|
|
1419
3604
|
return stableRef;
|
|
1420
3605
|
},
|
|
3606
|
+
info: {
|
|
3607
|
+
description: 'Stable React ref for the view container.',
|
|
3608
|
+
detail: 'React.RefObject<HTMLDivElement>',
|
|
3609
|
+
},
|
|
1421
3610
|
});
|
|
1422
3611
|
this.defineMethod('openView', async function (uid: string, options) {
|
|
1423
3612
|
const opts = { ...options };
|
|
@@ -1478,8 +3667,17 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
1478
3667
|
engineCtx: this.engine.context,
|
|
1479
3668
|
};
|
|
1480
3669
|
model.context.defineProperty('view', { value: pendingView });
|
|
3670
|
+
// 默认按 click 打开,但兼容 popupSettings 绑定到其他事件(例如 DuplicateActionModel 监听 openDuplicatePopup)。
|
|
3671
|
+
const popupFlow = model.getFlow?.('popupSettings');
|
|
3672
|
+
const on = (popupFlow as any)?.on;
|
|
3673
|
+
let openEventName = 'click';
|
|
3674
|
+
if (typeof on === 'string' && on) {
|
|
3675
|
+
openEventName = on;
|
|
3676
|
+
} else if (on && typeof on === 'object' && typeof (on as any).eventName === 'string' && (on as any).eventName) {
|
|
3677
|
+
openEventName = (on as any).eventName;
|
|
3678
|
+
}
|
|
1481
3679
|
await model.dispatchEvent(
|
|
1482
|
-
|
|
3680
|
+
openEventName,
|
|
1483
3681
|
{
|
|
1484
3682
|
// navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
|
|
1485
3683
|
// ...this.model?.['getInputArgs']?.(), // 避免部分关系字段信息丢失, 仿照 ClickableCollectionField 做法
|
|
@@ -1538,12 +3736,16 @@ export class FlowForkModelContext extends BaseFlowModelContext {
|
|
|
1538
3736
|
throw new Error('Invalid FlowModel instance');
|
|
1539
3737
|
}
|
|
1540
3738
|
super();
|
|
1541
|
-
this.addDelegate(
|
|
3739
|
+
this.addDelegate(this.master.context);
|
|
1542
3740
|
this.defineMethod('onRefReady', (ref, cb, timeout) => {
|
|
1543
3741
|
this.engine.reactView.onRefReady(ref, cb, timeout);
|
|
1544
3742
|
});
|
|
1545
3743
|
this.defineProperty('model', {
|
|
1546
3744
|
get: () => this.fork,
|
|
3745
|
+
info: {
|
|
3746
|
+
description: 'Current ForkFlowModel instance (as model).',
|
|
3747
|
+
detail: 'ForkFlowModel',
|
|
3748
|
+
},
|
|
1547
3749
|
});
|
|
1548
3750
|
// 提供稳定的 ref 实例,确保渲染端与运行时上下文使用同一对象
|
|
1549
3751
|
const stableRef = createRef<HTMLDivElement>();
|
|
@@ -1552,13 +3754,10 @@ export class FlowForkModelContext extends BaseFlowModelContext {
|
|
|
1552
3754
|
this.fork['_refCreated'] = true;
|
|
1553
3755
|
return stableRef;
|
|
1554
3756
|
},
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
version: options?.version,
|
|
1560
|
-
});
|
|
1561
|
-
return runner.run(code);
|
|
3757
|
+
info: {
|
|
3758
|
+
description: 'Stable React ref for the view container.',
|
|
3759
|
+
detail: 'React.RefObject<HTMLDivElement>',
|
|
3760
|
+
},
|
|
1562
3761
|
});
|
|
1563
3762
|
}
|
|
1564
3763
|
}
|
|
@@ -1569,7 +3768,19 @@ export class FlowRuntimeContext<
|
|
|
1569
3768
|
> extends BaseFlowModelContext {
|
|
1570
3769
|
declare steps: Record<string, { params: Record<string, any>; uiSchema?: any; result?: any }>;
|
|
1571
3770
|
stepResults: Record<string, any> = {};
|
|
1572
|
-
|
|
3771
|
+
/**
|
|
3772
|
+
* @deprecated use `initResource` instead
|
|
3773
|
+
*/
|
|
3774
|
+
declare useResource: (
|
|
3775
|
+
className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
|
|
3776
|
+
) => void;
|
|
3777
|
+
/**
|
|
3778
|
+
* Initialize a resource instance without adding it to the context.
|
|
3779
|
+
* @param className - The resource class name.
|
|
3780
|
+
*/
|
|
3781
|
+
declare initResource: (
|
|
3782
|
+
className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
|
|
3783
|
+
) => void;
|
|
1573
3784
|
declare getStepParams: (stepKey: string) => Record<string, any>;
|
|
1574
3785
|
declare setStepParams: (stepKey: string, params?: any) => void;
|
|
1575
3786
|
declare getStepResults: (stepKey: string) => any;
|
|
@@ -1591,15 +3802,15 @@ export class FlowRuntimeContext<
|
|
|
1591
3802
|
return _.get(this.steps, [stepKey, 'result']);
|
|
1592
3803
|
});
|
|
1593
3804
|
this.defineMethod(
|
|
1594
|
-
'
|
|
3805
|
+
'initResource',
|
|
1595
3806
|
(className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource') => {
|
|
1596
3807
|
if (model.context.has('resource')) {
|
|
1597
|
-
console.
|
|
3808
|
+
console.log(`[FlowRuntimeContext] useResource - resource already defined in context: ${className}`);
|
|
1598
3809
|
return;
|
|
1599
3810
|
}
|
|
1600
3811
|
model.context.defineProperty('resource', {
|
|
1601
3812
|
get: () => {
|
|
1602
|
-
return this.
|
|
3813
|
+
return this.makeResource(className);
|
|
1603
3814
|
},
|
|
1604
3815
|
});
|
|
1605
3816
|
if (!model['resource']) {
|
|
@@ -1607,6 +3818,13 @@ export class FlowRuntimeContext<
|
|
|
1607
3818
|
}
|
|
1608
3819
|
},
|
|
1609
3820
|
);
|
|
3821
|
+
// @deprecated use `initResource` instead
|
|
3822
|
+
this.defineMethod(
|
|
3823
|
+
'useResource',
|
|
3824
|
+
(className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource') => {
|
|
3825
|
+
return this.initResource(className);
|
|
3826
|
+
},
|
|
3827
|
+
);
|
|
1610
3828
|
this.defineProperty('resource', {
|
|
1611
3829
|
get: () => model['resource'] || model.context['resource'],
|
|
1612
3830
|
cache: false,
|
|
@@ -1614,13 +3832,6 @@ export class FlowRuntimeContext<
|
|
|
1614
3832
|
this.defineMethod('onRefReady', (ref, cb, timeout) => {
|
|
1615
3833
|
this.engine.reactView.onRefReady(ref, cb, timeout);
|
|
1616
3834
|
});
|
|
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
3835
|
}
|
|
1625
3836
|
|
|
1626
3837
|
protected _getOwnProperty(key: string): any {
|
|
@@ -1654,7 +3865,7 @@ export class FlowRuntimeContext<
|
|
|
1654
3865
|
}
|
|
1655
3866
|
|
|
1656
3867
|
exit() {
|
|
1657
|
-
throw new
|
|
3868
|
+
throw new FlowExitAllException(this.flowKey, this.model.uid);
|
|
1658
3869
|
}
|
|
1659
3870
|
|
|
1660
3871
|
exitAll() {
|
|
@@ -1673,6 +3884,16 @@ export type RunJSDocCompletionDoc = {
|
|
|
1673
3884
|
insertText?: string;
|
|
1674
3885
|
};
|
|
1675
3886
|
|
|
3887
|
+
export type RunJSDocHiddenDoc = boolean | ((ctx: any) => boolean | Promise<boolean>);
|
|
3888
|
+
|
|
3889
|
+
// `hidden` is the single visibility entrypoint for RunJSDoc property docs:
|
|
3890
|
+
// - boolean: hide the whole node and its subtree
|
|
3891
|
+
// - string[]: hide specific subpaths under the node (relative dot-paths)
|
|
3892
|
+
export type RunJSDocHiddenOrPathsDoc =
|
|
3893
|
+
| boolean
|
|
3894
|
+
| string[]
|
|
3895
|
+
| ((ctx: any) => boolean | string[] | Promise<boolean | string[]>);
|
|
3896
|
+
|
|
1676
3897
|
export type RunJSDocPropertyDoc =
|
|
1677
3898
|
| string
|
|
1678
3899
|
| {
|
|
@@ -1681,7 +3902,14 @@ export type RunJSDocPropertyDoc =
|
|
|
1681
3902
|
type?: string;
|
|
1682
3903
|
examples?: string[];
|
|
1683
3904
|
completion?: RunJSDocCompletionDoc;
|
|
3905
|
+
ref?: FlowContextDocRef;
|
|
3906
|
+
deprecated?: FlowDeprecationDoc;
|
|
3907
|
+
params?: FlowContextDocParam[];
|
|
3908
|
+
returns?: FlowContextDocReturn;
|
|
1684
3909
|
properties?: Record<string, RunJSDocPropertyDoc>;
|
|
3910
|
+
hidden?: RunJSDocHiddenOrPathsDoc;
|
|
3911
|
+
disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
|
|
3912
|
+
disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
|
|
1685
3913
|
};
|
|
1686
3914
|
|
|
1687
3915
|
export type RunJSDocMethodDoc =
|
|
@@ -1691,6 +3919,13 @@ export type RunJSDocMethodDoc =
|
|
|
1691
3919
|
detail?: string;
|
|
1692
3920
|
examples?: string[];
|
|
1693
3921
|
completion?: RunJSDocCompletionDoc;
|
|
3922
|
+
ref?: FlowContextDocRef;
|
|
3923
|
+
deprecated?: FlowDeprecationDoc;
|
|
3924
|
+
params?: FlowContextDocParam[];
|
|
3925
|
+
returns?: FlowContextDocReturn;
|
|
3926
|
+
hidden?: RunJSDocHiddenDoc;
|
|
3927
|
+
disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
|
|
3928
|
+
disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
|
|
1694
3929
|
};
|
|
1695
3930
|
|
|
1696
3931
|
export type RunJSDocMeta = {
|
|
@@ -1717,13 +3952,518 @@ function __runjsDeepMerge(base: any, patch: any) {
|
|
|
1717
3952
|
}
|
|
1718
3953
|
return out;
|
|
1719
3954
|
}
|
|
3955
|
+
|
|
3956
|
+
function __isPlainObject(val: any): val is Record<string, any> {
|
|
3957
|
+
return !!val && typeof val === 'object' && !Array.isArray(val);
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
type RunJSDeprecatedTreeNode = {
|
|
3961
|
+
deprecated?: FlowDeprecationDoc;
|
|
3962
|
+
children?: Record<string, RunJSDeprecatedTreeNode>;
|
|
3963
|
+
};
|
|
3964
|
+
|
|
3965
|
+
function __isPromiseLike(v: any): v is Promise<any> {
|
|
3966
|
+
return !!v && (typeof v === 'object' || typeof v === 'function') && typeof (v as any).then === 'function';
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
function __normalizeDeprecationDoc(v: any): FlowDeprecationDoc | undefined {
|
|
3970
|
+
if (v === true) return true;
|
|
3971
|
+
if (!v) return undefined;
|
|
3972
|
+
if (__isPlainObject(v)) return v as any;
|
|
3973
|
+
return undefined;
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
function __addDeprecatedPath(root: RunJSDeprecatedTreeNode, path: string[], deprecated: FlowDeprecationDoc) {
|
|
3977
|
+
if (!Array.isArray(path) || !path.length) return;
|
|
3978
|
+
let cur = root;
|
|
3979
|
+
for (const seg of path) {
|
|
3980
|
+
if (!seg) return;
|
|
3981
|
+
cur.children = cur.children || {};
|
|
3982
|
+
cur.children[seg] = cur.children[seg] || {};
|
|
3983
|
+
cur = cur.children[seg];
|
|
3984
|
+
}
|
|
3985
|
+
cur.deprecated = deprecated;
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
function __mergeDeprecatedTree(base: RunJSDeprecatedTreeNode, patch: RunJSDeprecatedTreeNode) {
|
|
3989
|
+
if (patch.deprecated !== undefined) base.deprecated = patch.deprecated;
|
|
3990
|
+
const pChildren = patch.children || {};
|
|
3991
|
+
const keys = Object.keys(pChildren);
|
|
3992
|
+
if (!keys.length) return;
|
|
3993
|
+
base.children = base.children || {};
|
|
3994
|
+
for (const k of keys) {
|
|
3995
|
+
base.children[k] = base.children[k] || {};
|
|
3996
|
+
__mergeDeprecatedTree(base.children[k], pChildren[k]);
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
function __buildDeprecatedTreeFromRunJSDoc(doc?: RunJSDocMeta): RunJSDeprecatedTreeNode {
|
|
4001
|
+
const root: RunJSDeprecatedTreeNode = {};
|
|
4002
|
+
if (!doc) return root;
|
|
4003
|
+
|
|
4004
|
+
const walkProps = (props: any, parentPath: string[]) => {
|
|
4005
|
+
if (!__isPlainObject(props)) return;
|
|
4006
|
+
for (const [key, raw] of Object.entries(props)) {
|
|
4007
|
+
if (!key) continue;
|
|
4008
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
|
4009
|
+
const node = raw as any;
|
|
4010
|
+
const dep = __normalizeDeprecationDoc(node.deprecated);
|
|
4011
|
+
if (dep) __addDeprecatedPath(root, [...parentPath, key], dep);
|
|
4012
|
+
if (__isPlainObject(node.properties)) {
|
|
4013
|
+
walkProps(node.properties, [...parentPath, key]);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
};
|
|
4017
|
+
|
|
4018
|
+
const walkMethods = (methods: any) => {
|
|
4019
|
+
if (!__isPlainObject(methods)) return;
|
|
4020
|
+
for (const [key, raw] of Object.entries(methods)) {
|
|
4021
|
+
if (!key) continue;
|
|
4022
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
|
4023
|
+
const node = raw as any;
|
|
4024
|
+
const dep = __normalizeDeprecationDoc(node.deprecated);
|
|
4025
|
+
if (dep) __addDeprecatedPath(root, [key], dep);
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
|
|
4029
|
+
walkProps((doc as any).properties, []);
|
|
4030
|
+
walkMethods((doc as any).methods);
|
|
4031
|
+
|
|
4032
|
+
return root;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
function __buildDeprecatedTreeFromFlowContextInfos(ctx: any): RunJSDeprecatedTreeNode {
|
|
4036
|
+
const root: RunJSDeprecatedTreeNode = {};
|
|
4037
|
+
const visited = new WeakSet<any>();
|
|
4038
|
+
|
|
4039
|
+
const collectInfoProperties = (basePath: string[], props: any) => {
|
|
4040
|
+
if (!__isPlainObject(props)) return;
|
|
4041
|
+
for (const [key, raw] of Object.entries(props)) {
|
|
4042
|
+
if (!key) continue;
|
|
4043
|
+
if (typeof raw === 'string') continue;
|
|
4044
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
|
4045
|
+
const node = raw as any;
|
|
4046
|
+
const dep = __normalizeDeprecationDoc(node.deprecated);
|
|
4047
|
+
if (dep) __addDeprecatedPath(root, [...basePath, key], dep);
|
|
4048
|
+
if (__isPlainObject(node.properties)) {
|
|
4049
|
+
collectInfoProperties([...basePath, key], node.properties);
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
};
|
|
4053
|
+
|
|
4054
|
+
const walk = (c: any) => {
|
|
4055
|
+
if (!c || (typeof c !== 'object' && typeof c !== 'function')) return;
|
|
4056
|
+
if (visited.has(c)) return;
|
|
4057
|
+
visited.add(c);
|
|
4058
|
+
|
|
4059
|
+
const methodInfos = (c as any)._methodInfos;
|
|
4060
|
+
if (__isPlainObject(methodInfos)) {
|
|
4061
|
+
for (const [name, info] of Object.entries(methodInfos)) {
|
|
4062
|
+
if (!name) continue;
|
|
4063
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) continue;
|
|
4064
|
+
const dep = __normalizeDeprecationDoc((info as any).deprecated);
|
|
4065
|
+
if (dep) __addDeprecatedPath(root, [name], dep);
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
const props = (c as any)._props;
|
|
4070
|
+
if (__isPlainObject(props)) {
|
|
4071
|
+
for (const [name, opt] of Object.entries(props)) {
|
|
4072
|
+
if (!name) continue;
|
|
4073
|
+
const info = (opt as any)?.info;
|
|
4074
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) continue;
|
|
4075
|
+
const dep = __normalizeDeprecationDoc((info as any).deprecated);
|
|
4076
|
+
if (dep) __addDeprecatedPath(root, [name], dep);
|
|
4077
|
+
if (__isPlainObject((info as any).properties)) {
|
|
4078
|
+
collectInfoProperties([name], (info as any).properties);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
const delegates = (c as any)._delegates;
|
|
4084
|
+
if (Array.isArray(delegates)) {
|
|
4085
|
+
for (const d of delegates) walk(d);
|
|
4086
|
+
}
|
|
4087
|
+
};
|
|
4088
|
+
|
|
4089
|
+
walk(ctx);
|
|
4090
|
+
return root;
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
export function createRunJSDeprecationProxy(
|
|
4094
|
+
ctx: any,
|
|
4095
|
+
options: {
|
|
4096
|
+
doc?: RunJSDocMeta;
|
|
4097
|
+
} = {},
|
|
4098
|
+
) {
|
|
4099
|
+
const fromDoc = __buildDeprecatedTreeFromRunJSDoc(options.doc);
|
|
4100
|
+
const fromInfo = __buildDeprecatedTreeFromFlowContextInfos(ctx);
|
|
4101
|
+
__mergeDeprecatedTree(fromDoc, fromInfo);
|
|
4102
|
+
|
|
4103
|
+
const warned = new Set<string>();
|
|
4104
|
+
const proxyToTarget = new WeakMap<object, object>();
|
|
4105
|
+
const objectProxyCache = new WeakMap<object, Map<string, any>>();
|
|
4106
|
+
const functionProxyCache = new WeakMap<Function, Map<string, any>>();
|
|
4107
|
+
|
|
4108
|
+
const extractRunJSLocation = (
|
|
4109
|
+
stack?: string,
|
|
4110
|
+
): { line?: number; column?: number; rawLine?: number; rawColumn?: number } => {
|
|
4111
|
+
if (!stack || typeof stack !== 'string') return {};
|
|
4112
|
+
const WRAPPER_PREFIX_LINES = 2; // JSRunner.run wraps user code with 2 lines before `${code}`
|
|
4113
|
+
const lines = stack.split('\n');
|
|
4114
|
+
for (const l of lines) {
|
|
4115
|
+
if (!l) continue;
|
|
4116
|
+
const m = l.match(/<anonymous>:(\d+):(\d+)/);
|
|
4117
|
+
if (!m) continue;
|
|
4118
|
+
const rawLine = Number(m[1]);
|
|
4119
|
+
const rawColumn = Number(m[2]);
|
|
4120
|
+
const line =
|
|
4121
|
+
Number.isFinite(rawLine) && rawLine > WRAPPER_PREFIX_LINES ? rawLine - WRAPPER_PREFIX_LINES : rawLine;
|
|
4122
|
+
const column = Number.isFinite(rawColumn) ? rawColumn : undefined;
|
|
4123
|
+
return { line, column, rawLine, rawColumn };
|
|
4124
|
+
}
|
|
4125
|
+
return {};
|
|
4126
|
+
};
|
|
4127
|
+
|
|
4128
|
+
const collectInfoProperties = (basePath: string[], props: any) => {
|
|
4129
|
+
if (!__isPlainObject(props)) return;
|
|
4130
|
+
for (const [key, raw] of Object.entries(props)) {
|
|
4131
|
+
if (!key) continue;
|
|
4132
|
+
if (typeof raw === 'string') continue;
|
|
4133
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
|
4134
|
+
const node = raw as any;
|
|
4135
|
+
const dep = __normalizeDeprecationDoc(node.deprecated);
|
|
4136
|
+
if (dep) __addDeprecatedPath(fromDoc, [...basePath, key], dep);
|
|
4137
|
+
if (__isPlainObject(node.properties)) {
|
|
4138
|
+
collectInfoProperties([...basePath, key], node.properties);
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
|
|
4143
|
+
const updateTreeFromDefineProperty = (name: string, options: any) => {
|
|
4144
|
+
if (!name) return;
|
|
4145
|
+
const info = options?.info;
|
|
4146
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) return;
|
|
4147
|
+
const dep = __normalizeDeprecationDoc((info as any).deprecated);
|
|
4148
|
+
if (dep) __addDeprecatedPath(fromDoc, [name], dep);
|
|
4149
|
+
if (__isPlainObject((info as any).properties)) {
|
|
4150
|
+
collectInfoProperties([name], (info as any).properties);
|
|
4151
|
+
}
|
|
4152
|
+
};
|
|
4153
|
+
|
|
4154
|
+
const updateTreeFromDefineMethod = (name: string, info: any) => {
|
|
4155
|
+
if (!name) return;
|
|
4156
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) return;
|
|
4157
|
+
const dep = __normalizeDeprecationDoc((info as any).deprecated);
|
|
4158
|
+
if (dep) __addDeprecatedPath(fromDoc, [name], dep);
|
|
4159
|
+
};
|
|
4160
|
+
|
|
4161
|
+
const unwrapProxy = (val: any) => {
|
|
4162
|
+
let cur = val;
|
|
4163
|
+
while (cur && (typeof cur === 'object' || typeof cur === 'function')) {
|
|
4164
|
+
const mapped = proxyToTarget.get(cur as any);
|
|
4165
|
+
if (!mapped) break;
|
|
4166
|
+
cur = mapped;
|
|
4167
|
+
}
|
|
4168
|
+
return cur;
|
|
4169
|
+
};
|
|
4170
|
+
|
|
4171
|
+
const formatReplacedBy = (replacedBy: any): string | undefined => {
|
|
4172
|
+
if (!replacedBy) return undefined;
|
|
4173
|
+
if (typeof replacedBy === 'string') return replacedBy.trim() || undefined;
|
|
4174
|
+
if (Array.isArray(replacedBy)) {
|
|
4175
|
+
const parts = replacedBy.map((x) => (typeof x === 'string' ? x.trim() : '')).filter(Boolean);
|
|
4176
|
+
return parts.length ? parts.join(', ') : undefined;
|
|
4177
|
+
}
|
|
4178
|
+
return undefined;
|
|
4179
|
+
};
|
|
4180
|
+
|
|
4181
|
+
const warnOnce = (apiPath: string, deprecated: FlowDeprecationDoc, stack?: string) => {
|
|
4182
|
+
if (!apiPath) return;
|
|
4183
|
+
if (warned.has(apiPath)) return;
|
|
4184
|
+
warned.add(apiPath);
|
|
4185
|
+
|
|
4186
|
+
const logger = (ctx as any)?.logger;
|
|
4187
|
+
const t =
|
|
4188
|
+
typeof (ctx as any)?.t === 'function'
|
|
4189
|
+
? (key: string, options?: any) =>
|
|
4190
|
+
(ctx as any).t(key, { ns: [FLOW_ENGINE_NAMESPACE, 'client'], nsMode: 'fallback', ...options })
|
|
4191
|
+
: (key: string, options?: any) => {
|
|
4192
|
+
const fallback = options?.defaultValue ?? key;
|
|
4193
|
+
if (typeof fallback !== 'string' || !options) return fallback;
|
|
4194
|
+
// lightweight interpolation for fallback strings (i18next-style: {{var}})
|
|
4195
|
+
return fallback.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_m, k) => {
|
|
4196
|
+
const v = options?.[k];
|
|
4197
|
+
return typeof v === 'string' || typeof v === 'number' ? String(v) : '';
|
|
4198
|
+
});
|
|
4199
|
+
};
|
|
4200
|
+
const meta = typeof deprecated === 'object' && deprecated ? deprecated : {};
|
|
4201
|
+
const replacedBy = formatReplacedBy((meta as any).replacedBy);
|
|
4202
|
+
const since = typeof (meta as any).since === 'string' ? String((meta as any).since) : undefined;
|
|
4203
|
+
const removedIn = typeof (meta as any).removedIn === 'string' ? String((meta as any).removedIn) : undefined;
|
|
4204
|
+
const message = typeof (meta as any).message === 'string' ? String((meta as any).message) : '';
|
|
4205
|
+
|
|
4206
|
+
const loc = extractRunJSLocation(stack);
|
|
4207
|
+
|
|
4208
|
+
const locText = loc.line ? `(line ${loc.line}${loc.column ? `:${loc.column}` : ''})` : '';
|
|
4209
|
+
|
|
4210
|
+
const msg = message.trim();
|
|
4211
|
+
const mainText = msg
|
|
4212
|
+
? t('RunJS deprecated warning with message', {
|
|
4213
|
+
defaultValue: '[RunJS][Deprecated] {{api}} {{message}}{{location}}',
|
|
4214
|
+
api: apiPath,
|
|
4215
|
+
message: msg,
|
|
4216
|
+
location: locText,
|
|
4217
|
+
})
|
|
4218
|
+
: t('RunJS deprecated warning', {
|
|
4219
|
+
defaultValue: '[RunJS][Deprecated] {{api}} is deprecated{{location}}',
|
|
4220
|
+
api: apiPath,
|
|
4221
|
+
location: locText,
|
|
4222
|
+
});
|
|
4223
|
+
|
|
4224
|
+
const separator = t('RunJS deprecated separator', { defaultValue: '; ' });
|
|
4225
|
+
const textParts: string[] = [mainText];
|
|
4226
|
+
if (replacedBy)
|
|
4227
|
+
textParts.push(t('RunJS deprecated replacedBy', { defaultValue: 'Use {{replacedBy}} instead', replacedBy }));
|
|
4228
|
+
if (since) textParts.push(t('RunJS deprecated since', { defaultValue: 'since {{since}}', since }));
|
|
4229
|
+
if (removedIn)
|
|
4230
|
+
textParts.push(t('RunJS deprecated removedIn', { defaultValue: 'will be removed in {{removedIn}}', removedIn }));
|
|
4231
|
+
const text = textParts.filter(Boolean).join(separator);
|
|
4232
|
+
|
|
4233
|
+
try {
|
|
4234
|
+
if (logger && typeof logger.warn === 'function') {
|
|
4235
|
+
logger.warn(text);
|
|
4236
|
+
} else {
|
|
4237
|
+
// fail-open: avoid breaking runjs execution when logger is missing
|
|
4238
|
+
console.warn(text);
|
|
4239
|
+
}
|
|
4240
|
+
} catch (_) {
|
|
4241
|
+
// ignore logger failures
|
|
4242
|
+
}
|
|
4243
|
+
};
|
|
4244
|
+
|
|
4245
|
+
const createFunctionProxy = (fn: Function, node: RunJSDeprecatedTreeNode, path: string) => {
|
|
4246
|
+
const dep = node.deprecated;
|
|
4247
|
+
if (!dep) return fn;
|
|
4248
|
+
|
|
4249
|
+
const cacheByPath = functionProxyCache.get(fn) || new Map<string, any>();
|
|
4250
|
+
functionProxyCache.set(fn, cacheByPath);
|
|
4251
|
+
if (cacheByPath.has(path)) return cacheByPath.get(path);
|
|
4252
|
+
|
|
4253
|
+
const proxied = new Proxy(fn, {
|
|
4254
|
+
apply(target, thisArg, argArray) {
|
|
4255
|
+
const stack = warned.has(path) ? undefined : new Error().stack;
|
|
4256
|
+
warnOnce(path, dep, stack);
|
|
4257
|
+
const realThis = unwrapProxy(thisArg);
|
|
4258
|
+
return Reflect.apply(target, realThis, argArray);
|
|
4259
|
+
},
|
|
4260
|
+
get(target, key, receiver) {
|
|
4261
|
+
return Reflect.get(target, key, receiver);
|
|
4262
|
+
},
|
|
4263
|
+
});
|
|
4264
|
+
|
|
4265
|
+
cacheByPath.set(path, proxied);
|
|
4266
|
+
return proxied as any;
|
|
4267
|
+
};
|
|
4268
|
+
|
|
4269
|
+
const createObjectProxy = (target: any, node: RunJSDeprecatedTreeNode, path: string): any => {
|
|
4270
|
+
if (!target || (typeof target !== 'object' && typeof target !== 'function')) return target;
|
|
4271
|
+
if (__isPromiseLike(target)) return target;
|
|
4272
|
+
const hasChildren = !!node.children && Object.keys(node.children).length > 0;
|
|
4273
|
+
if (!hasChildren && path !== 'ctx') return target;
|
|
4274
|
+
|
|
4275
|
+
const cacheByPath = objectProxyCache.get(target) || new Map<string, any>();
|
|
4276
|
+
objectProxyCache.set(target, cacheByPath);
|
|
4277
|
+
if (cacheByPath.has(path)) return cacheByPath.get(path);
|
|
4278
|
+
|
|
4279
|
+
const proxied = new Proxy(target, {
|
|
4280
|
+
get(t, key, receiver) {
|
|
4281
|
+
if (typeof key === 'symbol') {
|
|
4282
|
+
return Reflect.get(t, key, unwrapProxy(receiver));
|
|
4283
|
+
}
|
|
4284
|
+
const prop = String(key);
|
|
4285
|
+
const value = Reflect.get(t, key, unwrapProxy(receiver));
|
|
4286
|
+
|
|
4287
|
+
// Support dynamic deprecation registration via ctx.defineProperty/defineMethod during RunJS execution.
|
|
4288
|
+
// - This is especially useful when the deprecated API is introduced after JSRunner is created.
|
|
4289
|
+
if (path === 'ctx' && prop === 'defineProperty' && typeof value === 'function') {
|
|
4290
|
+
return (...args: any[]) => {
|
|
4291
|
+
const result = value(...args);
|
|
4292
|
+
try {
|
|
4293
|
+
updateTreeFromDefineProperty(String(args?.[0] ?? ''), args?.[1]);
|
|
4294
|
+
} catch (_) {
|
|
4295
|
+
// ignore
|
|
4296
|
+
}
|
|
4297
|
+
return result;
|
|
4298
|
+
};
|
|
4299
|
+
}
|
|
4300
|
+
if (path === 'ctx' && prop === 'defineMethod' && typeof value === 'function') {
|
|
4301
|
+
return (...args: any[]) => {
|
|
4302
|
+
const result = value(...args);
|
|
4303
|
+
try {
|
|
4304
|
+
updateTreeFromDefineMethod(String(args?.[0] ?? ''), args?.[2]);
|
|
4305
|
+
} catch (_) {
|
|
4306
|
+
// ignore
|
|
4307
|
+
}
|
|
4308
|
+
return result;
|
|
4309
|
+
};
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
const child = node.children?.[prop];
|
|
4313
|
+
if (!child) return value;
|
|
4314
|
+
|
|
4315
|
+
const childPath = `${path}.${prop}`;
|
|
4316
|
+
if (typeof value === 'function' && child.deprecated) {
|
|
4317
|
+
return createFunctionProxy(value, child, childPath);
|
|
4318
|
+
}
|
|
4319
|
+
if (child.deprecated) {
|
|
4320
|
+
// For non-callable APIs, "use" happens on access (there is no apply step).
|
|
4321
|
+
const stack = warned.has(childPath) ? undefined : new Error().stack;
|
|
4322
|
+
warnOnce(childPath, child.deprecated, stack);
|
|
4323
|
+
}
|
|
4324
|
+
if (value && (typeof value === 'object' || typeof value === 'function') && child.children) {
|
|
4325
|
+
return createObjectProxy(value, child, childPath);
|
|
4326
|
+
}
|
|
4327
|
+
return value;
|
|
4328
|
+
},
|
|
4329
|
+
has(t, key) {
|
|
4330
|
+
return Reflect.has(t, key);
|
|
4331
|
+
},
|
|
4332
|
+
});
|
|
4333
|
+
|
|
4334
|
+
proxyToTarget.set(proxied as any, target);
|
|
4335
|
+
cacheByPath.set(path, proxied);
|
|
4336
|
+
return proxied;
|
|
4337
|
+
};
|
|
4338
|
+
|
|
4339
|
+
return createObjectProxy(ctx, fromDoc, 'ctx');
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
function __mergeRunJSDocDocRecord(base: any, patch: any, mergeDoc: (b: any, p: any) => any): any {
|
|
4343
|
+
if (!__isPlainObject(patch)) return base;
|
|
4344
|
+
// Important: preserve `null` markers when base is not an object (e.g. child class wants to delete parent keys).
|
|
4345
|
+
// If we eagerly delete them here, the deletion intent is lost for later merges in the inheritance chain.
|
|
4346
|
+
if (!__isPlainObject(base)) return patch;
|
|
4347
|
+
const out: any = { ...base };
|
|
4348
|
+
for (const k of Object.keys(patch)) {
|
|
4349
|
+
const pv = patch[k];
|
|
4350
|
+
if (pv === null) {
|
|
4351
|
+
delete out[k];
|
|
4352
|
+
continue;
|
|
4353
|
+
}
|
|
4354
|
+
const bv = __isPlainObject(base) ? base[k] : undefined;
|
|
4355
|
+
const merged = mergeDoc(bv, pv);
|
|
4356
|
+
if (typeof merged === 'undefined') delete out[k];
|
|
4357
|
+
else out[k] = merged;
|
|
4358
|
+
}
|
|
4359
|
+
return out;
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
function __mergeRunJSDocPropertyDoc(base: any, patch: any): any {
|
|
4363
|
+
if (patch === null) return undefined;
|
|
4364
|
+
|
|
4365
|
+
const baseIsObj = __isPlainObject(base);
|
|
4366
|
+
const patchIsObj = __isPlainObject(patch);
|
|
4367
|
+
const baseIsStr = typeof base === 'string';
|
|
4368
|
+
const patchIsStr = typeof patch === 'string';
|
|
4369
|
+
|
|
4370
|
+
// Treat string docs as { description: string } when merging with object docs,
|
|
4371
|
+
// to avoid "whole replacement" that drops base hidden/properties/completion.
|
|
4372
|
+
if (patchIsStr) {
|
|
4373
|
+
if (baseIsObj) {
|
|
4374
|
+
return __mergeRunJSDocPropertyDoc(base, { description: patch });
|
|
4375
|
+
}
|
|
4376
|
+
return patch;
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
if (patchIsObj) {
|
|
4380
|
+
const baseObj: any = baseIsObj ? base : baseIsStr ? { description: base } : undefined;
|
|
4381
|
+
const out: any = { ...(baseObj || {}) };
|
|
4382
|
+
for (const k of Object.keys(patch)) {
|
|
4383
|
+
if (k === 'properties') {
|
|
4384
|
+
const pv = (patch as any).properties;
|
|
4385
|
+
if (pv === null) {
|
|
4386
|
+
delete out.properties;
|
|
4387
|
+
continue;
|
|
4388
|
+
}
|
|
4389
|
+
const mergedProps = __mergeRunJSDocDocRecord(baseObj?.properties, pv, __mergeRunJSDocPropertyDoc);
|
|
4390
|
+
if (typeof mergedProps === 'undefined') delete out.properties;
|
|
4391
|
+
else out.properties = mergedProps;
|
|
4392
|
+
continue;
|
|
4393
|
+
}
|
|
4394
|
+
const mergedVal = __runjsDeepMerge(baseObj?.[k], (patch as any)[k]);
|
|
4395
|
+
if (typeof mergedVal === 'undefined') delete out[k];
|
|
4396
|
+
else out[k] = mergedVal;
|
|
4397
|
+
}
|
|
4398
|
+
return out;
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
return patch ?? base;
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
function __mergeRunJSDocMethodDoc(base: any, patch: any): any {
|
|
4405
|
+
if (patch === null) return undefined;
|
|
4406
|
+
|
|
4407
|
+
const baseIsObj = __isPlainObject(base);
|
|
4408
|
+
const patchIsObj = __isPlainObject(patch);
|
|
4409
|
+
const baseIsStr = typeof base === 'string';
|
|
4410
|
+
const patchIsStr = typeof patch === 'string';
|
|
4411
|
+
|
|
4412
|
+
if (patchIsStr) {
|
|
4413
|
+
if (baseIsObj) {
|
|
4414
|
+
return __mergeRunJSDocMethodDoc(base, { description: patch });
|
|
4415
|
+
}
|
|
4416
|
+
return patch;
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
if (patchIsObj) {
|
|
4420
|
+
const baseObj: any = baseIsObj ? base : baseIsStr ? { description: base } : undefined;
|
|
4421
|
+
const out: any = { ...(baseObj || {}) };
|
|
4422
|
+
for (const k of Object.keys(patch)) {
|
|
4423
|
+
const mergedVal = __runjsDeepMerge(baseObj?.[k], (patch as any)[k]);
|
|
4424
|
+
if (typeof mergedVal === 'undefined') delete out[k];
|
|
4425
|
+
else out[k] = mergedVal;
|
|
4426
|
+
}
|
|
4427
|
+
return out;
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
return patch ?? base;
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
function __mergeRunJSDocMeta(base: any, patch: any): RunJSDocMeta {
|
|
4434
|
+
const baseObj: any = __isPlainObject(base) ? base : {};
|
|
4435
|
+
const patchObj: any = __isPlainObject(patch) ? patch : {};
|
|
4436
|
+
const out: any = { ...baseObj };
|
|
4437
|
+
|
|
4438
|
+
for (const k of Object.keys(patchObj)) {
|
|
4439
|
+
if (k === 'properties') {
|
|
4440
|
+
const mergedProps = __mergeRunJSDocDocRecord(baseObj.properties, patchObj.properties, __mergeRunJSDocPropertyDoc);
|
|
4441
|
+
if (typeof mergedProps === 'undefined') delete out.properties;
|
|
4442
|
+
else out.properties = mergedProps;
|
|
4443
|
+
continue;
|
|
4444
|
+
}
|
|
4445
|
+
if (k === 'methods') {
|
|
4446
|
+
const mergedMethods = __mergeRunJSDocDocRecord(baseObj.methods, patchObj.methods, __mergeRunJSDocMethodDoc);
|
|
4447
|
+
if (typeof mergedMethods === 'undefined') delete out.methods;
|
|
4448
|
+
else out.methods = mergedMethods;
|
|
4449
|
+
continue;
|
|
4450
|
+
}
|
|
4451
|
+
const mergedVal = __runjsDeepMerge(baseObj[k], patchObj[k]);
|
|
4452
|
+
if (typeof mergedVal === 'undefined') delete out[k];
|
|
4453
|
+
else out[k] = mergedVal;
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
return out as RunJSDocMeta;
|
|
4457
|
+
}
|
|
1720
4458
|
export class FlowRunJSContext extends FlowContext {
|
|
1721
4459
|
constructor(delegate: FlowContext) {
|
|
1722
4460
|
super();
|
|
1723
4461
|
this.addDelegate(delegate);
|
|
1724
4462
|
this.defineProperty('React', { value: React });
|
|
1725
4463
|
this.defineProperty('antd', { value: antd });
|
|
1726
|
-
this.defineProperty('dayjs', {
|
|
4464
|
+
this.defineProperty('dayjs', {
|
|
4465
|
+
value: dayjs,
|
|
4466
|
+
});
|
|
1727
4467
|
// 为 JS 运行时代码提供带有 antd/App/ConfigProvider 包裹的 React 根
|
|
1728
4468
|
// 保持与 ReactDOMClient 接口一致,优先覆盖 createRoot,其余方法透传
|
|
1729
4469
|
const ReactDOMShim: any = {
|
|
@@ -1735,19 +4475,10 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1735
4475
|
return this.engine.reactView.createRoot(realContainer as HTMLElement, options);
|
|
1736
4476
|
},
|
|
1737
4477
|
};
|
|
4478
|
+
ReactDOMShim.__nbRunjsInternalShim = true;
|
|
1738
4479
|
this.defineProperty('ReactDOM', { value: ReactDOMShim });
|
|
1739
4480
|
|
|
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 });
|
|
4481
|
+
setupRunJSLibs(this);
|
|
1751
4482
|
|
|
1752
4483
|
// Convenience: ctx.render(<App />[, container])
|
|
1753
4484
|
// - container defaults to ctx.element if available
|
|
@@ -1767,16 +4498,37 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1767
4498
|
globalRef.__nbRunjsRoots = globalRef.__nbRunjsRoots || new WeakMap<any, any>();
|
|
1768
4499
|
const rootMap: WeakMap<any, any> = globalRef.__nbRunjsRoots;
|
|
1769
4500
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
4501
|
+
const disposeEntry = (entry: any) => {
|
|
4502
|
+
if (!entry) return;
|
|
4503
|
+
if (entry.disposeTheme && typeof entry.disposeTheme === 'function') {
|
|
4504
|
+
try {
|
|
4505
|
+
entry.disposeTheme();
|
|
4506
|
+
} catch (_) {
|
|
4507
|
+
// ignore
|
|
4508
|
+
}
|
|
4509
|
+
entry.disposeTheme = undefined;
|
|
4510
|
+
}
|
|
4511
|
+
const root = entry.root || entry;
|
|
4512
|
+
if (root && typeof root.unmount === 'function') {
|
|
1774
4513
|
try {
|
|
1775
|
-
|
|
1776
|
-
}
|
|
1777
|
-
|
|
4514
|
+
root.unmount();
|
|
4515
|
+
} catch (_) {
|
|
4516
|
+
// ignore
|
|
1778
4517
|
}
|
|
1779
4518
|
}
|
|
4519
|
+
};
|
|
4520
|
+
|
|
4521
|
+
const unmountContainerRoot = () => {
|
|
4522
|
+
const existing = rootMap.get(containerEl);
|
|
4523
|
+
if (existing) {
|
|
4524
|
+
disposeEntry(existing);
|
|
4525
|
+
rootMap.delete(containerEl);
|
|
4526
|
+
}
|
|
4527
|
+
};
|
|
4528
|
+
|
|
4529
|
+
// If vnode is string (HTML), unmount react root and set sanitized HTML
|
|
4530
|
+
if (typeof vnode === 'string') {
|
|
4531
|
+
unmountContainerRoot();
|
|
1780
4532
|
const proxy: any = new ElementProxy(containerEl);
|
|
1781
4533
|
proxy.innerHTML = String(vnode ?? '');
|
|
1782
4534
|
return null;
|
|
@@ -1788,39 +4540,64 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1788
4540
|
(vnode as any).nodeType &&
|
|
1789
4541
|
((vnode as any).nodeType === 1 || (vnode as any).nodeType === 3 || (vnode as any).nodeType === 11)
|
|
1790
4542
|
) {
|
|
1791
|
-
|
|
1792
|
-
if (existingRoot && typeof existingRoot.unmount === 'function') {
|
|
1793
|
-
try {
|
|
1794
|
-
existingRoot.unmount();
|
|
1795
|
-
} finally {
|
|
1796
|
-
rootMap.delete(containerEl);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
4543
|
+
unmountContainerRoot();
|
|
1799
4544
|
while (containerEl.firstChild) containerEl.removeChild(containerEl.firstChild);
|
|
1800
4545
|
containerEl.appendChild(vnode as any);
|
|
1801
4546
|
return null;
|
|
1802
4547
|
}
|
|
1803
4548
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
4549
|
+
// 注意:rootMap 是“全局按容器复用”的(key=containerEl)。
|
|
4550
|
+
// 若不同 RunJS ctx 复用同一个 containerEl,且 ReactDOM 实例引用也相同,
|
|
4551
|
+
// 则会复用到旧 entry,进而复用旧 ctx 创建的 autorun(闭包捕获旧 ctx),造成:
|
|
4552
|
+
// 1) 旧 ctx 的 reaction 继续驱动新渲染(跨 ctx 复用风险)
|
|
4553
|
+
// 2) 新 ctx 的主题变化不再触发 rerender
|
|
4554
|
+
// 3) 旧 ctx 被 entry/autorun 间接持有,无法被 GC(内存泄漏)
|
|
4555
|
+
// 因此这里把 ownerKey(当前 ctx)也纳入复用判断;owner 变化时必须重建 entry。
|
|
4556
|
+
const rendererKey = this.ReactDOM;
|
|
4557
|
+
const ownerKey = this;
|
|
4558
|
+
let entry = rootMap.get(containerEl);
|
|
4559
|
+
if (!entry || entry.rendererKey !== rendererKey || entry.ownerKey !== ownerKey) {
|
|
4560
|
+
if (entry) {
|
|
4561
|
+
disposeEntry(entry);
|
|
4562
|
+
rootMap.delete(containerEl);
|
|
4563
|
+
}
|
|
4564
|
+
const root = this.ReactDOM.createRoot(containerEl);
|
|
4565
|
+
entry = { rendererKey, ownerKey, root, disposeTheme: undefined, lastVnode: undefined };
|
|
4566
|
+
rootMap.set(containerEl, entry);
|
|
1808
4567
|
}
|
|
1809
|
-
|
|
1810
|
-
return
|
|
4568
|
+
|
|
4569
|
+
return externalReactRender({
|
|
4570
|
+
ctx: this,
|
|
4571
|
+
entry,
|
|
4572
|
+
vnode,
|
|
4573
|
+
containerEl,
|
|
4574
|
+
rootMap,
|
|
4575
|
+
unmountContainerRoot,
|
|
4576
|
+
internalReact: React,
|
|
4577
|
+
internalAntd: antd,
|
|
4578
|
+
});
|
|
1811
4579
|
},
|
|
1812
4580
|
);
|
|
1813
4581
|
}
|
|
4582
|
+
|
|
4583
|
+
exit() {
|
|
4584
|
+
throw new FlowExitAllException(this.flowKey, this.model?.uid || 'runjs');
|
|
4585
|
+
}
|
|
4586
|
+
|
|
4587
|
+
exitAll() {
|
|
4588
|
+
throw new FlowExitAllException(this.flowKey, this.model?.uid || 'runjs');
|
|
4589
|
+
}
|
|
4590
|
+
|
|
1814
4591
|
static define(meta: RunJSDocMeta, options?: { locale?: string }) {
|
|
1815
4592
|
const locale = options?.locale;
|
|
1816
4593
|
if (locale) {
|
|
1817
4594
|
const map = __runjsClassLocaleMeta.get(this) || new Map<string, RunJSDocMeta>();
|
|
1818
4595
|
const prev = map.get(locale) || {};
|
|
1819
|
-
map.set(locale,
|
|
4596
|
+
map.set(locale, __mergeRunJSDocMeta(prev, meta));
|
|
1820
4597
|
__runjsClassLocaleMeta.set(this, map);
|
|
1821
4598
|
} else {
|
|
1822
4599
|
const prev = __runjsClassDefaultMeta.get(this) || {};
|
|
1823
|
-
__runjsClassDefaultMeta.set(this,
|
|
4600
|
+
__runjsClassDefaultMeta.set(this, __mergeRunJSDocMeta(prev, meta));
|
|
1824
4601
|
}
|
|
1825
4602
|
__runjsDocCache.delete(this);
|
|
1826
4603
|
}
|
|
@@ -1828,7 +4605,7 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1828
4605
|
const self = this as any as Function;
|
|
1829
4606
|
let cacheForClass = __runjsDocCache.get(self);
|
|
1830
4607
|
const cacheKey = String(locale || 'default');
|
|
1831
|
-
if (cacheForClass && cacheForClass.has(cacheKey)) return cacheForClass.get(cacheKey)
|
|
4608
|
+
if (cacheForClass && cacheForClass.has(cacheKey)) return cacheForClass.get(cacheKey) as RunJSDocMeta;
|
|
1832
4609
|
const chain: Function[] = [];
|
|
1833
4610
|
let cur: any = self;
|
|
1834
4611
|
while (cur && cur.prototype) {
|
|
@@ -1837,13 +4614,13 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
1837
4614
|
}
|
|
1838
4615
|
let merged: RunJSDocMeta = {};
|
|
1839
4616
|
for (const cls of chain) {
|
|
1840
|
-
merged =
|
|
4617
|
+
merged = __mergeRunJSDocMeta(merged, __runjsClassDefaultMeta.get(cls) || {});
|
|
1841
4618
|
}
|
|
1842
4619
|
if (locale) {
|
|
1843
4620
|
for (const cls of chain) {
|
|
1844
4621
|
const lmap = __runjsClassLocaleMeta.get(cls);
|
|
1845
4622
|
if (lmap && lmap.has(locale)) {
|
|
1846
|
-
merged =
|
|
4623
|
+
merged = __mergeRunJSDocMeta(merged, lmap.get(locale));
|
|
1847
4624
|
}
|
|
1848
4625
|
}
|
|
1849
4626
|
}
|