@nocobase/client-v2 2.1.37 → 2.1.39
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/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/index.mjs +3 -3
- package/lib/index.js +67 -67
- package/package.json +7 -7
- package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +37 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +29 -5
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.39",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.1.
|
|
31
|
-
"@nocobase/flow-engine": "2.1.
|
|
32
|
-
"@nocobase/sdk": "2.1.
|
|
33
|
-
"@nocobase/shared": "2.1.
|
|
34
|
-
"@nocobase/utils": "2.1.
|
|
30
|
+
"@nocobase/evaluators": "2.1.39",
|
|
31
|
+
"@nocobase/flow-engine": "2.1.39",
|
|
32
|
+
"@nocobase/sdk": "2.1.39",
|
|
33
|
+
"@nocobase/shared": "2.1.39",
|
|
34
|
+
"@nocobase/utils": "2.1.39",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"react-i18next": "^11.15.1",
|
|
49
49
|
"react-router-dom": "^6.30.1"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "ddbf88ac2a5de1ad94ff4779618cdf1b81aae037"
|
|
52
52
|
}
|
|
@@ -72,17 +72,64 @@ const flowKeepMobileHorizontalClassName = css`
|
|
|
72
72
|
function isGridDelegatedStep(flowKey: string, stepKey: string): boolean {
|
|
73
73
|
return !!GRID_DELEGATED_STEP_KEYS[flowKey]?.has(stepKey);
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
interface FormModelWithAvailableData {
|
|
77
|
+
hasAvailableData(): boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface FormModelWithSubmit {
|
|
81
|
+
submit(): Promise<unknown>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasAvailableDataCapability(model: object): model is FormModelWithAvailableData {
|
|
85
|
+
return 'hasAvailableData' in model && typeof model.hasAvailableData === 'function';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasSubmitCapability(model: object): model is FormModelWithSubmit {
|
|
89
|
+
return 'submit' in model && typeof model.submit === 'function';
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
export class FormBlockModel<
|
|
76
93
|
T extends DefaultCollectionBlockModelStructure = DefaultCollectionBlockModelStructure,
|
|
77
94
|
> extends CollectionBlockModel<T> {
|
|
78
95
|
formValueRuntime?: FormValueRuntime;
|
|
79
96
|
|
|
97
|
+
serialize() {
|
|
98
|
+
return { ...super.serialize(), variableContractType: { type: 'form', use: this.use } };
|
|
99
|
+
}
|
|
100
|
+
|
|
80
101
|
private userModifiedTopLevelFields = new Set<string>();
|
|
81
102
|
|
|
82
103
|
get form() {
|
|
83
104
|
return this.context.form as FormInstance;
|
|
84
105
|
}
|
|
85
106
|
|
|
107
|
+
submitFromRunJs() {
|
|
108
|
+
if (hasAvailableDataCapability(this) && !this.hasAvailableData()) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const submitAction = this.mapSubModels('actions', (action) => action).find((action) =>
|
|
113
|
+
action.getFlow('submitSettings'),
|
|
114
|
+
);
|
|
115
|
+
if (submitAction) {
|
|
116
|
+
return submitAction.onClick(undefined);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const isCoreForm = ['CreateFormModel', 'EditFormModel'].some((modelName) => {
|
|
120
|
+
const ModelClass = this.flowEngine.getModelClass(modelName);
|
|
121
|
+
return ModelClass && this instanceof ModelClass;
|
|
122
|
+
});
|
|
123
|
+
if (this.context.publicFormRuntime || !isCoreForm || !hasSubmitCapability(this)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.submit().catch((error: unknown) => {
|
|
128
|
+
this.context.message.error(this.context.t('Save failed'));
|
|
129
|
+
console.error('Form submission error:', error);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
86
133
|
_defaultCustomModelClasses = {
|
|
87
134
|
FormActionGroupModel: 'FormActionGroupModel',
|
|
88
135
|
FormItemModel: 'FormItemModel',
|
|
@@ -21,6 +21,10 @@ export type DefaultFormGridStructure = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export class FormGridModel<T extends DefaultFormGridStructure = DefaultFormGridStructure> extends GridModel<T> {
|
|
24
|
+
serialize() {
|
|
25
|
+
return { ...super.serialize(), variableContractType: { type: 'formGrid', use: this.use } };
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
itemFallback = (<Skeleton.Input block size="small" style={{ marginBottom: '0.5rem' }} />);
|
|
25
29
|
itemSettingsMenuLevel = 2;
|
|
26
30
|
itemFlowSettings = {
|
|
@@ -0,0 +1,131 @@
|
|
|
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 { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { CreateFormModel } from '../CreateFormModel';
|
|
13
|
+
import { EditFormModel } from '../EditFormModel';
|
|
14
|
+
import { FormActionModel } from '../FormActionModel';
|
|
15
|
+
|
|
16
|
+
function prepareFormModel<T extends CreateFormModel | EditFormModel>(model: T, publicFormRuntime = false) {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
engine.registerModels({ CreateFormModel, EditFormModel });
|
|
19
|
+
model.flowEngine = engine;
|
|
20
|
+
Object.defineProperty(model, 'context', {
|
|
21
|
+
value: {
|
|
22
|
+
publicFormRuntime,
|
|
23
|
+
message: { error: vi.fn() },
|
|
24
|
+
t: (value: string) => value,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return model;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createSubmitAction() {
|
|
31
|
+
const onClick = vi.fn();
|
|
32
|
+
const action = {
|
|
33
|
+
getFlow: vi.fn((key: string) => (key === 'submitSettings' ? {} : undefined)),
|
|
34
|
+
onClick,
|
|
35
|
+
} as unknown as FormActionModel;
|
|
36
|
+
return { action, onClick };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('FormBlockModel.submitFromRunJs', () => {
|
|
40
|
+
it('dispatches the first action with a submitSettings flow', () => {
|
|
41
|
+
const { action, onClick } = createSubmitAction();
|
|
42
|
+
const { action: secondAction, onClick: secondOnClick } = createSubmitAction();
|
|
43
|
+
const blockModel = prepareFormModel(
|
|
44
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
45
|
+
mapSubModels: (_key: string, callback: (item: FormActionModel) => FormActionModel) => [
|
|
46
|
+
callback(action),
|
|
47
|
+
callback(secondAction),
|
|
48
|
+
],
|
|
49
|
+
}) as CreateFormModel,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
blockModel.submitFromRunJs();
|
|
53
|
+
|
|
54
|
+
expect(onClick).toHaveBeenCalledWith(undefined);
|
|
55
|
+
expect(secondOnClick).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not dispatch an edit submit action without an available record', () => {
|
|
59
|
+
const { action, onClick } = createSubmitAction();
|
|
60
|
+
const blockModel = prepareFormModel(
|
|
61
|
+
Object.assign(Object.create(EditFormModel.prototype), {
|
|
62
|
+
hasAvailableData: () => false,
|
|
63
|
+
mapSubModels: (_key: string, callback: (item: FormActionModel) => FormActionModel) => [callback(action)],
|
|
64
|
+
}) as EditFormModel,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
blockModel.submitFromRunJs();
|
|
68
|
+
|
|
69
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('falls back to the core form submit handler when no submit action exists', () => {
|
|
73
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
74
|
+
const blockModel = prepareFormModel(
|
|
75
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
76
|
+
mapSubModels: vi.fn(() => []),
|
|
77
|
+
submit,
|
|
78
|
+
}) as CreateFormModel,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
blockModel.submitFromRunJs();
|
|
82
|
+
|
|
83
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('falls back to the core edit submit handler when a record exists', () => {
|
|
87
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
88
|
+
const blockModel = prepareFormModel(
|
|
89
|
+
Object.assign(Object.create(EditFormModel.prototype), {
|
|
90
|
+
hasAvailableData: () => true,
|
|
91
|
+
mapSubModels: vi.fn(() => []),
|
|
92
|
+
submit,
|
|
93
|
+
}) as EditFormModel,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
blockModel.submitFromRunJs();
|
|
97
|
+
|
|
98
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('uses the core fallback for specialized form subclasses', () => {
|
|
102
|
+
class SpecializedCreateFormModel extends CreateFormModel {}
|
|
103
|
+
|
|
104
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
105
|
+
const blockModel = prepareFormModel(
|
|
106
|
+
Object.assign(Object.create(SpecializedCreateFormModel.prototype), {
|
|
107
|
+
mapSubModels: vi.fn(() => []),
|
|
108
|
+
submit,
|
|
109
|
+
}) as SpecializedCreateFormModel,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
blockModel.submitFromRunJs();
|
|
113
|
+
|
|
114
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not use the core fallback for public forms without a submit action', () => {
|
|
118
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
119
|
+
const blockModel = prepareFormModel(
|
|
120
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
121
|
+
mapSubModels: vi.fn(() => []),
|
|
122
|
+
submit,
|
|
123
|
+
}) as CreateFormModel,
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
blockModel.submitFromRunJs();
|
|
128
|
+
|
|
129
|
+
expect(submit).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -445,6 +445,43 @@ describe('FormValueRuntime (default rules)', () => {
|
|
|
445
445
|
});
|
|
446
446
|
|
|
447
447
|
describe('FormValueRuntime (form assign rules)', () => {
|
|
448
|
+
it('uses the form grid as the variable contract owner', async () => {
|
|
449
|
+
const engineEmitter = new EventEmitter();
|
|
450
|
+
const formStub = createFormStub({ b: 'licensed' });
|
|
451
|
+
const onResolveJsonTemplate = vi.fn();
|
|
452
|
+
const blockModel: any = {
|
|
453
|
+
uid: 'form-contract-owner',
|
|
454
|
+
subModels: { grid: { uid: 'form-contract-owner-grid' } },
|
|
455
|
+
flowEngine: { emitter: engineEmitter },
|
|
456
|
+
emitter: new EventEmitter(),
|
|
457
|
+
dispatchEvent: vi.fn(),
|
|
458
|
+
getAclActionName: () => 'create',
|
|
459
|
+
};
|
|
460
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
461
|
+
const blockCtx = createFieldContext(runtime);
|
|
462
|
+
const resolveJsonTemplate = blockCtx.resolveJsonTemplate.bind(blockCtx);
|
|
463
|
+
blockCtx.defineMethod('resolveJsonTemplate', async (template: unknown, options?: unknown) => {
|
|
464
|
+
onResolveJsonTemplate(options);
|
|
465
|
+
return resolveJsonTemplate(template, options);
|
|
466
|
+
});
|
|
467
|
+
blockModel.context = blockCtx;
|
|
468
|
+
runtime.mount({ sync: true });
|
|
469
|
+
|
|
470
|
+
runtime.syncAssignRules([
|
|
471
|
+
{
|
|
472
|
+
key: 'license-version',
|
|
473
|
+
enable: true,
|
|
474
|
+
targetPath: 'a',
|
|
475
|
+
mode: 'assign',
|
|
476
|
+
condition: { logic: '$and', items: [] },
|
|
477
|
+
value: '__B__',
|
|
478
|
+
},
|
|
479
|
+
]);
|
|
480
|
+
|
|
481
|
+
await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('licensed'));
|
|
482
|
+
expect(onResolveJsonTemplate).toHaveBeenCalledWith({ contractModelUid: 'form-contract-owner-grid' });
|
|
483
|
+
});
|
|
484
|
+
|
|
448
485
|
it('migrates block-level rule to field instance on mount and restores on unmount', async () => {
|
|
449
486
|
const engineEmitter = new EventEmitter();
|
|
450
487
|
const blockEmitter = new EventEmitter();
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { isObservable, reaction, toJS } from '@formily/reactive';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
FlowContext,
|
|
13
|
+
FlowModel,
|
|
14
|
+
isRunJSValue,
|
|
15
|
+
normalizeRunJSValue,
|
|
16
|
+
runjsWithSafeGlobals,
|
|
17
|
+
type ResolveJsonTemplateOptions,
|
|
18
|
+
} from '@nocobase/flow-engine';
|
|
12
19
|
import { getValuesByPath } from '@nocobase/shared';
|
|
13
20
|
import _ from 'lodash';
|
|
14
21
|
import { dayjs } from '@nocobase/utils/client';
|
|
@@ -49,6 +56,7 @@ type RuntimeRule = {
|
|
|
49
56
|
getValue: () => any;
|
|
50
57
|
getCondition?: () => any;
|
|
51
58
|
getContext: () => any;
|
|
59
|
+
getContractModelUid?: () => string | undefined;
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
type ObservableBinding = {
|
|
@@ -94,6 +102,7 @@ type RuntimeItemChain = {
|
|
|
94
102
|
|
|
95
103
|
export type RuleEngineOptions = {
|
|
96
104
|
getBlockModelUid: () => string;
|
|
105
|
+
getAssignRulesModelUid: () => string | undefined;
|
|
97
106
|
getActionName: () => string | undefined;
|
|
98
107
|
getBlockContext: () => any;
|
|
99
108
|
getEngine: () => any;
|
|
@@ -217,6 +226,7 @@ export class RuleEngine {
|
|
|
217
226
|
getValue: () => template?.value,
|
|
218
227
|
getCondition: () => template?.condition,
|
|
219
228
|
getContext: () => this.options.getBlockContext(),
|
|
229
|
+
getContractModelUid: this.options.getAssignRulesModelUid,
|
|
220
230
|
};
|
|
221
231
|
|
|
222
232
|
this.rules.set(id, { rule, state: { deps: new Set(), depDisposers: [], runSeq: 0, scheduledAtWriteSeq: 0 } });
|
|
@@ -554,6 +564,7 @@ export class RuleEngine {
|
|
|
554
564
|
getValue: () => template?.value,
|
|
555
565
|
getCondition: () => template?.condition,
|
|
556
566
|
getContext: () => this.options.getBlockContext(),
|
|
567
|
+
getContractModelUid: this.options.getAssignRulesModelUid,
|
|
557
568
|
};
|
|
558
569
|
|
|
559
570
|
this.rules.set(id, { rule, state: { deps: new Set(), depDisposers: [], runSeq: 0, scheduledAtWriteSeq: 0 } });
|
|
@@ -922,6 +933,7 @@ export class RuleEngine {
|
|
|
922
933
|
getValue: () => template?.value,
|
|
923
934
|
getCondition: () => template?.condition,
|
|
924
935
|
getContext: () => model?.context,
|
|
936
|
+
getContractModelUid: this.options.getAssignRulesModelUid,
|
|
925
937
|
};
|
|
926
938
|
|
|
927
939
|
this.rules.set(id, { rule, state: { deps: new Set(), depDisposers: [], runSeq: 0, scheduledAtWriteSeq: 0 } });
|
|
@@ -1671,7 +1683,7 @@ export class RuleEngine {
|
|
|
1671
1683
|
}
|
|
1672
1684
|
}
|
|
1673
1685
|
|
|
1674
|
-
const evalCtx = this.createRuleEvaluationContext(baseCtx, collector, targetNamePath);
|
|
1686
|
+
const evalCtx = this.createRuleEvaluationContext(baseCtx, collector, targetNamePath, rule.getContractModelUid?.());
|
|
1675
1687
|
return { collector, evalCtx, rawValue, isRunJS };
|
|
1676
1688
|
}
|
|
1677
1689
|
|
|
@@ -2010,7 +2022,12 @@ export class RuleEngine {
|
|
|
2010
2022
|
return canOverwrite;
|
|
2011
2023
|
}
|
|
2012
2024
|
|
|
2013
|
-
private createRuleEvaluationContext(
|
|
2025
|
+
private createRuleEvaluationContext(
|
|
2026
|
+
baseCtx: any,
|
|
2027
|
+
collector: DepCollector,
|
|
2028
|
+
targetNamePath: NamePath | null,
|
|
2029
|
+
contractModelUid?: string,
|
|
2030
|
+
) {
|
|
2014
2031
|
const trackingFormValues = this.options.createTrackingFormValues(collector);
|
|
2015
2032
|
const ctx: any = new FlowContext();
|
|
2016
2033
|
try {
|
|
@@ -2030,12 +2047,19 @@ export class RuleEngine {
|
|
|
2030
2047
|
}
|
|
2031
2048
|
|
|
2032
2049
|
const delegatedResolveJsonTemplate =
|
|
2033
|
-
typeof ctx.resolveJsonTemplate === 'function'
|
|
2050
|
+
typeof ctx.resolveJsonTemplate === 'function'
|
|
2051
|
+
? (ctx.resolveJsonTemplate.bind(ctx) as (
|
|
2052
|
+
template: unknown,
|
|
2053
|
+
options?: ResolveJsonTemplateOptions,
|
|
2054
|
+
) => Promise<unknown>)
|
|
2055
|
+
: undefined;
|
|
2034
2056
|
ctx.defineMethod('resolveJsonTemplate', async (template: unknown) => {
|
|
2035
2057
|
const tokenStore = this.createLocalTemplateTokenStore();
|
|
2036
2058
|
const localResolved = this.resolveLocalFormValuesTemplates(baseCtx, template, collector, tokenStore);
|
|
2037
2059
|
const nextTemplate = localResolved.matched ? localResolved.value : template;
|
|
2038
|
-
const resolved = delegatedResolveJsonTemplate
|
|
2060
|
+
const resolved = delegatedResolveJsonTemplate
|
|
2061
|
+
? await delegatedResolveJsonTemplate(nextTemplate, contractModelUid ? { contractModelUid } : undefined)
|
|
2062
|
+
: nextTemplate;
|
|
2039
2063
|
return this.restoreLocalTemplateTokens(resolved, tokenStore);
|
|
2040
2064
|
});
|
|
2041
2065
|
|
|
@@ -81,6 +81,10 @@ export class FormValueRuntime {
|
|
|
81
81
|
|
|
82
82
|
this.ruleEngine = new RuleEngine({
|
|
83
83
|
getBlockModelUid: () => String(this.model?.uid),
|
|
84
|
+
getAssignRulesModelUid: () => {
|
|
85
|
+
const grid = this.model?.subModels?.grid;
|
|
86
|
+
return !Array.isArray(grid) && grid?.uid ? String(grid.uid) : undefined;
|
|
87
|
+
},
|
|
84
88
|
getActionName: () => this.model?.getAclActionName?.() ?? this.model?.context?.actionName,
|
|
85
89
|
getBlockContext: () => this.model?.context,
|
|
86
90
|
getEngine: () => this.model?.context?.engine,
|