@nocobase/flow-engine 2.0.0-alpha.9 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/BlockScopedFlowEngine.d.ts +23 -0
- package/lib/BlockScopedFlowEngine.js +92 -0
- package/lib/FlowDefinition.d.ts +6 -4
- package/lib/JSRunner.js +3 -0
- package/lib/ViewScopedFlowEngine.js +15 -1
- package/lib/acl/Acl.d.ts +12 -12
- package/lib/acl/Acl.js +78 -30
- package/lib/components/DynamicFlowsEditor.js +2 -4
- package/lib/components/FieldModelRenderer.js +10 -8
- package/lib/components/FieldSkeleton.d.ts +10 -0
- package/lib/components/FieldSkeleton.js +64 -0
- package/lib/components/FlowContextSelector.js +19 -3
- package/lib/components/FlowModelRenderer.d.ts +2 -1
- package/lib/components/FlowModelRenderer.js +34 -12
- package/lib/components/FormItem.js +5 -1
- package/lib/components/MobilePopup.d.ts +20 -0
- package/lib/components/MobilePopup.js +102 -0
- package/lib/components/MobilePopup.style.d.ts +17 -0
- package/lib/components/MobilePopup.style.js +186 -0
- package/lib/components/common/withFlowDesignMode.d.ts +1 -1
- package/lib/components/common/withFlowDesignMode.js +5 -5
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +3 -1
- package/lib/components/settings/independents/dropdown/FlowsDropdownButton.js +71 -53
- package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +19 -0
- package/lib/components/settings/wrappers/component/SelectWithTitle.js +136 -0
- package/lib/components/settings/wrappers/component/SwitchWithTitle.d.ts +10 -0
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +110 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +221 -93
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +71 -54
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +2 -2
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +63 -23
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +11 -6
- package/lib/components/settings/wrappers/embedded/FlowSettings.js +42 -28
- package/lib/components/settings/wrappers/embedded/FlowsSettings.js +3 -3
- package/lib/components/settings/wrappers/embedded/FlowsSettingsContent.js +52 -32
- package/lib/components/subModel/AddSubModelButton.d.ts +7 -0
- package/lib/components/subModel/AddSubModelButton.js +78 -8
- package/lib/components/subModel/LazyDropdown.js +14 -15
- package/lib/components/subModel/utils.d.ts +1 -1
- package/lib/components/subModel/utils.js +21 -11
- package/lib/components/variables/VariableInput.js +5 -3
- package/lib/components/variables/types.d.ts +2 -0
- package/lib/components/variables/utils.js +4 -2
- package/lib/data-source/index.d.ts +43 -4
- package/lib/data-source/index.js +104 -11
- package/lib/data-source/jioToJoiSchema.js +1 -0
- package/lib/emitter.d.ts +6 -0
- package/lib/emitter.js +12 -0
- package/lib/executor/FlowExecutor.js +48 -7
- package/lib/flow-registry/GlobalFlowRegistry.d.ts +1 -0
- package/lib/flow-registry/GlobalFlowRegistry.js +3 -0
- package/lib/flow-registry/InstanceFlowRegistry.d.ts +1 -0
- package/lib/flow-registry/InstanceFlowRegistry.js +3 -0
- package/lib/flowContext.d.ts +6 -0
- package/lib/flowContext.js +111 -30
- package/lib/flowEngine.d.ts +49 -0
- package/lib/flowEngine.js +265 -10
- package/lib/flowSettings.d.ts +4 -3
- package/lib/flowSettings.js +33 -11
- package/lib/hooks/useApplyAutoFlows.d.ts +1 -0
- package/lib/hooks/useApplyAutoFlows.js +2 -2
- package/lib/index.d.ts +4 -2
- package/lib/index.js +11 -5
- package/lib/locale/de-DE.json +62 -0
- package/lib/locale/en-US.json +57 -45
- package/lib/locale/es-ES.json +62 -0
- package/lib/locale/fr-FR.json +62 -0
- package/lib/locale/hu-HU.json +62 -0
- package/lib/locale/id-ID.json +62 -0
- package/lib/locale/index.d.ts +114 -90
- package/lib/locale/it-IT.json +62 -0
- package/lib/locale/ja-JP.json +62 -0
- package/lib/locale/ko-KR.json +62 -0
- package/lib/locale/nl-NL.json +62 -0
- package/lib/locale/pt-BR.json +62 -0
- package/lib/locale/ru-RU.json +62 -0
- package/lib/locale/tr-TR.json +62 -0
- package/lib/locale/uk-UA.json +62 -0
- package/lib/locale/vi-VN.json +62 -0
- package/lib/locale/zh-CN.json +58 -46
- package/lib/locale/zh-TW.json +62 -0
- package/lib/models/CollectionFieldModel.d.ts +6 -2
- package/lib/models/CollectionFieldModel.js +60 -14
- package/lib/models/flowModel.d.ts +43 -4
- package/lib/models/flowModel.js +128 -26
- package/lib/models/forkFlowModel.d.ts +6 -2
- package/lib/models/forkFlowModel.js +9 -2
- package/lib/provider.d.ts +3 -1
- package/lib/provider.js +4 -3
- package/lib/reactive/index.d.ts +10 -0
- package/lib/reactive/index.js +41 -0
- package/lib/reactive/observer.d.ts +19 -0
- package/lib/reactive/observer.js +109 -0
- package/lib/resources/baseRecordResource.d.ts +1 -0
- package/lib/resources/baseRecordResource.js +14 -3
- package/lib/resources/multiRecordResource.d.ts +4 -2
- package/lib/resources/multiRecordResource.js +15 -6
- package/lib/resources/singleRecordResource.js +6 -3
- package/lib/resources/sqlResource.d.ts +1 -0
- package/lib/resources/sqlResource.js +22 -25
- package/lib/runjs-context/contexts/base.js +42 -6
- package/lib/runjs-context/snippets/global/clipboard-copy-text.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/clipboard-copy-text.snippet.js +61 -0
- package/lib/runjs-context/snippets/index.js +3 -0
- package/lib/runjs-context/snippets/scene/block/render-antd-icons.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/block/render-antd-icons.snippet.js +65 -0
- package/lib/runjs-context/snippets/scene/block/render-button-handler.snippet.js +6 -4
- package/lib/runjs-context/snippets/scene/block/render-info-card.snippet.js +15 -16
- package/lib/runjs-context/snippets/scene/block/render-react-jsx.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/block/render-react-jsx.snippet.js +58 -0
- package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +7 -7
- package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +24 -29
- package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +20 -21
- package/lib/scheduler/ModelOperationScheduler.d.ts +51 -0
- package/lib/scheduler/ModelOperationScheduler.js +262 -0
- package/lib/types.d.ts +42 -7
- package/lib/types.js +4 -3
- package/lib/utils/associationObjectVariable.d.ts +32 -0
- package/lib/utils/associationObjectVariable.js +157 -0
- package/lib/utils/createCollectionContextMeta.d.ts +1 -1
- package/lib/utils/createCollectionContextMeta.js +8 -4
- package/lib/utils/createEphemeralContext.d.ts +13 -0
- package/lib/utils/createEphemeralContext.js +140 -0
- package/lib/utils/flows.d.ts +10 -0
- package/lib/utils/flows.js +48 -0
- package/lib/utils/index.d.ts +7 -3
- package/lib/utils/index.js +20 -0
- package/lib/utils/jsxTransform.d.ts +15 -0
- package/lib/utils/jsxTransform.js +68 -0
- package/lib/utils/params-resolvers.js +3 -3
- package/lib/utils/parsePathnameToViewParams.d.ts +1 -1
- package/lib/utils/parsePathnameToViewParams.js +41 -5
- package/lib/utils/pruneFilter.d.ts +21 -0
- package/lib/utils/pruneFilter.js +52 -0
- package/lib/utils/safeGlobals.d.ts +5 -3
- package/lib/utils/safeGlobals.js +42 -1
- package/lib/utils/schema-utils.d.ts +6 -0
- package/lib/utils/schema-utils.js +71 -6
- package/lib/utils/serverContextParams.d.ts +3 -0
- package/lib/utils/serverContextParams.js +2 -0
- package/lib/utils/translation.d.ts +4 -1
- package/lib/utils/translation.js +6 -2
- package/lib/utils/variablesParams.d.ts +21 -5
- package/lib/utils/variablesParams.js +103 -34
- package/lib/views/DialogComponent.js +1 -5
- package/lib/views/DrawerComponent.js +18 -9
- package/lib/views/PageComponent.js +3 -4
- package/lib/views/ViewNavigation.d.ts +11 -15
- package/lib/views/ViewNavigation.js +37 -19
- package/lib/views/createViewMeta.d.ts +3 -2
- package/lib/views/createViewMeta.js +164 -53
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +36 -30
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +33 -26
- package/lib/views/usePage.d.ts +2 -1
- package/lib/views/usePage.js +40 -29
- package/package.json +6 -3
- package/src/BlockScopedFlowEngine.ts +88 -0
- package/src/JSRunner.ts +3 -0
- package/src/ViewScopedFlowEngine.ts +16 -0
- package/src/__tests__/JSRunner.test.ts +62 -53
- package/src/__tests__/blockScopedFlowEngine.test.ts +154 -0
- package/src/__tests__/createViewMeta.popup.test.ts +142 -0
- package/src/__tests__/flow-engine.test.ts +3 -0
- package/src/__tests__/flowContext.test.ts +70 -0
- package/src/__tests__/flowEngine.destroyModel.test.ts +74 -0
- package/src/__tests__/flowEngine.moveModel.test.ts +43 -0
- package/src/__tests__/flowEngine.removeModel.test.ts +72 -0
- package/src/__tests__/flowEngine.saveModel.test.ts +4 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +3 -2
- package/src/__tests__/flowSettings.open.test.tsx +2 -0
- package/src/__tests__/flowSettings.test.ts +2 -0
- package/src/__tests__/globalFlowRegistry.test.ts +1 -1
- package/src/__tests__/modelOperationScheduler.test.ts +346 -0
- package/src/__tests__/objectVariable.test.ts +464 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +12 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +98 -0
- package/src/acl/Acl.tsx +85 -31
- package/src/acl/__tests__/Acl.test.tsx +43 -1
- package/src/components/DynamicFlowsEditor.tsx +0 -10
- package/src/components/FieldModelRenderer.tsx +15 -8
- package/src/components/FieldSkeleton.tsx +27 -0
- package/src/components/FlowContextSelector.tsx +20 -2
- package/src/components/FlowModelRenderer.tsx +46 -12
- package/src/components/FormItem.tsx +8 -1
- package/src/components/MobilePopup.style.ts +220 -0
- package/src/components/MobilePopup.tsx +86 -0
- package/src/components/__tests__/FlowModelRenderer.test.tsx +89 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +1 -1
- package/src/components/common/withFlowDesignMode.tsx +5 -5
- package/src/components/index.ts +1 -0
- package/src/components/settings/independents/dropdown/FlowsDropdownButton.tsx +34 -17
- package/src/components/settings/wrappers/component/SelectWithTitle.tsx +110 -0
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +82 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +260 -121
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +34 -18
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +56 -18
- package/src/components/settings/wrappers/contextual/StepSettings.tsx +1 -2
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +12 -6
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +565 -0
- package/src/components/settings/wrappers/embedded/FlowSettings.tsx +47 -35
- package/src/components/settings/wrappers/embedded/FlowsSettings.tsx +1 -1
- package/src/components/settings/wrappers/embedded/FlowsSettingsContent.tsx +64 -42
- package/src/components/subModel/AddSubModelButton.tsx +104 -9
- package/src/components/subModel/LazyDropdown.tsx +14 -14
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +168 -7
- package/src/components/subModel/__tests__/utils.test.ts +12 -12
- package/src/components/subModel/utils.ts +25 -6
- package/src/components/variables/VariableInput.tsx +5 -3
- package/src/components/variables/types.ts +2 -0
- package/src/components/variables/utils.ts +7 -3
- package/src/data-source/index.ts +169 -11
- package/src/data-source/jioToJoiSchema.ts +1 -0
- package/src/emitter.ts +14 -0
- package/src/executor/FlowExecutor.ts +56 -8
- package/src/executor/__tests__/ctx-defs-injection.test.ts +197 -0
- package/src/flow-registry/GlobalFlowRegistry.ts +1 -0
- package/src/flow-registry/InstanceFlowRegistry.ts +1 -0
- package/src/flow-registry/__tests__/globalFlowRegistry.test.ts +54 -0
- package/src/flowContext.ts +144 -29
- package/src/flowEngine.ts +328 -8
- package/src/flowSettings.ts +47 -19
- package/src/hooks/useApplyAutoFlows.ts +3 -3
- package/src/index.ts +4 -2
- package/src/locale/de-DE.json +62 -0
- package/src/locale/en-US.json +57 -45
- package/src/locale/es-ES.json +62 -0
- package/src/locale/fr-FR.json +62 -0
- package/src/locale/hu-HU.json +62 -0
- package/src/locale/id-ID.json +62 -0
- package/src/locale/it-IT.json +62 -0
- package/src/locale/ja-JP.json +62 -0
- package/src/locale/ko-KR.json +62 -0
- package/src/locale/nl-NL.json +62 -0
- package/src/locale/pt-BR.json +62 -0
- package/src/locale/ru-RU.json +62 -0
- package/src/locale/tr-TR.json +62 -0
- package/src/locale/uk-UA.json +62 -0
- package/src/locale/vi-VN.json +62 -0
- package/src/locale/zh-CN.json +58 -46
- package/src/locale/zh-TW.json +62 -0
- package/src/models/CollectionFieldModel.tsx +79 -17
- package/src/models/__tests__/dispatchEvent.behavior.test.ts +169 -0
- package/src/models/__tests__/flowEngine.resolveUse.test.ts +170 -0
- package/src/models/__tests__/flowModel.getFlows.sort.test.ts +29 -5
- package/src/models/__tests__/flowModel.scheduleModelOperation.test.tsx +129 -0
- package/src/models/__tests__/flowModel.test.ts +65 -27
- package/src/models/__tests__/forkFlowModel.test.ts +40 -7
- package/src/models/flowModel.tsx +192 -30
- package/src/models/forkFlowModel.ts +11 -3
- package/src/provider.tsx +5 -5
- package/src/reactive/__tests__/observer.test.tsx +211 -0
- package/src/reactive/index.ts +11 -0
- package/src/reactive/observer.tsx +101 -0
- package/src/resources/baseRecordResource.ts +15 -3
- package/src/resources/multiRecordResource.ts +17 -8
- package/src/resources/singleRecordResource.ts +6 -3
- package/src/resources/sqlResource.ts +22 -26
- package/src/runjs-context/contexts/base.ts +47 -6
- package/src/runjs-context/snippets/global/clipboard-copy-text.snippet.ts +42 -0
- package/src/runjs-context/snippets/index.ts +3 -0
- package/src/runjs-context/snippets/scene/block/render-antd-icons.snippet.ts +46 -0
- package/src/runjs-context/snippets/scene/block/render-button-handler.snippet.ts +6 -4
- package/src/runjs-context/snippets/scene/block/render-info-card.snippet.ts +15 -16
- package/src/runjs-context/snippets/scene/block/render-react-jsx.snippet.ts +39 -0
- package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +7 -7
- package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +24 -29
- package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +20 -21
- package/src/scheduler/ModelOperationScheduler.ts +304 -0
- package/src/types.ts +50 -4
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +51 -0
- package/src/utils/__tests__/flows.test.ts +65 -0
- package/src/utils/__tests__/jsxTransform.test.ts +38 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +25 -0
- package/src/utils/__tests__/pruneFilter.test.ts +38 -0
- package/src/utils/__tests__/safeGlobals.test.ts +23 -1
- package/src/utils/__tests__/utils.test.ts +114 -15
- package/src/utils/__tests__/variablesParams.test.ts +120 -0
- package/src/utils/associationObjectVariable.ts +180 -0
- package/src/utils/createCollectionContextMeta.ts +8 -3
- package/src/utils/createEphemeralContext.ts +142 -0
- package/src/utils/flows.ts +23 -0
- package/src/utils/index.ts +11 -2
- package/src/utils/jsxTransform.ts +39 -0
- package/src/utils/params-resolvers.ts +2 -2
- package/src/utils/parsePathnameToViewParams.ts +50 -6
- package/src/utils/pruneFilter.ts +41 -0
- package/src/utils/safeGlobals.ts +51 -4
- package/src/utils/schema-utils.ts +81 -3
- package/src/utils/serverContextParams.ts +5 -0
- package/src/utils/translation.ts +7 -2
- package/src/utils/variablesParams.ts +125 -42
- package/src/views/DialogComponent.tsx +1 -4
- package/src/views/DrawerComponent.tsx +19 -7
- package/src/views/PageComponent.tsx +2 -4
- package/src/views/ViewNavigation.ts +49 -43
- package/src/views/__tests__/FlowView.usePage.test.tsx +133 -0
- package/src/views/__tests__/ViewNavigation.test.ts +54 -34
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +132 -0
- package/src/views/createViewMeta.ts +179 -42
- package/src/views/useDialog.tsx +36 -24
- package/src/views/useDrawer.tsx +37 -24
- package/src/views/usePage.tsx +46 -27
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { compileRunJs } from '../../utils/jsxTransform';
|
|
12
|
+
|
|
13
|
+
describe('compileRunJs', () => {
|
|
14
|
+
it('returns original code when no JSX is present', async () => {
|
|
15
|
+
const src = `const a = 1; const b = a + 1;`;
|
|
16
|
+
const out = await compileRunJs(src);
|
|
17
|
+
expect(out).toBe(src);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('transforms JSX when sucrase is available (skip if missing)', async () => {
|
|
21
|
+
const src = `ctx.render(<div className="x">hi</div>);`;
|
|
22
|
+
|
|
23
|
+
// Try to import sucrase to decide if this environment has it installed
|
|
24
|
+
const hasSucrase = await import('sucrase').then(() => true).catch(() => false);
|
|
25
|
+
|
|
26
|
+
const out = await compileRunJs(src);
|
|
27
|
+
|
|
28
|
+
if (!hasSucrase) {
|
|
29
|
+
// Environment without sucrase: current implementation falls back to original code
|
|
30
|
+
expect(out).toBe(src);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// sucrase available: output should contain React.createElement mapping
|
|
35
|
+
expect(out).not.toBe(src);
|
|
36
|
+
expect(out).toMatch(/ctx\.React\.createElement/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -101,4 +101,29 @@ describe('parsePathnameToViewParams', () => {
|
|
|
101
101
|
const result = parsePathnameToViewParams('///admin//xxx//tab//yyy//');
|
|
102
102
|
expect(result).toEqual([{ viewUid: 'xxx', tabUid: 'yyy' }]);
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
test('should parse filterByTk from key-value encoded segment into object', () => {
|
|
106
|
+
const kv = encodeURIComponent('id=1&tenant=ac');
|
|
107
|
+
const path = `/admin/xxx/filterbytk/${kv}`;
|
|
108
|
+
const result = parsePathnameToViewParams(path);
|
|
109
|
+
expect(result).toEqual([{ viewUid: 'xxx', filterByTk: { id: '1', tenant: 'ac' } }]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should parse filterByTk from JSON object segment', () => {
|
|
113
|
+
const json = encodeURIComponent('{"id":"1","tenant":"ac"}');
|
|
114
|
+
const path = `/admin/xxx/filterbytk/${json}`;
|
|
115
|
+
const result = parsePathnameToViewParams(path);
|
|
116
|
+
expect(result).toEqual([{ viewUid: 'xxx', filterByTk: { id: '1', tenant: 'ac' } }]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should keep non-object JSON (array/number) as string for filterByTk', () => {
|
|
120
|
+
const arr = encodeURIComponent('["a"]');
|
|
121
|
+
const num = encodeURIComponent('123');
|
|
122
|
+
const t = encodeURIComponent('true');
|
|
123
|
+
expect(parsePathnameToViewParams(`/admin/xxx/filterbytk/${arr}`)).toEqual([
|
|
124
|
+
{ viewUid: 'xxx', filterByTk: '["a"]' },
|
|
125
|
+
]);
|
|
126
|
+
expect(parsePathnameToViewParams(`/admin/xxx/filterbytk/${num}`)).toEqual([{ viewUid: 'xxx', filterByTk: '123' }]);
|
|
127
|
+
expect(parsePathnameToViewParams(`/admin/xxx/filterbytk/${t}`)).toEqual([{ viewUid: 'xxx', filterByTk: 'true' }]);
|
|
128
|
+
});
|
|
104
129
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { pruneFilter } from '../pruneFilter';
|
|
12
|
+
|
|
13
|
+
describe('pruneFilter', () => {
|
|
14
|
+
it('keeps boolean false and number 0', () => {
|
|
15
|
+
const input = { a: { $eq: false }, b: { $eq: 0 } };
|
|
16
|
+
const out = pruneFilter(input);
|
|
17
|
+
expect(out).toEqual(input);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('removes null/undefined/empty string and empty containers', () => {
|
|
21
|
+
const input = { a: null, b: undefined, c: '', d: [], e: {} } as any;
|
|
22
|
+
const out = pruneFilter(input);
|
|
23
|
+
expect(out).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('works recursively with nested objects/arrays', () => {
|
|
27
|
+
const input = {
|
|
28
|
+
$and: [{ isRead: { $eq: false } }, { name: { $eq: '' } }, {}, [], { nested: { empty: {}, ok: 1 } }],
|
|
29
|
+
} as any;
|
|
30
|
+
const out = pruneFilter(input);
|
|
31
|
+
expect(out).toEqual({ $and: [{ isRead: { $eq: false } }, { nested: { ok: 1 } }] });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns undefined for empty arrays/objects produced by pruning', () => {
|
|
35
|
+
expect(pruneFilter([null, undefined, '', [], {}] as any)).toBeUndefined();
|
|
36
|
+
expect(pruneFilter({ a: { b: '' } } as any)).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it } from 'vitest';
|
|
11
|
-
import { createSafeDocument, createSafeWindow } from '../safeGlobals';
|
|
11
|
+
import { createSafeDocument, createSafeWindow, createSafeNavigator } from '../safeGlobals';
|
|
12
12
|
|
|
13
13
|
describe('safeGlobals', () => {
|
|
14
14
|
it('createSafeWindow exposes only allowed globals and extras', () => {
|
|
@@ -16,6 +16,7 @@ describe('safeGlobals', () => {
|
|
|
16
16
|
expect(typeof win.setTimeout).toBe('function');
|
|
17
17
|
expect(win.console).toBeDefined();
|
|
18
18
|
expect(win.foo).toBe(123);
|
|
19
|
+
expect(new win.FormData()).toBeInstanceOf(window.FormData);
|
|
19
20
|
// access to location proxy is allowed, but sensitive props throw
|
|
20
21
|
expect(() => win.location.href).toThrow(/not allowed/);
|
|
21
22
|
});
|
|
@@ -26,4 +27,25 @@ describe('safeGlobals', () => {
|
|
|
26
27
|
expect(doc.bar).toBe(true);
|
|
27
28
|
expect(() => doc.cookie).toThrow(/not allowed/);
|
|
28
29
|
});
|
|
30
|
+
|
|
31
|
+
it('createSafeNavigator exposes limited props and guards others', () => {
|
|
32
|
+
const nav: any = createSafeNavigator();
|
|
33
|
+
// clipboard object should always exist
|
|
34
|
+
expect(typeof nav.clipboard).toBe('object');
|
|
35
|
+
// writeText may or may not exist depending on environment
|
|
36
|
+
if (typeof navigator !== 'undefined' && (navigator as any).clipboard?.writeText) {
|
|
37
|
+
expect(typeof nav.clipboard.writeText).toBe('function');
|
|
38
|
+
} else {
|
|
39
|
+
expect(typeof nav.clipboard.writeText === 'undefined' || typeof nav.clipboard.writeText === 'function').toBe(
|
|
40
|
+
true,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
// readable properties
|
|
44
|
+
expect(() => void nav.onLine).not.toThrow();
|
|
45
|
+
expect(() => void nav.language).not.toThrow();
|
|
46
|
+
expect(() => void nav.languages).not.toThrow();
|
|
47
|
+
// blocked properties
|
|
48
|
+
expect(() => (nav as any).geolocation).toThrow(/not allowed/);
|
|
49
|
+
expect(() => (nav as any).userAgent).toThrow(/not allowed/);
|
|
50
|
+
});
|
|
29
51
|
});
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
isInheritedFrom,
|
|
14
14
|
resolveDefaultParams,
|
|
15
15
|
resolveStepUiSchema,
|
|
16
|
+
shouldHideStepInSettings,
|
|
16
17
|
FlowExitException,
|
|
17
18
|
defineAction,
|
|
18
19
|
compileUiSchema,
|
|
@@ -30,6 +31,7 @@ import type {
|
|
|
30
31
|
StepDefinition,
|
|
31
32
|
} from '../../types';
|
|
32
33
|
import { FlowRuntimeContext } from '../../flowContext';
|
|
34
|
+
import { ContextPathProxy } from '../../ContextPathProxy';
|
|
33
35
|
|
|
34
36
|
// Helper functions
|
|
35
37
|
const createMockFlowEngine = (): FlowEngine => {
|
|
@@ -996,26 +998,123 @@ describe('Utils', () => {
|
|
|
996
998
|
|
|
997
999
|
consoleSpy.mockRestore();
|
|
998
1000
|
});
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
999
1003
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
+
// ==================== shouldHideStepInSettings() FUNCTION ====================
|
|
1005
|
+
describe('shouldHideStepInSettings()', () => {
|
|
1006
|
+
let mockFlow: any;
|
|
1007
|
+
let mockStep: StepDefinition;
|
|
1004
1008
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
+
beforeEach(() => {
|
|
1010
|
+
mockFlow = {
|
|
1011
|
+
key: 'testFlow',
|
|
1012
|
+
title: 'Test Flow',
|
|
1013
|
+
steps: {},
|
|
1014
|
+
};
|
|
1009
1015
|
|
|
1010
|
-
|
|
1016
|
+
mockStep = {
|
|
1017
|
+
key: 'testStep',
|
|
1018
|
+
handler: vi.fn(),
|
|
1019
|
+
};
|
|
1020
|
+
});
|
|
1011
1021
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1022
|
+
test('returns true when step is falsy', async () => {
|
|
1023
|
+
const result = await shouldHideStepInSettings(mockModel, mockFlow, null);
|
|
1024
|
+
expect(result).toBe(true);
|
|
1025
|
+
});
|
|
1015
1026
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1027
|
+
test('respects static step.hideInSettings=true', async () => {
|
|
1028
|
+
mockStep.hideInSettings = true;
|
|
1029
|
+
|
|
1030
|
+
const result = await shouldHideStepInSettings(mockModel, mockFlow, mockStep);
|
|
1031
|
+
expect(result).toBe(true);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
test('respects static step.hideInSettings=false', async () => {
|
|
1035
|
+
mockStep.hideInSettings = false;
|
|
1036
|
+
|
|
1037
|
+
const result = await shouldHideStepInSettings(mockModel, mockFlow, mockStep);
|
|
1038
|
+
expect(result).toBe(false);
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
test('falls back to action.hideInSettings when step.hideInSettings is undefined', async () => {
|
|
1042
|
+
const action: ActionDefinition = {
|
|
1043
|
+
name: 'testAction',
|
|
1044
|
+
handler: vi.fn(),
|
|
1045
|
+
hideInSettings: true,
|
|
1046
|
+
} as any;
|
|
1047
|
+
|
|
1048
|
+
mockStep.use = 'testAction';
|
|
1049
|
+
mockModel.flowEngine.getAction = vi.fn().mockReturnValue(action);
|
|
1050
|
+
|
|
1051
|
+
const result = await shouldHideStepInSettings(mockModel, mockFlow, mockStep);
|
|
1052
|
+
expect(mockModel.flowEngine.getAction).toHaveBeenCalledWith('testAction');
|
|
1053
|
+
expect(result).toBe(true);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
test('prefers step.hideInSettings over action.hideInSettings', async () => {
|
|
1057
|
+
const action: ActionDefinition = {
|
|
1058
|
+
name: 'testAction',
|
|
1059
|
+
handler: vi.fn(),
|
|
1060
|
+
hideInSettings: true,
|
|
1061
|
+
} as any;
|
|
1062
|
+
|
|
1063
|
+
mockStep.use = 'testAction';
|
|
1064
|
+
mockStep.hideInSettings = false;
|
|
1065
|
+
mockModel.flowEngine.getAction = vi.fn().mockReturnValue(action);
|
|
1066
|
+
|
|
1067
|
+
const result = await shouldHideStepInSettings(mockModel, mockFlow, mockStep);
|
|
1068
|
+
expect(result).toBe(false);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
test('evaluates function step.hideInSettings with FlowRuntimeContext', async () => {
|
|
1072
|
+
const hideFn = vi.fn().mockResolvedValue(true);
|
|
1073
|
+
mockStep.hideInSettings = hideFn as any;
|
|
1074
|
+
|
|
1075
|
+
const result = await shouldHideStepInSettings(mockModel, mockFlow, mockStep);
|
|
1076
|
+
|
|
1077
|
+
expect(hideFn).toHaveBeenCalledTimes(1);
|
|
1078
|
+
const ctx = hideFn.mock.calls[0][0] as FlowRuntimeContext;
|
|
1079
|
+
expect(ctx).toBeInstanceOf(FlowRuntimeContext);
|
|
1080
|
+
expect((ctx as any).model).toBe(mockModel);
|
|
1081
|
+
expect((ctx as any).flowKey).toBe('testFlow');
|
|
1082
|
+
expect((ctx as any).mode).toBe('settings');
|
|
1083
|
+
expect((ctx as any).currentStep).toBeInstanceOf(ContextPathProxy);
|
|
1084
|
+
expect(String((ctx as any).currentStep)).toBe('{{ctx.currentStep}}');
|
|
1085
|
+
expect(result).toBe(true);
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
test('evaluates function action.hideInSettings when step.hideInSettings is undefined', async () => {
|
|
1089
|
+
const hideFn = vi.fn().mockResolvedValue(false);
|
|
1090
|
+
const action: ActionDefinition = {
|
|
1091
|
+
name: 'testAction',
|
|
1092
|
+
handler: vi.fn(),
|
|
1093
|
+
hideInSettings: hideFn as any,
|
|
1094
|
+
} as any;
|
|
1095
|
+
|
|
1096
|
+
mockStep.use = 'testAction';
|
|
1097
|
+
mockModel.flowEngine.getAction = vi.fn().mockReturnValue(action);
|
|
1098
|
+
|
|
1099
|
+
const result = await shouldHideStepInSettings(mockModel, mockFlow, mockStep);
|
|
1100
|
+
|
|
1101
|
+
expect(hideFn).toHaveBeenCalledTimes(1);
|
|
1102
|
+
const ctx = hideFn.mock.calls[0][0] as FlowRuntimeContext;
|
|
1103
|
+
expect((ctx as any).currentStep).toBeInstanceOf(ContextPathProxy);
|
|
1104
|
+
expect(String((ctx as any).currentStep)).toBe('{{ctx.currentStep}}');
|
|
1105
|
+
expect(result).toBe(false);
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
test('returns false and logs warning when function hideInSettings throws', async () => {
|
|
1109
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1110
|
+
const hideFn = vi.fn().mockRejectedValue(new Error('boom'));
|
|
1111
|
+
mockStep.hideInSettings = hideFn as any;
|
|
1112
|
+
|
|
1113
|
+
const result = await shouldHideStepInSettings(mockModel, mockFlow, mockStep);
|
|
1114
|
+
|
|
1115
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
1116
|
+
expect(result).toBe(false);
|
|
1117
|
+
consoleSpy.mockRestore();
|
|
1019
1118
|
});
|
|
1020
1119
|
});
|
|
1021
1120
|
});
|
|
@@ -11,9 +11,11 @@ import { describe, expect, it } from 'vitest';
|
|
|
11
11
|
import {
|
|
12
12
|
collectContextParamsForTemplate,
|
|
13
13
|
createRecordMetaFactory,
|
|
14
|
+
createRecordResolveOnServerWithLocal,
|
|
14
15
|
inferParentRecordRef,
|
|
15
16
|
inferRecordRef,
|
|
16
17
|
} from '../variablesParams';
|
|
18
|
+
import { FlowEngine, SingleRecordResource } from '../..';
|
|
17
19
|
|
|
18
20
|
describe('variablesParams helpers', () => {
|
|
19
21
|
it('inferRecordRef and inferParentRecordRef from FlowContext-like object', () => {
|
|
@@ -30,6 +32,29 @@ describe('variablesParams helpers', () => {
|
|
|
30
32
|
expect(inferParentRecordRef(ctx)).toEqual({ collection: 'posts', dataSourceKey: 'main', filterByTk: 9 });
|
|
31
33
|
});
|
|
32
34
|
|
|
35
|
+
it('inferRecordRef fallback to collection.getFilterByTK when resource has no filterByTk', () => {
|
|
36
|
+
const engine = new FlowEngine();
|
|
37
|
+
const ds = engine.context.dataSourceManager.getDataSource('main')!;
|
|
38
|
+
ds.addCollection({
|
|
39
|
+
name: 'users',
|
|
40
|
+
filterTargetKey: 'id',
|
|
41
|
+
fields: [
|
|
42
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
43
|
+
{ name: 'roles', type: 'belongsToMany', target: 'roles', interface: 'm2m' },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
const collection = ds.getCollection('users');
|
|
47
|
+
const resource = engine.context.createResource(SingleRecordResource);
|
|
48
|
+
resource.setResourceName('main.users');
|
|
49
|
+
// 不设置 filterByTk,模拟资源无 filter 时的 fallback
|
|
50
|
+
const ctx: any = engine.context;
|
|
51
|
+
ctx.defineProperty('resource', { value: resource });
|
|
52
|
+
ctx.defineProperty('collection', { value: collection });
|
|
53
|
+
ctx.defineProperty('record', { value: { id: 5 } });
|
|
54
|
+
|
|
55
|
+
expect(inferRecordRef(ctx)).toEqual({ collection: 'users', dataSourceKey: 'main', filterByTk: 5 });
|
|
56
|
+
});
|
|
57
|
+
|
|
33
58
|
it('collectContextParamsForTemplate builds input for used variables only', async () => {
|
|
34
59
|
const ctx: any = {
|
|
35
60
|
getPropertyOptions: (k: string) => ({
|
|
@@ -49,4 +74,99 @@ describe('variablesParams helpers', () => {
|
|
|
49
74
|
expect(res).toHaveProperty('record');
|
|
50
75
|
expect(res).not.toHaveProperty('user');
|
|
51
76
|
});
|
|
77
|
+
|
|
78
|
+
it('collectContextParamsForTemplate keeps associationName/sourceId from RecordRef', async () => {
|
|
79
|
+
const ctx: any = {
|
|
80
|
+
getPropertyOptions: (k: string) => ({
|
|
81
|
+
meta:
|
|
82
|
+
k === 'popup'
|
|
83
|
+
? async () => ({
|
|
84
|
+
type: 'object',
|
|
85
|
+
title: 'Popup',
|
|
86
|
+
buildVariablesParams: () => ({
|
|
87
|
+
record: {
|
|
88
|
+
collection: 'posts',
|
|
89
|
+
dataSourceKey: 'main',
|
|
90
|
+
filterByTk: 1,
|
|
91
|
+
associationName: 'users.posts',
|
|
92
|
+
sourceId: 9,
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
})
|
|
96
|
+
: undefined,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const tpl = { value: '{{ ctx.popup.record.id }}' } as any;
|
|
101
|
+
const res = await collectContextParamsForTemplate(ctx, tpl);
|
|
102
|
+
expect(res?.['popup.record']).toMatchObject({
|
|
103
|
+
collection: 'posts',
|
|
104
|
+
dataSourceKey: 'main',
|
|
105
|
+
filterByTk: 1,
|
|
106
|
+
associationName: 'users.posts',
|
|
107
|
+
sourceId: 9,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('createRecordResolveOnServerWithLocal: no local record => always use server', () => {
|
|
112
|
+
const resolver = createRecordResolveOnServerWithLocal(
|
|
113
|
+
() => ({ name: 'posts', dataSourceKey: 'main' }) as any,
|
|
114
|
+
() => undefined,
|
|
115
|
+
);
|
|
116
|
+
expect(resolver('')).toBe(true);
|
|
117
|
+
expect(resolver('title')).toBe(true);
|
|
118
|
+
expect(resolver('author.name')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('createRecordResolveOnServerWithLocal: local record + non-association subpaths => use local', () => {
|
|
122
|
+
const collection: any = {
|
|
123
|
+
getField: (name: string) => {
|
|
124
|
+
if (name === 'id') return { name: 'id', isAssociationField: () => false };
|
|
125
|
+
if (name === 'title') return { name: 'title', isAssociationField: () => false };
|
|
126
|
+
return undefined;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const record = { id: 1, title: 'Hello' };
|
|
130
|
+
const resolver = createRecordResolveOnServerWithLocal(
|
|
131
|
+
() => collection,
|
|
132
|
+
() => record,
|
|
133
|
+
);
|
|
134
|
+
expect(resolver('')).toBe(false);
|
|
135
|
+
expect(resolver('id')).toBe(false);
|
|
136
|
+
expect(resolver('title')).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('createRecordResolveOnServerWithLocal: association with local value => still use local', () => {
|
|
140
|
+
const collection: any = {
|
|
141
|
+
getField: (name: string) => {
|
|
142
|
+
if (name === 'author') return { name: 'author', isAssociationField: () => true };
|
|
143
|
+
if (name === 'title') return { name: 'title', isAssociationField: () => false };
|
|
144
|
+
return undefined;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const record = { title: 'Hello', author: { id: 1, name: 'Alice' } };
|
|
148
|
+
const resolver = createRecordResolveOnServerWithLocal(
|
|
149
|
+
() => collection,
|
|
150
|
+
() => record,
|
|
151
|
+
);
|
|
152
|
+
expect(resolver('author')).toBe(false);
|
|
153
|
+
expect(resolver('author.name')).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('createRecordResolveOnServerWithLocal: association without local value => use server', () => {
|
|
157
|
+
const collection: any = {
|
|
158
|
+
getField: (name: string) => {
|
|
159
|
+
if (name === 'author') return { name: 'author', isAssociationField: () => true };
|
|
160
|
+
if (name === 'title') return { name: 'title', isAssociationField: () => false };
|
|
161
|
+
return undefined;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
const record = { title: 'Hello', author: null };
|
|
165
|
+
const resolver = createRecordResolveOnServerWithLocal(
|
|
166
|
+
() => collection,
|
|
167
|
+
() => record,
|
|
168
|
+
);
|
|
169
|
+
expect(resolver('author')).toBe(true);
|
|
170
|
+
expect(resolver('author.name')).toBe(true);
|
|
171
|
+
});
|
|
52
172
|
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Collection, CollectionField } from '../data-source';
|
|
11
|
+
import _ from 'lodash';
|
|
12
|
+
import type { FlowContext, PropertyMeta, PropertyMetaFactory } from '../flowContext';
|
|
13
|
+
import { createCollectionContextMeta } from './createCollectionContextMeta';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 提取变量子路径的顶层字段名。
|
|
17
|
+
* 例如:
|
|
18
|
+
* - 'author.name' => 'author'
|
|
19
|
+
* - 'tags[0].name' => 'tags'
|
|
20
|
+
*
|
|
21
|
+
* @param subPath 变量在对象中的子路径字符串
|
|
22
|
+
* @returns 顶层字段名,找不到时返回 undefined
|
|
23
|
+
*/
|
|
24
|
+
function baseFieldNameOf(subPath: string): string | undefined {
|
|
25
|
+
if (!subPath) return undefined;
|
|
26
|
+
const m = subPath.match(/^([^.[]+)/);
|
|
27
|
+
return m?.[1];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 在集合中根据字段名查找字段定义,兼容 getField/getFields 两种方式。
|
|
32
|
+
*
|
|
33
|
+
* @param collection 集合对象
|
|
34
|
+
* @param name 字段名
|
|
35
|
+
* @returns 匹配的字段定义,找不到时返回 undefined
|
|
36
|
+
*/
|
|
37
|
+
function findFieldByName(collection: Collection | null | undefined, name?: string): CollectionField | undefined {
|
|
38
|
+
if (!collection || !name) return undefined;
|
|
39
|
+
const direct = collection.getField(name);
|
|
40
|
+
if (direct) return direct;
|
|
41
|
+
const fields = collection.getFields?.() || [];
|
|
42
|
+
return fields.find((f) => f.name === name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 从值中提取主键:
|
|
47
|
+
* - 支持主键原始值(string/number)
|
|
48
|
+
* - 支持对象(按主键名取值)
|
|
49
|
+
*
|
|
50
|
+
* @param value 字段当前值
|
|
51
|
+
* @param primaryKey 主键字段名
|
|
52
|
+
* @returns 解析出的主键值,无法解析时返回 undefined
|
|
53
|
+
*/
|
|
54
|
+
function toFilterByTk(value: unknown, primaryKey: string | string[]) {
|
|
55
|
+
if (value == null) return undefined;
|
|
56
|
+
if (Array.isArray(primaryKey)) {
|
|
57
|
+
if (typeof value !== 'object' || !value) return undefined;
|
|
58
|
+
const out: Record<string, any> = {};
|
|
59
|
+
for (const k of primaryKey) {
|
|
60
|
+
const v = (value as any)[k];
|
|
61
|
+
if (typeof v === 'undefined' || v === null) return undefined;
|
|
62
|
+
out[k] = v;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === 'string' || typeof value === 'number') return value;
|
|
67
|
+
if (typeof value === 'object') {
|
|
68
|
+
return (value as any)[primaryKey];
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 创建一个用于“对象类变量”(如 formValues / currentObject)的 `resolveOnServer` 判定函数。
|
|
75
|
+
* 仅当访问路径以“关联字段名”开头(且继续访问其子属性)时,返回 true 交由服务端解析;
|
|
76
|
+
* 否则在前端解析即可。
|
|
77
|
+
*
|
|
78
|
+
* @param collectionAccessor 返回当前对象所在 collection
|
|
79
|
+
* @param valueAccessor 可选,本地值访问器。若本地已存在目标子路径的值,则认为无需走后端,优先使用本地值。
|
|
80
|
+
* @returns `(subPath) => boolean` 判断是否需要服务端解析
|
|
81
|
+
*/
|
|
82
|
+
export function createAssociationSubpathResolver(
|
|
83
|
+
collectionAccessor: () => Collection | null,
|
|
84
|
+
valueAccessor?: () => unknown,
|
|
85
|
+
): (subPath: string) => boolean {
|
|
86
|
+
return (p: string) => {
|
|
87
|
+
// 仅在访问子属性时才考虑后端
|
|
88
|
+
if (!p || !p.includes('.')) return false;
|
|
89
|
+
const base = baseFieldNameOf(p);
|
|
90
|
+
if (!base) return false;
|
|
91
|
+
const collection = collectionAccessor();
|
|
92
|
+
const field = findFieldByName(collection, base);
|
|
93
|
+
const isAssoc = !!field?.isAssociationField();
|
|
94
|
+
if (!isAssoc) return false;
|
|
95
|
+
|
|
96
|
+
// 可选:本地优先。当提供 valueAccessor 时,若本地已有该子路径值,则不走后端
|
|
97
|
+
if (typeof valueAccessor === 'function') {
|
|
98
|
+
const local = valueAccessor();
|
|
99
|
+
if (local && typeof local === 'object') {
|
|
100
|
+
const v = _.get(local as Record<string, unknown>, p);
|
|
101
|
+
if (typeof v !== 'undefined') return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return true;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 构建“对象类变量”的 PropertyMetaFactory:
|
|
111
|
+
* - 暴露集合字段结构(通过 createCollectionContextMeta)用于变量选择器;
|
|
112
|
+
* - 提供 buildVariablesParams:基于对象当前值,收集所有“已选择的关联字段”
|
|
113
|
+
* 以便服务端在 variables:resolve 时按需补全关联数据。
|
|
114
|
+
*
|
|
115
|
+
* @param collectionAccessor 获取集合对象,用于字段/元信息来源
|
|
116
|
+
* @param title 变量组标题(用于 UI 展示)
|
|
117
|
+
* @param valueAccessor 获取当前对象值(如 ctx.form.getFieldsValue() / ctx.currentObject)
|
|
118
|
+
* @returns PropertyMetaFactory
|
|
119
|
+
*/
|
|
120
|
+
export function createAssociationAwareObjectMetaFactory(
|
|
121
|
+
collectionAccessor: () => Collection | null,
|
|
122
|
+
title: string,
|
|
123
|
+
valueAccessor: (ctx: FlowContext) => any,
|
|
124
|
+
): PropertyMetaFactory {
|
|
125
|
+
const baseFactory = createCollectionContextMeta(collectionAccessor, title, true);
|
|
126
|
+
const factory: PropertyMetaFactory = async () => {
|
|
127
|
+
const base = (await baseFactory()) as PropertyMeta | null;
|
|
128
|
+
if (!base) return null;
|
|
129
|
+
|
|
130
|
+
const meta: PropertyMeta = {
|
|
131
|
+
...base,
|
|
132
|
+
buildVariablesParams: (ctx: FlowContext) => {
|
|
133
|
+
const collection = collectionAccessor();
|
|
134
|
+
const obj = valueAccessor(ctx);
|
|
135
|
+
if (!collection || !obj || typeof obj !== 'object') return {};
|
|
136
|
+
const params: Record<string, any> = {};
|
|
137
|
+
|
|
138
|
+
const fields: CollectionField[] = collection.getFields?.() || [];
|
|
139
|
+
for (const field of fields) {
|
|
140
|
+
const name = field.name as string | undefined;
|
|
141
|
+
if (!name) continue;
|
|
142
|
+
if (!field.isAssociationField()) continue;
|
|
143
|
+
const target = field.target as string | undefined;
|
|
144
|
+
const targetCollection = field.targetCollection;
|
|
145
|
+
if (!target || !targetCollection) continue;
|
|
146
|
+
const primaryKey = targetCollection.filterTargetKey as string | string[];
|
|
147
|
+
|
|
148
|
+
const associationValue = (obj as any)[name];
|
|
149
|
+
if (associationValue == null) continue;
|
|
150
|
+
|
|
151
|
+
if (Array.isArray(associationValue)) {
|
|
152
|
+
const ids = associationValue.map((item) => toFilterByTk(item, primaryKey)).filter((v) => v != null);
|
|
153
|
+
if (ids.length) {
|
|
154
|
+
params[name] = {
|
|
155
|
+
collection: target,
|
|
156
|
+
dataSourceKey: targetCollection.dataSourceKey,
|
|
157
|
+
filterByTk: ids,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
const id = toFilterByTk(associationValue, primaryKey);
|
|
162
|
+
if (id != null) {
|
|
163
|
+
params[name] = {
|
|
164
|
+
collection: target,
|
|
165
|
+
dataSourceKey: targetCollection.dataSourceKey,
|
|
166
|
+
filterByTk: id,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return params;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return meta;
|
|
177
|
+
};
|
|
178
|
+
factory.title = title;
|
|
179
|
+
return factory;
|
|
180
|
+
}
|
|
@@ -17,7 +17,7 @@ const NUMERIC_FIELD_TYPES = ['integer', 'float', 'double', 'decimal'] as const;
|
|
|
17
17
|
/**
|
|
18
18
|
* 创建字段的完整元数据(统一处理关联和非关联字段)
|
|
19
19
|
*/
|
|
20
|
-
function createFieldMetadata(field: CollectionField) {
|
|
20
|
+
function createFieldMetadata(field: CollectionField, includeNonFilterable?: boolean) {
|
|
21
21
|
const baseProperties = createMetaBaseProperties(field);
|
|
22
22
|
|
|
23
23
|
if (field.isAssociationField()) {
|
|
@@ -36,7 +36,9 @@ function createFieldMetadata(field: CollectionField) {
|
|
|
36
36
|
properties: async () => {
|
|
37
37
|
const subProperties: Record<string, any> = {};
|
|
38
38
|
targetCollection.fields.forEach((subField) => {
|
|
39
|
-
|
|
39
|
+
if (includeNonFilterable || subField.filterable) {
|
|
40
|
+
subProperties[subField.name] = createFieldMetadata(subField, includeNonFilterable);
|
|
41
|
+
}
|
|
40
42
|
});
|
|
41
43
|
return subProperties;
|
|
42
44
|
},
|
|
@@ -93,6 +95,7 @@ function createMetaBaseProperties(field: CollectionField) {
|
|
|
93
95
|
export function createCollectionContextMeta(
|
|
94
96
|
collectionOrFactory: Collection | (() => Collection | null),
|
|
95
97
|
title?: string,
|
|
98
|
+
includeNonFilterable?: boolean,
|
|
96
99
|
): PropertyMetaFactory {
|
|
97
100
|
const metaFn: PropertyMetaFactory = async () => {
|
|
98
101
|
const collection = typeof collectionOrFactory === 'function' ? collectionOrFactory() : collectionOrFactory;
|
|
@@ -110,7 +113,9 @@ export function createCollectionContextMeta(
|
|
|
110
113
|
|
|
111
114
|
// 添加所有字段
|
|
112
115
|
collection.fields.forEach((field) => {
|
|
113
|
-
|
|
116
|
+
if (includeNonFilterable || field.filterable) {
|
|
117
|
+
properties[field.name] = createFieldMetadata(field, includeNonFilterable);
|
|
118
|
+
}
|
|
114
119
|
});
|
|
115
120
|
|
|
116
121
|
return properties;
|